Proven C Book한국어 GitHub

54 Several files — splitting and linking

What to know first

chapter 16, The general shape of compilation · translation units and linking
chapter 24, Declaring and defining functions · declaration and definition

Looking back

Chapter 24 said “write prototypes at the top of the file and the definitions may be anywhere below — and, more importantly, they may be in another file.” Explaining the grounds for that with chapter 16′s relay?

A. Because the compilation stage works from declarations alone, and it is the linker that finds and joins the bodies (chapter 16). So when compiling one file the contents of other files are not needed at all — all that is needed is the promise that “a function of this name with this signature exists somewhere”, and the vessel that carries that promise is the header file. This chapter sees that division of labour in the flesh.

The need for this chapter, and its context

Chapter 16 watched the four-stage relay, but its last runner — linking — has nothing to do while there is only one file. Now that there are several, this is the only moment to learn what linking actually does. Chapter 24′s distinction between declaration and definition finally earns its keep here.

By the end of this chapter

Every program so far has been one file. Now it grows into several — the division of labour between header and source, the notion of the translation unit, the linkage of names (external and internal), and how to read link errors. Chapter 16′s relay and chapter 24′s distinction between declaration and definition come together here.

The questions this chapter answers

  1. How are link error messages read?
  2. So a link error can suddenly appear while the program is running?

54.1 Translation units and headers

One source file after preprocessing — the result of all the #includes being spread out — is called a translation unit. The compiler always sees one translation unit at a time. A multi-file program means making several translation units into object files separately and having the linker join them into one.

We see the division of labour in a three-file example.

examples-en/ch54/main.c

/* A multi-file example — this file compiles seeing only greet's declaration. */
#include "greet.h"

int main(void)
{
    greet("world");
    printf_count();
    return 0;
}

Output

Hello, world!
greet was called 1 times

greet.h (the header) contains only the declaration — the contract signature. greet.c has the definition, and main.c includes only the header, knows the contract and calls. Compilation happens separately for each, and the linker joins the call to greet with its definition.

The header’s #ifndef GREET_H ... #define ... #endif is an include guard. It is the idiom that makes the contents unfold only once even if the header is attached twice through different paths, preventing things that error when declared twice (struct definitions and the like). In practice it is often replaced by the single line #pragma once (not standard, but supported by all the major compilers).

54.2 Linkage — does a name cross file boundaries?

A name has one more property besides scope (chapter 24) — linkage, that is, whether that name is visible from another translation unit.

That the word static is used with different meanings in chapter 44 (static lifetime) and here (internal linkage) is C’s famous word recycling — inside a function static means duration, at file level it means linkage.

Internal linkage is a basic weapon in practice. C gives the programmer no way to dig a new name space (chapter 55), so every external name meets in one yard; attach static to helper functions and variables used only inside a file and those names never go out into the yard at all — no collisions, and the compiler can optimise more aggressively (knowing that name cannot be called from another file — an honest signal to chapter 13′s editor). This weapon and the rest of the defences against name collisions are the subject of chapter 56.

A common misconception. “Putting a function definition in a header is convenient, so it is fine”

The definition is copied into every file that includes it, so if two files include that header there are two definitions of the same function and the linker raises a multiple definition error (“multiple definition of …”). A header’s role is to carry the contract, not the implementation — declarations in headers, definitions in sources, is the basic form. (There are exceptions: static inline functions and macros are conventionally put in headers, and so are C23′s constexpr constants. Knowing the exception and using it differs from breaking the rule in ignorance.)

Q. How are link error messages read?

A. Two sentences solve most of it. “undefined reference to X” — the declaration was seen but the definition could not be found (chapter 16). A source file was left out of the build, a library was not joined, or the spelling differs. “multiple definition of X” — there are two or more definitions. Common when a definition was put in a header, or a global variable was declared and initialised in a header. That both messages come from the linker, not the compiler is the starting point of the diagnosis — it means the syntax was fine.

54.3 Linking is not one step — three names

So far we have said loosely that “the linker joins things up”. It is in fact three stages, each with a name.1

StageWhat it doesWhen
link-editingmerges object files and binds symbols to addressesat build time — ld
loadingbrings the executable’s segments into the address spaceat run time — the kernel
runtime linkingfinds and brings in shared libraries, joins symbolsat run time — the dynamic linker

Table 55.1

A statically linked program ends after the first two. A dynamically linked one goes through all three. That is why the one word “linking” appears at both build time and run time, and confuses people.

★ One practical fact attaches here. The runtime linker brings shared objects into the address space before main is called, but it does not join function symbols until they areactually called. This is lazy binding. So linking against a library you never call costs almost nothing at start-up.

On Linux, LD_BIND_NOW=1 turns the deferral off and joins everything at start. Use it when the delay just after launch matters, or when you want to learn about a link failure now rather than later.

Q. So a link error can suddenly appear while the program is running?

A. It can, and that is the price of lazy binding. If a code path that calls a missing symbol is first taken after the program has been running for a while, the dynamic linker fails at that moment — the message symbol lookup error.

★ So there is a habit of running once with LD_BIND_NOW=1 before shipping, or of building with -Wl,-z,now for immediate binding outright. Security people prefer immediate binding too, because once binding is finished the table can be made read-only (used together with -Wl,-z,relro).

54.4 The real purpose of dynamic linking — not size but the ABI

The benefit of dynamic linking is commonly given as “the executable gets smaller”. Measured, that is true enough.

BuildSizeWhat it holds
gcc -O2 -o hello hello.cabout 16 KBonly the call to puts. the implementation is outside
gcc -O2 -static -o hello hello.cabout 758 KBit carries the needed part of libc bodily

Table 55.2

But the size is a by-product. The real purpose lies elsewhere. Dynamic linking detaches the program from a particular version of a library. The system promises “these services in this shape”, and the program relies only on that promise. That is why a library can be fixed without rebuilding the program.

This promise is called the ABI (Application Binary Interface). If an API is a promise at the source level, an ABI is one at the binary level — not only what the function is called, but which register the arguments ride in, how a struct is laid out, where the return value goes.

What the ABI fixesExample
calling conventionwhich register each argument goes in, and where the overflow sits on the stack
data layoutalignment and padding of struct members (chapter 45)
sizes of basic typeswhether long is 4 or 8 bytes — it can differ on one CPU
naming ruleswhether symbols get a leading underscore
library versionssoname and symbol versioning

Table 55.3

soname is the part met most often in practice. The number in a name like libfoo.so.2 declares “binary compatibility is kept up to this version”; a change that breaks compatibility raises the number. Old programs still look for .so.2 while new ones look for .so.3, so both can live on one system.

In practice. Static linking did not die

Unix literature of the 1990s treated static linking as effectively obsolete, the ABI’s advantages being so great. ★ Yet static linking has come back, for reasons different from then.

Containers and distribution. A program you deploy by copying one file is overwhelmingly simpler to ship. There is no question of whether that machine has that library at that version.

Reproducible builds. Getting the same bytes from the same source means pinning dependencies, and dynamic linking leaves the result to the run-time environment.

Small libc implementations. Ones like musl are designed with static linking in mind and produce results far smaller than the 758 KB above.

So today’s judgement is not “which is right” but “what are you buying” — if you want the system libraries’ security updates automatically, dynamic linking is better; if simplicity of distribution and reproducibility matter more, static is. ★ The security-update point is the big one: a statically linked program has to be rebuilt and redeployed whenever a hole appears in libc.

54.5 Position-independent code (PIC) — why libraries must be built this way

A shared library has a problem to solve. The same library lands at a different address in each process. Yet addresses inside machine code have to be somewhere.

The naive answer is “fix up the addresses in the code when loading”. That answer has a fatal cost. ★ The moment you patch a code page, that page can no longer be shared. If eight processes use one library, the code should exist once in physical memory; patch it differently in each and there are eight copies. The very reason shared libraries exist collapses.

Hence position-independent code (PIC). The idea is simple — never patch the code; gather every address that needs fixing into a “table”. A table is data, so having one per process costs little.

WhatTableShape of the indirection
global data accessGOT (Global Offset Table)read the address from the table → go there
function callPLT (Procedure Linkage Table)go by way of a small relay

Table 55.4

It is plain when seen for real. Here is int counter; int bump(void){ return ++counter; } built with -fPIC -shared and taken apart with objdump (x86-64).

bump:
  mov 0x2ec9(%rip),%rdx     <- read counter's address out of the GOT
  mov (%rdx),%eax           <- read the value at that address
  add $0x1,%eax
  mov %eax,(%rdx)

Two reads. The very structure that split arrays from pointers in chapter 40 — “read the address first, then read the value” — returns here. The cost of PIC is, in the end, that one step.

The function side goes through the PLT.

bump@plt:
  jmp *0x2fc2(%rip)         <- jump to the GOT entry
  push $0x1                 <- if not yet joined, fall through to here
  jmp  <dynamic linker>     <- the linker fills the address in and jumps again

Those two lines are the machinery of lazy binding. On the first call the dynamic linker is summoned to fill the GOT entry; after that a single first jmp goes straight there.

Platform note. Today’s circumstances — what changed since the 1990s

Older literature wrote “PIC is a little slower, so do not use it for executables”. Several things have changed.

① On x86-64 the cost fell sharply. RIP-relative addressing arrived, removing the 32-bit era’s separate work of finding out where one is. The 0x2ec9(%rip) above is exactly that.

② It is no longer optional. Try to make a shared library on x86-64 without -fPIC and the linker refuses — literally: relocation R_X86_64_PC32 against symbol 'counter' can not be used when making a shared object; recompile with -fPIC

③ Executables are built position-independent too. Most distributions today build executables as PIEs (position-independent executables). The reason is not performance but security — the load address of the executable itself must be randomised for ASLR to be worth much.

④ The old option names can be forgotten. The Sun compiler’s -K pic, and -G for making a shared object, are -fPIC and -shared in today’s GCC and Clang.

A few knobs are worth knowing. -Bsymbolic and -fno-semantic-interposition keep a library’s internal calls from being intercepted from outside, reducing the indirect cost; --as-needed records no dependency that is not actually used.

54.6 When names collide they get replaced — interpositioning

The previous section showed that external linkage is the default. Meet that with dynamic linking and something surprising happens. A function I wrote can quietly stand in for a library function.

This is interpositioning. The dynamic linker searches for a symbol in a fixed order and uses the first one found. So if my program has a global function named malloc, even the malloc calls made inside the library come to mine.

★ Two things must coincide for this to become an accident. One is that functions are global by default; the other is that the compiler does not treat the redefinition as an error. The latter is C’s old attitude — it assumes the programmer meant it.

Counter-example. Using a common name for a global function

/* in my file, with no static */
char *mktemp(char *tpl) { ... }      /* a name the standard library also uses */
int   index(int i)      { ... }      /* an old Unix function name */

Such names may already be in use by the standard or a system library. When they collide, my function intercepts even the library’s internal calls. The symptom usually appears somewhere else entirely — a function I never called misbehaves.

The cure is the discipline of the previous section. If it is not going out as an interface, mark it static. And give what does go out a prefix (chapter 55). Avoiding the shapes of name the standard reserves belongs here too.

In practice. Where interpositioning becomes a tool

There are places where this property is used on purpose. Linux’s LD_PRELOAD environment variable says “search this library before anything else”, so a particular function can be swapped without touching the program.

Several uses are legitimate — tools that wrap malloc/free to track leaks, test harnesses that fake system calls, pinning the time functions to make tests reproducible.

★ But the same property is dangerous: it means another program’s behaviour can be changed from outside, so the variable is ignored for setuid programs. Judgement about such “powerful and dangerous knobs” follows chapter 12′s ladder — write down what you buy and what you pay, then use it.

We have learned how to cross file boundaries. But #include and #ifndef appeared again in this chapter — the layer chapter 16 passed over with “the preprocessor is a text tool that does not know C”. The next chapter opens that layer head on, and also sees the formal stages by which source code becomes a program.

Notes

  1. These three names and the distinction follow the standard account of linking and loading. Particularly helpful for clarifying them were John R. Levine, Linkers and Loaders (Morgan Kaufmann, 1999), the ELF sections of the System V ABI, and ld.so(8). For a 1994 presentation of the distinction aimed at C programmers, see Peter van der Linden, Expert C Programming, in the chapter on linking.