On Linking, Loading, and C

Reading time: 6 minute(s).
2026-09-19

Ever had troubles linking some library, linking some library statically, or producing a static library? Nothing better than a quick but solid, theoretical refresher of how it works.

This is a draft for feedback.

What Is a Program

Despite all sorts of tricks, computers are "semantically" always running a single program. That program is often your operating system, which can most likely run on single-core processors too. The illusion of running multiple things at once is orchestrated by your operating system, aware of your physical core count.

Different operating system have different ideas of what an executable is on their system, so we'll only look at Linux and UNIX-like systems.

On there, executables use the ELF format. This crucially specifies how to organize and package code into a file: there's a header with important metadata and then different "sections" to store different parts of your compiled binary.

This is an excellent explanation of the ELF format.

A file won't execute unless a program parses this format, maps the relevant sections to memory to allow for execution, and "call" the program into memory. This is what the linux elf loader does, under the hood, every time you run an executable. In this process it'll also need to load and find shared libraries, load those in memory (or find where they're already loaded), and make sure your program jumps to that piece of code when you call some symbol.

The concept of "symbols" is really present in an executable as debug information, but as far as execution is concerned it's just jumps in memory.

What Are Libraries

It was thought to be wise to split up your code in specialized "libraries" that can be shared independently. This was not enough to do in code alone: the ELF specification, besides executable, also defines relocatable files and shared object files, also known as static libraries (.a) and shared libraries (.so).

Shared libraries are meant to be shared by many programs on the same machine during execution, since only a single copy of the (whole) library needs to be loaded this saves a lot of memory. Just like executables, they're elf files that can be inspected sitting somewhere in the /usr/lib/ directory.

Static libraries are meant to be used during the creation of executables and shared libraries. You can usually find them as .a files, which are just archives of said relocatable files sporting an .o extension, again in ELF format. These are the same files that your c compiler of choice usually produces as an "intermediate" object when compiling a library, cluttering your source directories when done carelessly. Just like shared libraries, you can find some in /usr/lib.

/usr/lib> fd -e "a" | wc
   1004    1004   30060
/usr/lib> fd -e "so" | wc
   4014    4014  142527

I myself have a bunch.

Caveats in Sharing Libraries

A library I compile won't necessarily work on your machine. Firstly, our architectures must match for your processor to understand my compiled code. Your operating system must understand the format of my library (e.g. a Linux library won't work on Windows), and our calling convention must match.

The calling convention refers to how, in compiled code, functions expect arguments and how structures are laid out in memory. Both the linker and loader don't care much about your calling convention, but your programs will. You might fail to link the program, run the program, or it might run and corrupt silently, the worst of all outcomes.

In awe and admiration to the C language the only existing calling convention is C's calling convention, with the caveats of the particular platform.

If your program does anything at all it's probably interacting with the system libc instead of calling syscalls directly, so glibc will be dynamically linked to your program (more on this below). This is true for all dependencies, but your installed shared libraries (and most importantly glibc) must be recent enough to run my program.

Caveats in Not Using Libraries

If you have access to a static build of your library or the source code you might decide to statically link the whole thing in your resulting binary. This wouldn't be so bad and most languages will statically link most they can, as you're only bringing inside the executable the functions you need and not the whole library. The compiler can also reason about optimizations better, since it's impossible to reason about the performance of a function call when the function will be dynamically loaded sometime in the future. Having more information available means it can do a better job, the whole source code being the most informative.

You can't statically link glibc, and it would be useless anyway. As a spoiler for later, there are in fact more ways to load dynamic libraries than relying on the loader: you can call the convenient dlopen function, and glibc does just that for DNS resolution. Plus, glibc assumes there is only one copy of itself in memory.

musl is an alternative libc that is entirely embeddable inside programs. But switching entirely to musl isn't entirely straightforward, and glibc is only a single example of a library that's hard to statically link; each sufficiently large library will have some caveats or things to watch out for.

Where Do Libraries come From

The maintainers of your linux distribution painfully decide what's packaged as a library, what's linked statically and what's loaded dynamically. Then they build many binary packages (or static/shared libraries) for all supported mix of architectures and (occasionally) glibc libraries. It's a thankless undertaking that comes with many headaches, including frequent patching of the software itself. They also adapt all software to your distro's choice of directories and standards, but that's beyond this post. Contact with distro-packaged libraries is unavoidable, so it's worth mentioning where they come from when something is seemingly wrong with them.

LD_PRELOAD Trick

The loader can be conveniently hijacked by setting the environment variable LD_PRELOAD to the path of some shared library of your liking. The symbols in this shared library will be loaded before anything else, including libc. The trick is often encountered when trying to change malloc implementation temporarily to some wrapper or a tracing version.

The C Compilation Model

will write next

Header Files

Code Files

Include

Macro Guards

Your Goddamn Build System

top↑ end↓