Object files, symbols, and relocations bridge separate compilation and executable images.
By the end: Explain undefined symbols, relocation, linking, loading, and the limits of source-level type checking across boundaries.
Separate compilation leaves unresolved addresses
A compiler can emit an object file before knowing where every function or global will reside. Symbol information names definitions and references. Relocation records describe places that must be adjusted when addresses or relative displacements become known. An object file is therefore more than raw instruction bytes.
A linker combines objects and libraries, resolves symbols, lays out sections, and applies relocations according to the target format and rules. Missing definitions can cause unresolved-symbol errors; conflicting definitions can also fail. The exact treatment of weak symbols, visibility, and duplicate definitions depends on the platform.
Keep symbol identity separate from final address
Imagine separately compiling a caller and a helper function. The caller's object can contain instructions for its own arithmetic while still lacking the helper's final address. Instead of guessing that address, the object records a reference to the symbol helper and a relocation describing how a particular instruction field depends on it. The helper's object records a definition at an offset within its code section. These records let compilation finish before the linker chooses how the two sections occupy the final image. No source execution has occurred yet.
Use byte offsets to make the distinction concrete. Suppose helper begins twelve bytes into its object's code section. If the linker places that section at address 400, helper's address becomes 412. Placing the section at 600 instead makes the address 612 without changing the symbol's section-relative offset. The name identifies a definition under the linker's lookup rules; the layout turns that definition into an address. A relocation combines the selected definition with an encoding rule, so finding the name and computing the field are separate steps.
The section offset stays fixed when placement changes
Byte geometry uses 8 SVG units per byte within each section. A 24-byte schematic section is shown twice. The symbol is 12 bytes after the section base; absolute addresses change with placement.
A useful object therefore contains more than the bytes that will eventually execute. Sections group code and data with placement requirements. Symbol records describe definitions and unresolved references, while relocation records identify dependent locations and calculation kinds. Some formats keep additional metadata for debugging or linking. The exact fields vary by format, and an object can contain information not copied into the final executable. Treat the conceptual records below as a teaching inventory, not a byte-for-byte specification of ELF, COFF, or another real object format.
Now introduce a second definition named helper in another input. A linker needs platform rules to decide whether that is an error or whether symbol strength and visibility select one definition. Conversely, a reference with no usable definition may remain unresolved and stop the link. These outcomes are not lexical or parsing errors in the caller's source. They concern agreement among separately produced inputs. Successful source compilation only establishes the compiler's checks for that translation unit, not the availability of every definition required by the final program.
Work through the records as a small linking exercise. Select the helper definition at section offset twelve, choose section base 400, and derive address 412. Change only the base to 600 and identify which records need not change. The section-relative definition remains twelve, while address-dependent patches may differ. Then remove the defining object and explain why the caller's machine bytes alone cannot establish the missing helper's behavior. A numeric zero placeholder in an instruction field is not evidence that address zero is the intended target.
This separation also explains why a linker cannot generally recover source intent from symbol spelling. The same readable name can accompany incompatible implementations, and internal symbol names may differ from source names. A linker follows its object-format and lookup rules rather than rerunning the caller's entire type checker. Richer metadata can support extra checks or link-time optimization, but it does not make every link a proof of semantic compatibility. Keep the definition selected by lookup separate from the contract that the caller expects it to satisfy.
Separate unresolved references from patch rules
Conceptual record
Example information
Question answered
Caller section
Instruction bytes with a dependent field
Where is the field?
Caller symbol reference
helper is undefined in this object
Which definition is needed?
Helper definition
helper at code-section offset 12
Where within its section?
Chosen section placement
Helper section base = 400
Where is the section in the image?
Relocation record
Site, symbol helper, calculation kind
How must the field be patched?
Conceptual records, not a specific object-file encoding. The selected helper address is 400 + 12 = 412; the field value still depends on its relocation kind.
Loading is another responsibility
A loader maps a program and its required data into a process and arranges startup. Dynamic linking can resolve some references through shared libraries at load time or later. Static linking incorporates relevant code differently, but neither phrase alone specifies every platform detail.
For a toy PC-relative branch whose displacement is measured from the address after a four-byte instruction, a call at address 100 to a target at 160 needs displacement 160 − 104 = 56. Real relocation types define their own bases, addends, widths, and overflow rules. The arithmetic example is not an encoding for a named CPU.
Derive the displacement from its specified base
Our hypothetical call instruction occupies four bytes and interprets its signed displacement relative to the address immediately after the instruction. Put the call at byte address 100. Its bytes occupy addresses 100 through 103, so the next instruction begins at 104. To reach target 160, the encoded displacement must be positive 56. Starting at the call's first byte would give 60 instead, and execution would land four bytes too far forward. The choice of base is part of the format's definition, not an arithmetic convention that can be guessed.
A backward call uses the same rule. A call at address 200 ends just before 204. Reaching target 150 therefore requires displacement negative 54. Check the result by adding negative 54 to 204 and recovering 150. The sign says which direction to move along byte addresses; it does not specify whether the call is valid under language rules. Nor does this arithmetic establish how many bits store the displacement or how the processor represents that field. Those are further constraints for a concrete instruction format.
Measure from the exclusive end of the call
Addresses and displacements are bytes. Each horizontal row uses 4 SVG units per byte; vertical separation has no numerical meaning. Colored intervals represent four-byte instructions, and arrowheads identify target direction.
For a separate encoding exercise, assume the field is an eight-bit signed byte displacement with no scaling or reserved values. Its permitted range is negative 128 through positive 127. From base 104, target 231 needs positive 127 and fits exactly. Target 232 needs positive 128 and does not fit. Target negative 24 would need negative 128 mathematically, though whether such an address is meaningful depends on the address space. The range calculation checks representability only; a real linker must also apply its actual target's relocation rules.
Layout changes can preserve or alter a relative displacement. Move both the call at 100 and its target at 160 forward by 1000 bytes. Their new addresses are 1100 and 1160, and the following-instruction base becomes 1104. The displacement remains 56 because the common shift cancels. Move only the target forward by sixteen bytes instead and the displacement becomes 72. This explains why some relative references survive a common rebasing, without establishing that every reference in an image is relative or that a whole executable needs no relocation.
Use the address geometry below to check the rule without relying on memorized signs. Mark the four-byte instruction interval first, place its exclusive end as the base, and only then measure toward the target. Predict the displacement for call address 300 and target 280. The base is 304 and the target lies 24 bytes behind it, so the displacement is negative 24. Reconstruct the target by adding the displacement back to the base. This inverse check catches both sign errors and the common mistake of using the instruction's start.
In a real object format, the relocation type can specify a different base, a symbol addend, a scaled field, or restrictions on the result. Some instruction sets require additional sequences when a direct displacement cannot represent the destination. The linker and target backend must agree on those details. Our invented four-byte instruction is only an address-calculation model, not native code generated by the lesson. The expression compiler emits stack bytecode and cannot test relocation overflow, choose a longer machine instruction, or execute these hypothetical calls.
Check the base, then check representability
Call / target, bytes
Base / displacement, bytes
Fits invented signed 8-bit field?
100 / 160
104 / +56
Yes
200 / 150
204 / −54
Yes
100 / 231
104 / +127
Yes; upper boundary
100 / 232
104 / +128
No
300 / 280
304 / −24
Yes
1100 / 1160
1104 / +56
Yes; common shift cancels
Four-byte calls, next-instruction-relative displacements, no scaling, and an invented −128…127 field range. This is representability arithmetic, not a real CPU encoding.
All addresses and offsets in this example are measured in bytes. t is the target address, a the call instruction’s address, s its length, and d the signed displacement from the following instruction. Real relocation types may use another base and additional addends.
// Hypothetical relocation, not a real CPU encoding
call site =100
instruction size =4
target address =160
displacement =160-(100+4)=56
Artifact
Relevant information
Object A
Call reference to symbol helper
Object B
Definition of helper
Relocation record
Where and how to patch the reference
Linked image
Layout and resolved references
Some platforms defer selected resolutions to dynamic linking; the exact rules vary.
Distinguish image layout from process startup
Linking constructs an output image under a chosen layout and resolution policy. Loading makes an executable image available to a process and arranges the conditions for startup. A teaching diagram can show code and data mapped into a process address space, but mapping is not the same operation as evaluating source expressions. The loader needs information about regions and permissions rather than an AST. Startup code can then prepare runtime state before invoking the program's entry behavior. The precise division of responsibilities depends on the operating system and executable format.
Static linking commonly incorporates needed library code into the linked result. Dynamic linking leaves some relationships to shared components for resolution when the program starts or later. Neither description means that the process already contains every required runtime state at link time. Memory for active invocations and heap allocations belongs to execution. Dynamic linking need not involve a network. A shared library can reside on local storage and still be missing or incompatible on another deployment machine.
Take a hypothetical image whose layout gives helper an offset of 160 bytes from the image base. Loading it at base 1000 places helper at address 1160; loading at base 5000 places it at 5160. An absolute stored address may need adjustment under the format's relocation rules. An internal relative reference can remain unchanged if both endpoints move together, as in the preceding calculation. References across independently placed components require their own mechanisms. Do not generalize the cancellation for one internal call into a guarantee that every address-dependent field remains valid.
One image offset, two process addresses
Two possible placements of the same hypothetical image. Each ruler uses 1.5 SVG units per byte relative to its own base, not one shared absolute address axis. Section colors describe conceptual permissions, not a platform mapping specification.
Permissions make another distinction visible. A conceptual code region may permit execution and reading while a data region permits writing and reading. These permissions describe allowed process memory operations, not whether source-level types are correct. A correctly mapped function can still receive incompatible arguments, and a correctly linked data symbol can still refer to a structure with an unexpected layout. Conversely, a mapping or dependency failure can prevent startup before the application reaches the function whose source compiled successfully. Diagnose the failing responsibility before changing arithmetic code.
Explore the loading comparison by holding the object definitions fixed and changing only the image base. Compute helper's absolute address in each column, then identify the internal relative call that keeps its displacement. Next imagine that helper instead comes from an independently positioned shared component. The earlier common-shift proof no longer applies automatically because the endpoints need not move by the same amount. Explain what additional resolution information the implementation would need. The table describes conceptual responsibilities rather than a runnable loader or a platform-specific dynamic-binding algorithm.
Deployment checks should therefore preserve the executable, required components, and relevant versions as one reproducible case. A successful link on the build machine does not show that a loader elsewhere can locate compatible dependencies. A successful startup does not prove that every later optional component will load or that every deferred reference has been exercised. These are specific limits on the evidence, not reasons to treat all linking as uncertain. Record which image was produced, which dependencies were selected, and at which stage the observed failure actually occurred.
A successful stage does not certify the next one
Observed evidence
What it supports
What remains unchecked
Source compilation succeeds
Accepted translation unit under chosen checks
Definitions in other inputs; deployment
Link produces an image
Inputs satisfy the linker's resolution and layout rules
Dependency availability on another machine
Loader reaches startup
Required initial mappings and resolutions succeeded
Deferred components and later execution
One helper call returns correctly
That call worked for those conditions
Other inputs, layouts, and ownership paths
Conceptual stage responsibilities. The precise checks and any deferred symbol binding depend on the platform.
Agreement across modules is more than a name
Two pieces can agree on a symbol spelling while disagreeing about calling convention, argument representation, structure layout, or ownership. A linker may not have enough type information to detect that mismatch. Foreign-function interfaces and binary compatibility require explicit agreements.
Link-time optimization can expose more program structure across object boundaries, but it does not abolish ABI or language rules. Build flags, target versions, generated code, and library versions belong in an investigation. A source file compiling successfully does not prove that the assembled program is valid or deployable.
Check the binary contract beyond symbol lookup
Suppose two pseudocode modules agree on the symbol name helper but disagree on argument placement. The caller puts its integer argument in location P, while the callee reads location Q. Symbol resolution can connect the call to that callee without noticing the disagreement. The callee then reads unrelated state rather than the intended value. The ABI must specify the placement, representation, and preservation rules shared by both pieces. Each piece can be internally consistent under its own convention.
Structure layout creates a second concrete mismatch. Assume one module represents a record as two four-byte fields, placing tag at offset zero and count at offset four. Another module expects padding after tag and reads count at offset eight. Passing a pointer to the first representation does not move count to the offset the second expects. A shared symbol and a valid pointer address do not repair the disagreement. These layouts are invented alternatives for comparison, not claims about a particular language's packing rules or a named machine's ABI.
Matching pointers do not imply matching field offsets
Invented layouts with four-byte fields. Every cell is four bytes, drawn at 140 SVG units per cell. These alternatives do not assert any real platform packing rule. Reading offset 8 does not read count from layout A.
Ownership rules can fail even when all bytes and registers agree. If the caller expects to retain responsibility for releasing a buffer but the callee also releases it, a later caller release may use an already released resource. If each expects the other to release it, the resource can remain allocated. The interface must state who owns it, how long references remain usable, and which operation releases it. A linker that knows only a symbol reference cannot deduce these lifetime rules from the function's spelling or the number of its arguments.
Link-time optimization can make function bodies visible across module boundaries and enable transformations unavailable during isolated compilation. That visibility can support inlining or constant propagation, subject to the language and build rules. It does not make an invalid ownership contract valid or permit incompatible foreign code to exchange arbitrary representations. Even with richer information, some components may remain opaque. Ask which information a particular check or transformation actually uses before concluding that a successful optimized link establishes every interface property. More visibility and a complete proof are different claims.
Use the contract table to diagnose three hypothetical symptoms. For an unresolved helper symbol, inspect whether the intended definition entered the link and whether lookup rules expose it. For a call that reaches helper but reads the wrong argument, compare the calling conventions and declarations before blaming name resolution. For a correct returned value followed by a release error, inspect ownership and lifetime agreements. Each investigation preserves the original artifact and build settings. Changing several options at once can remove the symptom without identifying which contract was wrong.
For a final transfer task, imagine that compilation and linking succeed, but deployment fails before the first application action. Separate dependency discovery and image loading from source typing, then state which evidence would distinguish those possibilities. If execution instead starts and returns a wrong record field, inspect the exchanged layout and the actual callee selected. The relocation arithmetic proves where a reference points under its assumed format. It does not prove what the destination computes, whether its arguments match, or whether any measured runtime improvement came from the linker.
Locate the broken agreement
Symptom or agreement
Evidence to inspect
What symbol resolution alone proves
Unresolved helper
Input objects, libraries, symbol visibility
No usable definition was selected
Wrong argument at callee
Declarations and ABI locations P versus Q
Only that a definition was selected
Wrong record field
Field offsets and padding in both modules
No guarantee of matching layout
Release error after correct result
Ownership and permitted lifetime of buffer
No guarantee of matching ownership
Failure before first action
Loader diagnostics and deployed dependencies
Build-time resolution is insufficient
These are diagnostic starting points, not unique causes inferred from symptoms. Preserve the exact artifact and build settings before changing them.
Exercises
Compute a different displacement
In the same hypothetical four-byte, next-instruction-relative format, a call is at address 200 and its target is at 150. What displacement is needed?
Hint
Subtract the address after the call, not the call’s first byte.
Worked answer
Quantity
Value
Call address
200
Base after instruction
204
Target
150
Signed displacement
−54
A relocation formula needs a precisely defined base.
The base is 200 + 4 = 204. Displacement is 150 − 204 = −54. Whether −54 is encodable depends on the real field width and encoding rules, which this arithmetic example does not specify.
Resolved name, incompatible call
Two modules use the same function name but disagree on how arguments are passed. Does successful symbol resolution prove the call is safe?
Hint
Name lookup does not establish every binary convention.
Worked answer
Agreement
What it establishes
Symbol spelling resolves
A definition was found under lookup rules
ABI and layout match
Values can be exchanged as intended
Ownership contract matches
Resources are handled consistently
Each agreement answers a different question.
No. The linker may resolve the name while the call still violates the ABI or interface contract. Compatible declarations, layout, calling conventions, and build settings are required. Available type metadata or link-time checks can catch some mismatches, but successful linking alone is not a proof.
Sources and further reading
The explanations, examples, and worked answers are complete without these optional links. The sources describe specific implementations and language contracts, not one universal compiler pipeline. SmallExpr and the later pseudocode models are constructed teaching examples. Source notes identify the selected material reviewed; this is not an exhaustive compiler survey, production conformance test, or independent specialist review.
Overview of combining object/archive files, relocation, and symbol references. The numerical displacement exercise uses an explicitly hypothetical instruction format.
Selected overview and register-allocation material describing instruction selection, target constraints, liveness, and native-code generation. The storage lab is not an implementation of LLVM’s allocator. The reviewed register-allocation example has inconsistent def/use prose; the lesson follows explicit assignment direction instead.