Skip to content

Chapter 7. Linking

Notes on Computer Systems: A Programmer's Perspective (Bryant, O'Hallaron; 3rd ed.), Chapter 7. Linking is the process of collecting and combining pieces of code and data into a single file that can be loaded into memory and executed. It can be performed at compile time (static linkers), at load time, or at run time (dynamic linkers). The discussion targets x86-64 Linux with ELF-64, but the concepts are universal across systems and object formats.

Understanding linking pays off in five ways: diagnosing linker errors in large builds, avoiding the silent-multiple-definition bugs described in §7.6, understanding how language scoping (static, global vs. local) is implemented, grounding later systems concepts (loading, virtual memory, paging, memory mapping), and exploiting shared libraries.

7.1 Compiler Drivers

A compiler driver (e.g., gcc) invokes the preprocessor, compiler, assembler, and linker on the user's behalf. For gcc -Og -o prog main.c sum.c (gcc -v shows the steps):

Stage Program Input → Output
Preprocess cpp main.c → main.i (ASCII intermediate)
Compile cc1 main.i → main.s (ASCII assembly)
Assemble as main.s → main.o (relocatable object file)
Link ld main.o, sum.o, system object files → prog (executable object file)

Running ./prog causes the shell to invoke the loader, which copies the code and data of prog into memory and transfers control to the beginning of the program.

7.2 Static Linking

A static linker takes relocatable object files plus command-line arguments and produces a fully linked executable. It performs two tasks:

  1. Symbol resolution. Associate each symbol reference with exactly one symbol definition. Symbols correspond to functions, global variables, and static variables.
  2. Relocation. Compilers and assemblers generate code and data sections starting at address 0. The linker assigns each symbol definition a run-time memory location, then modifies every reference to point there — blindly, following relocation entries generated by the assembler.

Object files are merely collections of blocks of bytes: code blocks, data blocks, and data structures that guide the linker and loader. The linker concatenates blocks, decides run-time locations, and patches locations within blocks. It has minimal understanding of the target machine; the compiler and assembler have already done most of the work.

7.3 Object Files

Form Contents Produced by
Relocatable object file Code and data combinable with other relocatable files at compile time into an executable Compilers and assemblers
Executable object file Code and data copyable directly into memory and executed Linkers
Shared object file Special relocatable file loadable and linkable dynamically, at load time or run time Compilers and assemblers

An object module is a byte sequence; an object file is a module stored on disk (terms used interchangeably). Formats: early Unix used a.out (the name survives); Windows uses PE; macOS uses Mach-O; modern x86-64 Linux/Unix uses ELF.

7.4 Relocatable Object Files

The ELF header begins with a 16-byte sequence describing the word size and byte ordering of the generating system, followed by the header size, object file type, machine type, and the offset, entry size, and entry count of the section header table, which has one fixed-size entry per section.

Section Contents
.text Machine code of the compiled program.
.rodata Read-only data: printf format strings, switch jump tables.
.data Initialized global and static C variables. Local variables live on the run-time stack and appear in neither .data nor .bss.
.bss Uninitialized globals and statics, plus globals/statics initialized to zero. Occupies no file space — a mere placeholder; allocated in memory and zeroed at run time.
.symtab Symbol table for defined and referenced functions and global variables.
.rel.text Relocation entries for .text: locations to patch when combining with other files (calls to external functions, references to globals). Calls to local functions need no patching.
.rel.data Relocation entries for .data: initialized globals whose initial value is the address of a global or external function.
.debug Debugging symbol table (locals, typedefs, globals, original source). Present only with -g.
.line Mapping between C source line numbers and .text instructions. Present only with -g.
.strtab Null-terminated strings for the symbol tables and section names.

.symtab does not require -g. Every relocatable object file has a .symtab unless it has been explicitly removed with strip. Unlike a compiler's internal symbol table, it contains no entries for local nonstatic variables. Relocation information is unnecessary in executable object files and is usually omitted unless explicitly requested.

Why "bss". Originally the "block started by symbol" directive from IBM 704 assembly (circa 1957). Mnemonic for the .data/.bss distinction: "Better Save Space!" — uninitialized variables need no disk space.

7.5 Symbols and Symbol Tables

Relative to a module m, there are three kinds of linker symbols:

Kind Definition
Global Defined by m, referenceable by other modules: nonstatic functions and nonstatic global variables.
External Referenced by m but defined by some other module.
Local Defined and referenceable exclusively within m: static functions and static global variables.

Local linker symbols are not local program variables — the stack-managed nonstatic locals are of no interest to the linker. Local procedure variables declared static, however, are not kept on the stack: the compiler allocates space in .data or .bss and creates a uniquely named local linker symbol. Two functions each defining static int x yield two symbols, e.g. x.1 and x.2.

Symbol tables are built by assemblers from symbols exported by the compiler into the .s file. An ELF symbol table entry:

typedef struct {
    int  name;       /* String table offset */
    char type:4,     /* Function or data (4 bits) */
         binding:4;  /* Local or global (4 bits) */
    char reserved;   /* Unused */
    short section;   /* Section header index */
    long value;      /* Section offset or absolute address */
    long size;       /* Object size in bytes */
} Elf64_Symbol;

value is a section offset in relocatable modules and an absolute run-time address in executables. type is usually data or function (entries also exist for sections and for the source-file path). Each symbol is assigned to a section via the section index — or to one of three pseudosections that exist only in relocatable object files:

Pseudosection Meaning
ABS Symbols that should not be relocated.
UNDEF Undefined symbols: referenced here, defined elsewhere.
COMMON Uninitialized data objects not yet allocated. value gives the alignment requirement; size gives the minimum size.

Modern gcc assigns relocatable-file symbols by this convention (rationale in §7.6.1):

Destination Symbols
COMMON Uninitialized global variables
.bss Uninitialized static variables; global or static variables initialized to zero

readelf output for the running example's main.o shows main as a 24-byte FUNC GLOBAL at offset 0 of section 1 (.text), array as an 8-byte OBJECT GLOBAL at offset 0 of section 3 (.data), and sum as NOTYPE GLOBAL UND — an external reference.

7.6 Symbol Resolution

The linker associates each reference with exactly one definition. References to local symbols are straightforward: the compiler allows one definition per local symbol per module and gives static locals unique names. For a global symbol not defined in the current module, the compiler assumes it is defined elsewhere, generates a linker symbol table entry, and leaves it to the linker; if no input module defines it, the linker fails with undefined reference to ....

7.6.1 How Linkers Resolve Duplicate Symbol Names

At compile time, every global symbol is exported to the assembler as strong or weak; the assembler encodes this in the symbol table.

Strength Symbols
Strong Functions and initialized global variables
Weak Uninitialized global variables

Linux linkers resolve duplicates by three rules:

  1. Multiple strong symbols with the same name are not allowed (link-time error).
  2. Given one strong symbol and multiple weak symbols of the same name, choose the strong symbol.
  3. Given multiple weak symbols of the same name, choose any of them.

Two modules defining int main() — or both defining int x = 15213 — violate rule 1. Rules 2 and 3 apply silently, which is where the danger lies, especially with mismatched types. With int y = 15212; int x = 15213; in one module and double x; void f() { x = -0.0; } in another, rule 2 resolves both modules' references to the strong 4-byte int x; the assignment in f then writes 8 bytes of the double-precision representation of −0.0 over x and the adjacent y (on the book's system, x at 0x601020, y at 0x601024), printing x = 0x0 y = 0x80000000. The linker emits only an alignment warning, and the failure typically manifests far from the cause. Defenses: gcc -fno-common (multiply-defined globals become an error) and -Werror (all warnings become errors).

Why the COMMON convention exists. When the compiler translates a module and meets a weak global x, it cannot know whether other modules also define x nor which definition the linker will pick — so it defers the decision by assigning x to COMMON. A global initialized to zero is strong, hence unique by rule 2, so the compiler can confidently place it in .bss; static symbols are unique by construction and go confidently to .data or .bss.

7.6.2 Linking with Static Libraries

Related object modules can be packaged into a single static library (archive) file supplied to the linker, which copies only the members the program actually references. Alternatives all fail:

Approach Drawback
Compiler recognizes standard functions itself (the Pascal approach) Adds complexity to the compiler; every library change requires a new compiler version. Infeasible for C's large standard library.
One monolithic libc.o linked into every program Every executable and every running process carries a full copy (libc.a ≈ 5 MB, libm.a ≈ 2 MB); any change forces recompiling the whole library.
One object file per function, linked explicitly Error-prone and time consuming for programmers.

On Linux, static libraries are stored as archives — concatenated relocatable object files with a header describing each member's size and location — with suffix .a:

linux> gcc -c addvec.c multvec.c
linux> ar rcs libvector.a addvec.o multvec.o
linux> gcc -c main2.c
linux> gcc -static -o prog2c main2.o ./libvector.a     # or: -L. -lvector

-static requests a fully linked executable needing no further link work at load time; -lvector abbreviates libvector.a; -L. adds the search directory. Only the referenced member (addvec.o) is copied, not multvec.o. Compiler drivers always pass libc.a to the linker automatically.

7.6.3 How Linkers Use Static Libraries to Resolve References

During symbol resolution the linker scans the relocatable files and archives left to right in command-line order, maintaining a set E of object files to be merged, a set U of unresolved symbols, and a set D of defined symbols — all initially empty.

  • Input is an object file f: add f to E; update U and D from its definitions and references.
  • Input is an archive: match U against the archive's members. Any member m defining a symbol in U is added to E, updating U and D; iterate over members until U and D reach a fixed point. Unused members are discarded.
  • After the last input, a nonempty U is an error; otherwise merge and relocate E.

Ordering is therefore significant. If a library precedes the object file that references it, its members are never pulled in:

linux> gcc -static ./libvector.a main2.c
/tmp/cc9XH6Rp.o: In function `main':
/tmp/cc9XH6Rp.o(.text+0x18): undefined reference to `addvec'

The general rule is to place libraries at the end of the command line. If the libraries are independent, any relative order works; if not, they must be ordered so that each symbol is defined by a library appearing after all references to it, repeating libraries as needed for cycles (gcc foo.c libx.a liby.a libx.a) or combining them into one archive.

7.7 Relocation

After symbol resolution, every reference is bound to exactly one definition and the linker knows the exact section sizes of its inputs. Relocation proceeds in two steps:

  1. Relocating sections and symbol definitions. Merge all sections of the same type into one aggregate section (all input .data sections become the output .data), assign run-time addresses to the aggregate sections and to every section and symbol of the inputs.
  2. Relocating symbol references within sections. Patch every reference to point to the correct run-time address, guided by relocation entries.

7.7.1 Relocation Entries

The assembler does not know where code and data will ultimately reside, nor where external functions or globals live, so for every reference to an object with unknown final location it emits a relocation entry — into .rel.text for code and .rel.data for data:

typedef struct {
    long offset;      /* Offset of the reference to relocate */
    long type:32,     /* Relocation type */
         symbol:32;   /* Symbol table index */
    long addend;      /* Constant part of relocation expression */
} Elf64_Rela;

ELF defines 32 relocation types; two matter here:

Type Meaning
R_X86_64_PC32 Relocate a reference using a 32-bit PC-relative address — an offset added to the run-time PC, which holds the address of the next instruction (see CSAPP §3.6.3).
R_X86_64_32 Relocate a reference using a 32-bit absolute address.

Both types assume the small code model — total code plus data under 2 GB, reachable with 32-bit PC-relative addressing — which is gcc's default; larger programs need -mcmodel=medium or -mcmodel=large.

7.7.2 Relocating Symbol References

The relocation algorithm, with ADDR(s) and ADDR(r.symbol) already chosen:

foreach section s {
    foreach relocation entry r {
        refptr = s + r.offset;    /* ptr to reference to be relocated */

        /* Relocate a PC-relative reference */
        if (r.type == R_X86_64_PC32) {
            refaddr = ADDR(s) + r.offset;  /* ref's run-time address */
            *refptr = (unsigned) (ADDR(r.symbol) + r.addend - refaddr);
        }

        /* Relocate an absolute reference */
        if (r.type == R_X86_64_32)
            *refptr = (unsigned) (ADDR(r.symbol) + r.addend);
    }
}

PC-relative example. In main.o, the call to sum is e8 00 00 00 00 at section offset 0xe: a 1-byte opcode followed by a 4-byte placeholder. Its relocation entry is r.offset = 0xf, r.symbol = sum, r.type = R_X86_64_PC32, r.addend = -4. With ADDR(.text) = 0x4004d0 and ADDR(sum) = 0x4004e8:

refaddr = 0x4004d0 + 0xf                    = 0x4004df
*refptr = 0x4004e8 + (-4) - 0x4004df        = 0x5

Yielding 4004de: e8 05 00 00 00 callq 4004e8 <sum>. When the CPU executes the callq, PC holds 0x4004e3 (the following instruction); it pushes PC and sets PC ← 0x4004e3 + 0x5 = 0x4004e8 — the first instruction of sum. The addend of −4 compensates for the reference beginning 4 bytes before the point the PC-relative offset is measured from (refaddr + 4).

Absolute example. The mov loading the address of array is bf plus a 4-byte placeholder at offset 0x9, with r.offset = 0xa, r.symbol = array, r.type = R_X86_64_32, r.addend = 0. With ADDR(array) = 0x601018, the patched instruction is 4004d9: bf 18 10 60 00 mov $0x601018,%edi. At load time the loader can copy the relocated sections directly into memory and execute without further modification.

7.8 Executable Object Files

The executable's format resembles the relocatable one, with differences: the ELF header additionally records the entry point (address of the first instruction to execute); .text, .rodata, and .data are relocated to their final run-time addresses; a .init section defines the _init function called by initialization code; no .rel sections are needed.

┌────────────────────────────┐
│ ELF header                 │ ┐
├────────────────────────────┤ │
│ Segment header table       │ │  read-only code segment
├────────────────────────────┤ │
│ .init  .text  .rodata      │ ┘
├────────────────────────────┤
│ .data  .bss                │ ─  read/write data segment
├────────────────────────────┤
│ .symtab .debug .line       │ ─  not loaded into memory
│ .strtab                    │
├────────────────────────────┤
│ Section header table       │
└────────────────────────────┘

ELF executables are designed for easy loading: contiguous file chunks map to contiguous memory segments, as described by the program header table. For the example prog:

Segment Permissions vaddr memsz filesz Initialized from
Code read/execute 0x400000 0x69c 0x69c First 0x69c bytes of the file: ELF header, program header table, .init, .text, .rodata
Data read/write 0x600df8 0x230 0x228 0x228 bytes of .data starting at file offset 0xdf8; the remaining 8 bytes are .bss, zeroed at run time

For any segment, the linker must choose a starting address satisfying vaddr mod align = off mod align, where off is the file offset of the segment's first section and align is the alignment in the program header (2²¹ = 0x200000). This is an optimization that lets segments be transferred efficiently through the paging machinery of virtual memory (Chapter 9).

7.9 Loading Executable Object Files

Since prog is not a built-in command, the shell treats it as an executable and invokes the loader (any program can do so via execve, §8.4.6). The loader copies the executable's code and data from disk into memory and jumps to the entry point — the process called loading. The run-time image on Linux x86-64:

2^48   ┌───────────────────────────────┐
       │ Kernel memory                 │  invisible to user code
2^48-1 ├───────────────────────────────┤
       │ User stack (grows down)       │ ◀── %rsp
       │              ↓                │
       ├───────────────────────────────┤
       │ Memory-mapped region for      │
       │ shared libraries              │
       ├───────────────────────────────┤
       │              ↑                │
       │ Run-time heap (via malloc)    │ ◀── brk
       ├───────────────────────────────┤
       │ Read/write data segment       │ ┐
       │ (.data, .bss)                 │ │ loaded from the
       ├───────────────────────────────┤ │ executable file
       │ Read-only code segment        │ │
       │ (.init, .text, .rodata)       │ ┘
0x400000 ─────────────────────────────┤
       │                               │
0      └───────────────────────────────┘

The code segment starts at 0x400000, the data segment follows, the heap follows the data segment and grows upward via malloc, then comes the shared-module region. The user stack starts just below the largest legal user address, 2⁴⁸ − 1, and grows downward; the region from 2⁴⁸ upward is reserved for kernel code and data. (The book's Figure 7.15 labels the stack pointer %esp — an erratum; on x86-64 it is %rsp.) In practice a gap separates code and data segments due to the .data alignment requirement, and ASLR (§3.10.4) randomizes the stack, shared-library, and heap locations per run while preserving their relative order.

Guided by the program header table, the loader creates this image, then jumps to the entry point: always the _start function, defined in the system file crt1.o and identical for all C programs. _start calls __libc_start_main (defined in libc.so), which initializes the execution environment, calls the user-level main, handles its return value, and if necessary returns control to the kernel.

How loading actually works. Each program runs in a process with its own virtual address space. The shell forks a child (a duplicate of the parent), which invokes the loader via execve. The loader deletes the child's existing virtual memory segments and creates fresh code, data, heap, and stack segments; the new stack and heap are zeroed; the new code and data segments are initialized by mapping virtual pages to page-size chunks of the executable file. Aside from header information, no data is copied from disk during loading — copying is deferred until the CPU first references a mapped page, at which point the paging mechanism transfers it automatically.

7.10 Dynamic Linking with Shared Libraries

Static libraries leave two problems: updated libraries require programmers to notice and explicitly relink, and ubiquitous code such as printf is duplicated in the text segment of every running process — a significant waste of memory.

A shared library is an object module that can be loaded at an arbitrary memory address and linked with a program in memory, at load time or run time — a process called dynamic linking, performed by a dynamic linker. Linux shared objects use the .so suffix; Windows calls them DLLs. They are "shared" in two senses: a filesystem holds exactly one .so per library, whose code and data are shared by all referencing executables (rather than copied into each, as with archives); and in memory, a single copy of the library's .text can be shared by multiple running processes (Chapter 9).

linux> gcc -shared -fpic -o libvector.so addvec.c multvec.c
linux> gcc -o prog2l main2.c ./libvector.so

-fpic requests position-independent code (§7.12); -shared requests a shared object. The executable link performs partial linking: none of libvector.so's code or data is copied into prog2l — only relocation and symbol-table information allowing load-time resolution. When the loader loads prog2l, it notices the .interp section containing the path of the dynamic linker (ld-linux.so, itself a shared object) and runs it instead of the application. The dynamic linker then relocates the text and data of libc.so into some memory segment, relocates libvector.so into another, and relocates every reference in prog2l to symbols defined by either. Finally it passes control to the application; the shared libraries' locations are fixed for the duration of execution.

7.11 Loading and Linking Shared Libraries from Applications

An application can also ask the dynamic linker to load and link arbitrary shared libraries while running, without having linked against them at compile time. Real-world uses: distributing software updates (regenerate one shared library; applications pick it up on next run) and high-performance web servers, which package each dynamic-content generator in a shared library, load and call it directly on request — the function stays cached in the server's address space, replacing the old fork/execve-per-request CGI model, and functions can be updated or added without stopping the server.

Function Behavior
void *dlopen(const char *filename, int flag) Loads and links shared library filename; returns a handle, or NULL on error. External symbols are resolved using libraries previously opened with RTLD_GLOBAL; if the executable was compiled with -rdynamic, its own globals are also available for resolution. flag must include RTLD_NOW (resolve external references immediately) or RTLD_LAZY (defer resolution until code executes); either may be OR'd with RTLD_GLOBAL.
void *dlsym(void *handle, char *symbol) Returns the address of symbol in the opened library, or NULL.
int dlclose(void *handle) Unloads the library if no other shared libraries are still using it. Returns 0 or −1.
const char *dlerror(void) Returns a message describing the most recent dlopen/dlsym/dlclose error, or NULL if none.
dll.c
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

int x[2] = {1, 2};
int y[2] = {3, 4};
int z[2];

int main()
{
    void *handle;
    void (*addvec)(int *, int *, int *, int);
    char *error;

    /* Dynamically load the shared library containing addvec() */
    handle = dlopen("./libvector.so", RTLD_LAZY);
    if (!handle) {
        fprintf(stderr, "%s\n", dlerror());
        exit(1);
    }

    /* Get a pointer to the addvec() function we just loaded */
    addvec = dlsym(handle, "addvec");
    if ((error = dlerror()) != NULL) {
        fprintf(stderr, "%s\n", error);
        exit(1);
    }

    /* Now we can call addvec() just like any other function */
    addvec(x, y, z, 2);
    printf("z = [%d %d]\n", z[0], z[1]);

    /* Unload the shared library */
    if (dlclose(handle) < 0) {
        fprintf(stderr, "%s\n", dlerror());
        exit(1);
    }
    return 0;
}

Compile with gcc -rdynamic -o prog2r dll.c -ldl.

7.12 Position-Independent Code (PIC)

Sharing one in-memory copy of library code across processes cannot work by pre-assigning each library a dedicated address-space chunk: the space would be wasted for non-users, chunks must never overlap, modified libraries may outgrow their chunk, and over time the address space fragments into unusable holes — with a different library-to-memory assignment per system. Instead, shared modules are compiled so their code segment loads anywhere without link-time modification: position-independent code, requested with -fpic. Shared libraries must always be compiled with this option. One code-segment copy then serves unlimited processes; each process still gets its own read/write data segment.

On x86-64, references to symbols within the same executable module need no special treatment — they compile to PC-relative addresses relocated by the static linker. References to externally defined procedures and globals need more.

PIC data references — the GOT. Wherever an object module is loaded, its data segment is always the same distance from its code segment: the code-to-data distance is a run-time constant. Compilers exploit this by placing a global offset table (GOT) at the beginning of the data segment, with an 8-byte entry per referenced global object (procedure or variable) and a relocation record per entry. At load time the dynamic linker fills each entry with the object's absolute address. Every object module referencing globals has its own GOT. The code accesses a global indirectly, via a PC-relative reference to its GOT entry whose offset is a run-time constant:

addvec:
    mov    0x2008b9(%rip),%rax    # %rax = *GOT[3] = &addcnt
    addl   $0x1,(%rax)            # addcnt++

Since addcnt is defined in libvector.so itself, the compiler could have used a direct PC-relative reference with a relocation for the static linker; it chose the most general solution — the GOT — for all references, which is necessary whenever the object is defined by another shared module.

PIC function calls — lazy binding. The compiler cannot predict the run-time address of a function defined in a shared library, and a load-time relocation of the reference would modify the calling module's code segment, breaking PIC. GNU systems instead use lazy binding: the binding of each procedure address is deferred until the first call. The motivation is that a typical program calls only a handful of the hundreds or thousands of functions a library like libc.so exports; deferring resolution avoids equally many unnecessary load-time relocations. The first call pays a nontrivial overhead; each later call costs one instruction plus a memory reference.

Lazy binding is an interaction between two structures, present in every object module that calls shared-library functions:

Structure Location Contents
Procedure linkage table (PLT) Code segment Array of 16-byte code entries. PLT[0] is special: it jumps into the dynamic linker. PLT[1] invokes __libc_start_main. Entries from PLT[2] on invoke user-called functions — here PLT[2] is addvec, PLT[3] is printf.
Global offset table (GOT) Data segment Array of 8-byte address entries. With the PLT: GOT[0] and GOT[1] hold information used by the dynamic linker (address of .dynamic, address of relocation entries); GOT[2] is the dynamic linker's entry point in ld-linux.so. Each remaining entry pairs with a PLT entry (GOT[4] ↔ PLT[2] = addvec) and initially points to the second instruction of that PLT entry.

First invocation of addvec:

  1. The program calls PLT[2] (the PLT entry for addvec) rather than addvec directly.
  2. The first PLT instruction jumps indirectly through GOT[4]; since the entry initially points back into PLT[2], control lands on the next instruction.
  3. PLT[2] pushes an ID for addvec (0x1) and jumps to PLT[0].
  4. PLT[0] pushes an argument for the dynamic linker indirectly through GOT[1] and jumps into the dynamic linker through GOT[2]. The dynamic linker uses the two stack entries to determine addvec's run-time location, overwrites GOT[4] with that address, and passes control to addvec.

Every subsequent invocation enters PLT[2] as before, but the indirect jump through GOT[4] now transfers control directly to addvec.

7.13 Library Interpositioning

Library interpositioning intercepts calls to shared-library functions and executes arbitrary code instead — to count calls, validate and trace inputs and outputs, or substitute a different implementation. The pattern: given target function f, create a wrapper whose prototype matches f; interpositioning tricks the system into calling the wrapper, which typically calls the real f, does its extra work, and returns the result. The running example traces malloc(32) and free in a small program int.c.

7.13.1 Compile-Time Interpositioning

A local malloc.h redirects calls via the preprocessor:

malloc.h
#define malloc(size) mymalloc(size)
#define free(ptr)    myfree(ptr)

void *mymalloc(size_t size);
void myfree(void *ptr);

The wrappers (compiled with -DCOMPILETIME and the standard malloc.h) call the real function and print a trace. Build with gcc -DCOMPILETIME -c mymalloc.c then gcc -I. -o intc int.c mymalloc.o — the -I. makes the preprocessor find the local malloc.h before the system one.

The static linker's --wrap f flag resolves references to f as __wrap_f, and references to __real_f as f. The wrapper __wrap_malloc calls __real_malloc (the libc function), traces, returns. Build:

linux> gcc -DLINKTIME -c mymalloc.c
linux> gcc -c int.c
linux> gcc -Wl,--wrap,malloc -Wl,--wrap,free -o intl int.o mymalloc.o

-Wl,option passes option to the linker with each comma replaced by a space, so -Wl,--wrap,malloc passes --wrap malloc.

7.13.3 Run-Time Interpositioning

Requires access only to the executable. If LD_PRELOAD is set to a list of shared-library pathnames (separated by spaces or colons), the dynamic linker searches those libraries first when resolving undefined references — for any function in any shared library, in any executable. The wrappers (built into mymalloc.so with #define _GNU_SOURCE) locate the real function at run time with dlsym(RTLD_NEXT, "malloc"), which returns the next occurrence of the symbol in the search order:

linux> gcc -DRUNTIME -shared -fpic -o mymalloc.so mymalloc.c -ldl
linux> gcc -o intr int.c
linux> LD_PRELOAD="./mymalloc.so" ./intr
malloc(32) = 0x1bf7010
free(0x1bf7010)
linux> LD_PRELOAD="./mymalloc.so" /usr/bin/uptime     # works on any executable
Mechanism Interposes at Requires access to Key tool
Preprocessor macros Compile time Source files #define + -I.
--wrap Link time Relocatable object files -Wl,--wrap,f → __wrap_f, __real_f
LD_PRELOAD Run time (load) Executable only dlsym(RTLD_NEXT, ...)

7.14 Tools for Manipulating Object Files

Tool Function
ar Creates static libraries; inserts, deletes, lists, extracts members.
strings Lists the printable strings in an object file.
strip Deletes symbol table information.
nm Lists the symbols in an object file's symbol table.
size Lists the names and sizes of sections.
readelf Displays the complete structure, including the ELF header; subsumes size and nm.
objdump The mother of all binary tools; displays everything, most usefully disassembling .text.
ldd Lists the shared libraries an executable needs at run time.

7.15 Summary

Linking can be performed at compile time by static linkers and at load or run time by dynamic linkers, operating on relocatable, executable, and shared object files. Its two tasks are symbol resolution — binding each global symbol reference to a unique definition — and relocation — fixing each symbol's ultimate memory address and patching references. Multiple object files may define the same symbol; the silent resolution rules can introduce subtle bugs, as can the left-to-right archive scan. Shared objects are loaded and linked dynamically, either implicitly at load time or on demand through the dlopen interface; position-independent code lets one in-memory copy of library code serve any number of processes.