Linkers, Loaders & Object Formats

position-independent code

/ PIC, said pick /

A shared library will be loaded at different memory addresses in different programs — there is no single fixed home for it, because two libraries might both want the same address. So the code inside a shared library cannot bake in absolute addresses like 'jump to 0x401000'. Position-independent code is code written so that it runs correctly no matter what base address it is loaded at: every jump, call, and data access is expressed relative to the current location or routed through a small per-instance table, never as a fixed absolute number.

The trick has two parts. For reaching code and nearby data, PIC uses PC-relative addressing: instead of 'load from absolute address 0x404000', the instruction says 'load from a spot 0x3000 bytes ahead of the current instruction pointer (rip)'. Because the offset is relative, it stays correct wherever the whole block is placed. For reaching global variables and functions in OTHER modules — whose addresses are only known at load time — PIC adds one level of indirection: it looks up the real address in a per-process table called the global offset table (GOT), which the dynamic linker fills in once at startup. So the code itself contains no module-specific absolute addresses; only the GOT does, and the GOT is the only thing that needs load-time fixups. You ask the compiler for this with the flag -fPIC.

It matters because PIC is what makes a single copy of a shared library's code pages shareable across every process that uses it: the code is identical and read-only, so the kernel maps the same physical pages everywhere, and only the small writable GOT differs per process. The cost is a little indirection and slightly larger code. A common misconception is that PIC means 'no relocations at all' — it means FEW relocations, concentrated in the GOT, instead of scattered through every instruction. On 64-bit x86 the PC-relative addressing is cheap, which is part of why position-independent executables became the default.

$ gcc -fPIC -shared -o libmath.so math.c # Reaching a local variable, PC-relative (no absolute address): lea rax, [rip + 0x2f01] ; address of a nearby global # Reaching an external symbol, through the GOT: mov rax, [rip + foo@GOTPCREL]

PIC reaches nearby data with rip-relative addressing and reaches other modules' symbols indirectly through the GOT, so no absolute addresses are baked into the code.

PIC does not eliminate relocations; it concentrates them in the GOT so the code pages themselves stay address-independent and shareable. Shared libraries on Linux essentially must be PIC; a non-PIC .so would need text relocations, which break code sharing and are disallowed by modern toolchains.

Also called
PICposition-independent code與位置無關的程式碼