Back to course

Position Independence

24 minadvancedModule 3: The FixupContinues from obj-reloc-tables
24 min · Module 3: The Fixup · Advanced

Why This Matters

Everything so far in this module has been about a fixup's arithmetic. This concept is about something more consequential: which fixups you emit in the first place. That choice is what “position independence” means at the object-file level, and it is a decision a compiler makes, not a property a format has.

Put plainly: a non-position-independent object asks the linker for absolute addresses, and therefore requires the linker to know where everything will be. A position-independent object asks for distances, and therefore works anywhere. That is the whole idea, and it is entirely expressed in the relocation types. No new attribute, no flag in the file header, no structural difference — the same mechanism, asked a different question.

It also matters for a reason the previous concepts did not prepare you for. Changing the relocation type changes the instruction encoding, which changes every offset after it. So position independence is not a flag a linker can ignore or a property it can add later. By the time a linker sees the file, the decision has been made and baked into the byte offsets.

A Simple Model

Two questions a relocation can ask, and the answer determines whether the code can be moved:

AN ABSOLUTE RELOCATION asks: "what is the address of this symbol?" the answer is a fixed number once layout is done -> the code is welded to that layout -> it CANNOT be moved afterwards A RELATIVE RELOCATION asks: "how far is this symbol from HERE?" the answer is a distance, and distances survive translation -> the whole object can be shifted and still work and a third kind, for shared libraries specifically: A GOT-RELATIVE RELOCATION asks: "how far is this symbol's GOT slot from HERE?" -> one extra indirection, and now the DATA is relative too, without needing a distance from here at all

And that third kind is the interesting one, because it is the only one that solves a problem the second kind cannot. Being able to move the object is not the same as being able to move its data independently of the code. Consider a global variable: a PC32 reference to it is a distance from the instruction to the variable, and that distance is stable only as long as the code and the data move together. They do not, in a shared library, because the data has to be writable while the code is not — and on a system with W^X they cannot even be on the same page.

So the data must be addressed through a table of addresses that the loader can fill in with wherever things actually landed. That table is the Global Offset Table, and referencing through it is what buys full independence. The cost is one memory indirection on every global access, and the benefit is that a library can be loaded at an address nobody chose in advance.

The Actual Detail

One source file, one machine, three builds. The .text relocations from each, from llvm-objdump-21 -r:

  OFFSET (no flag)   OFFSET (-fPIC)    TYPE (no flag)      TYPE (-fPIC)

  0x04              0x05             R_X86_64_PC32         R_X86_64_REX_GOTPCRELX
  0x0a              0x0e             R_X86_64_PC32         R_X86_64_REX_GOTPCRELX
  0x21              0x21             R_X86_64_PLT32        R_X86_64_PLT32
  0x33              0x33             R_X86_64_PC32         R_X86_64_REX_GOTPCRELX

  .data reloc:     R_X86_64_64       in all three builds
  internal calls:  R_X86_64_PC32 x3   in all three builds

Three rows change under -fPIC and one does not, and the pattern is precise.

The calls do not change

The call at 0x21 is PLT32 in all three builds, and so are the three internal calls. The call path is already indirection-based, so position independence costs it nothing. A call to an undefined function must go somewhere the linker decides, and that somewhere is the PLT; a call to a function in the same object is a direct relative branch, and a direct relative branch is already position-independent.

Compare that to the previous concept's data reference, which is an absolute address by default. Calls were position-independent before anybody asked, and data was not. That asymmetry is the whole reason -fPIC changed exactly three relocations out of eight.

The data references all become GOT-relative

All three global data references — global_counter, uninitialised and message — switch from PC32 to REX_GOTPCRELX. And the name is worth reading carefully, because it is three relocations wearing a trenchcoat:

And the REX prefix is why the offsets moved. The data reference at 0x04 became 0x05, and the one at 0x0a became 0x0e — a shift of one byte and then four. The instruction got longer, because the GOT-relative encoding needs a prefix byte the plain RIP-relative form does not. Everything after the first one slid along with it.

-fPIE is identical to no flag, and that is the surprise

The third build used -fPIE, and its relocation set is byte-for-byte the same as the build with no flag at all. Same types, same offsets, same eight records.

That is genuinely counter-intuitive if you have absorbed the slogan “PIE means position independent”, and the reason is that the two flags are solving different problems with different amounts of freedom.

-fPIC build a SHARED LIBRARY. It may be loaded at ANY address, chosen by whoever loads it, on a machine nobody pictured when it was compiled. -> needs the GOT for its data. COSTS an indirection per global access. -fPIE build an EXECUTABLE that is position independent. The executable is the FIRST thing mapped, and the loader may place it near its link address. -> its own data moves WITH it, so PC-relative distances are already correct. -> COSTS NOTHING over the non-PIE build. so: "position independent" is two different requirements, and only the harder one pays.

And that is the finding worth taking away: the expensive one is for libraries, not for executables. A PIE executable is a hardening measure — it defeats fixed-address exploits by making the code unreadable until it is mapped. A shared library is a deployment requirement, because its address is not knowable when it is built. Same word, same mechanism, completely different cost, and the reason is who chooses the load address.

One more measured detail. The .data relocation is R_X86_64_64 in all three builds — a 64-bit absolute reference to the string literal, unchanged by -fPIC. That looks like an oversight and is not. The 64-bit field can hold any address on any platform, so a PIC library still links correctly if loaded at an address above 4 GB; the only thing that would break is a load below the link address, and that cannot happen for a library the linker cannot relocate. On a 64-bit target, an absolute 64-bit field is already position-independent in practice, which is why the cost of PIC shows up in the 32-bit code references and not in the data.

A Real Example

What a code generator actually has to decide, and why the decision is not optional. The choice is per-reference, not per-file:

a code generator emitting a reference to symbol S from instruction at address P must choose: 1. a CALL, to a function: if S is defined in this object -> PC-relative. Already independent. No cost. if S is not defined here -> PLT-relative. The linker may need a PLT entry. No cost. 2. a DATA reference, to an object: for a PIE executable -> PC-relative. The data travels with the code. No cost. for a shared library -> GOT-relative. One indirection. THAT is the price of PIC. 3. a reference within a section, to a local or a section symbol: PC-relative, always. Never needs the GOT.

Row 3 is the one that makes PIC cheap, and it is easy to miss. References to something inside the same object — a static variable, a string literal's own section, a local label — are always relative, because the linker controls both ends and can compute the distance exactly. The GOT is only needed for references that leave the object. That is why a typical library's .text has a handful of GOT relocations rather than hundreds, and it means the real cost of PIC scales with the library's external surface rather than its size.

And the failure mode, which is worth stating because it is silent in a way that matters. A non-PIC object linked into a shared library works, and then breaks the moment the library is loaded somewhere inconvenient. The absolute addresses were baked in at link time; if the loader honours them, fine; if it cannot, the library is rejected or, worse, mapped and the data references point into whatever happens to be there. Nothing in the object file records that it was built non-PIC — there is no flag, no bit, no attribute. The only evidence is the absence of GOT relocations, which is exactly the kind of fact a file does not defend.

So the same rule from the COMMON concept applies, and it is worth naming as a general property of link-time facts. Whether an object is position-independent is not recorded — it is inferred from the relocation types present, and the inference is a negative one. A tool that wants to know must look for the presence of an absolute relocation, because absence is the signal. That is much harder to check than reading a flag, it cannot be cross-validated against a second field, and it fails silently when a producer changes its defaults. Compare the ELF e_type field, which records "relocatable" explicitly, and compare a hypothetical format that recorded "position independent: yes". Neither exists, in either format, and the reason is the same in both: position independence was a compiler flag long before it was a file-format concern, so it never got a field.

The connection to the previous concept is exact and worth stating in one line: the TYPE flag and PIC are the same fact seen from two sides. An absolute relocation is flagged TYPE because the value must fit its field — and in a 32-bit field, a position-independent load address may not fit. That is why a 32-bit absolute relocation in a PIE is not merely inelegant but fatal, and it is why hardened-binary checks look for the presence of those relocations as evidence that a binary is not position-independent. A flag in a relocation table, a compiler command-line option, and a security property are the same thing at three layers.

Try It Yourself

$ clang -target x86_64-pc-linux-gnu -O1 -c -o a.o demo.c
$ clang -target x86_64-pc-linux-gnu -O1 -fPIC -c -o b.o demo.c
$ llvm-objdump-21 -r a.o
$ llvm-objdump-21 -r b.o

Check Your Understanding

Without looking back: the same source compiled with -fPIE and with no flag at all produces byte-identical relocation sets, while -fPIC changes three of the eight. Under -fPIC the relocation at 0x04 moves to 0x05. Why does -fPIE cost nothing, why does changing the relocation type move a later relocation's offset, and what is the practical consequence of the first fact for a tool that wants to know whether an object is position independent?

Apply It

You are writing a build tool that must decide whether each object in a project is safe to put into a shared library. Your current check reads e_type from the ELF header and accepts anything that is ET_REL. It passes a library that then fails to load on a machine where the loader cannot honour its absolute addresses, and it has no way to predict which objects will cause that. What is the check the object file actually supports, what is its fundamental weakness, and what is the practical mitigation given that you cannot change the producers?

The general habit: when a property is not recorded, stop inferring it and constrain it at the point where it is known. A negative inference from a file is a guess; a build-time assertion is a fact.

Connect

This concept is where the previous one's table stops being a reference. The Object Relocation Sets gave 44 and 147 classified entries and said the choice among them is what position independence means; this is that claim paid off. Three entries changed and the reason is a data-structure decision — one table the loader can rewrite, versus distances that survive translation. And the TYPE flag's overflow check, which looked like a safety measure, turns out to be the mechanism by which a position-independent load is refused when a 32-bit field cannot hold it.

The GOT connects to the section-table work in a way that is easy to miss. The GOT is a section, allocated by the linker, containing pointers the loader rewrites — and that is the same pattern as the .plt stubs, the .dynamic table and the .got.plt array that the segments concept found appearing only in the linked output. The linker generates structures that no compiler emitted, and the GOT is the one PIC specifically requires. Compare the non-PIC build, which needs a .plt for its calls and no .got for its data — the file's contents differ by a whole section depending on this concept's subject.

The REX prefix is a small, instructive anomaly and it connects to instruction encodings directly. An encoding can be genuinely ambiguous, and a format's answer is to add a byte that says which reading is intended. That is a different answer from the one ELF gives everywhere else — a separate relocation type, a separate flag — and it exists because of one opcode-reuse accident in 1980s x86. It is the clearest example in the course of a format carrying a historical scar, and it is a reminder that reading a relocation type's name tells you very little about its mechanics. A linker author has to read the specification for that one rather than generalising from the other 43.

On the security side, the connection is direct and it is the same thread the relocation tables concept pulled. A non-PIE executable has fixed, predictable addresses, which is the precondition for a large class of exploitation technique. That is why “is this binary position independent” is a question distributions, auditors and hardening tools ask — and it is answered by looking for exactly the absolute relocations this concept taught you to recognise. The relocation table is simultaneously a linking concern, a compiler flag, and a security property, and understanding why is the difference between knowing a flag exists and knowing what it costs.

And the “not recorded” theme is the last connection, because it is the course's running argument rather than this concept's. Object files do not record whether they are position independent, whether they use COMMON, or which compiler produced them — yet all three are facts a tool needs. The format's discipline is to record only what a linker needs, and to leave everything else to be inferred from the structure. That is a coherent design and it has a cost, and the cost is that every consumer beyond the linker has to guess. The ELF course's discussion of .gnu.linkonce and section groups is the same trade in the opposite direction — an explicit mechanism for recording a merge decision — and comparing the two is the clearest way to understand where a format's priorities are.