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.
Two questions a relocation can ask, and the answer determines whether the code can be moved:
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.
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 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.
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:
REX is an x86-64 prefix byte, not part of the relocation at all. It is a marker the linker needs for a historical reason: the mov form that can use a GOT-relative operand was originally a call opcode, so the linker has to know it is patching a mov. The prefix is the format's way of saying "this particular encoding is ambiguous, here is how to disambiguate it."GOTPCREL is the arithmetic: PC-relative, and the value it computes is the address of a GOT slot rather than the symbol's address.X marks the ambiguity above.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.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.
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.
What a code generator actually has to decide, and why the decision is not optional. The choice is per-reference, not per-file:
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.
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.
$ 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-fPIC, -fPIE, and diff the relocation output. Then assert that -fPIE and no-flag are byte-identical and check whether the files themselves are identical too — which they may not be, because -fPIE can still change codegen without changing relocations, and finding out which is a good exercise.llvm-objdump-21 -d on a.o and b.o, and find the instruction at 0x04 in each. Count the bytes. The GOT-relative form should be one byte longer because of the REX prefix, and seeing that prefix appear is the moment the concept stops being abstract.static variable and a string literal, compile with -fPIC, and check that neither produces a GOT relocation. Then add one extern variable and watch exactly one appear. The cost of PIC scaling with external surface rather than size is the practical lesson.clang -shared over a non-PIC object. Note whether it errors, warns, or succeeds — and if it succeeds, check whether the data references are absolute. Then write a tiny program that loads the library and reads the global. This is the failure mode the concept warns about, reproduced rather than described.-fPIC and count them. Compare against the number of distinct external symbols it references, not against its line count. The ratio is the interesting number and it is usually far smaller than expected.clang -target aarch64-linux-gnu -fPIC -c and count relocations. Expect roughly double, from the ADRP/LO12 pairing, plus a GOT base register to set up — and then work out which register holds the GOT pointer and which relocation set it. That is where obj-arch-table becomes necessary, and seeing the need is the right way to meet it.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?
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.
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.