Reading x86-64 Compiler Output and Object Files
Compiler output is evidence of a particular source file, compiler version, target triple, options, and optimization level. Read it as a chain of representations: source becomes assembly, assembly becomes a relocatable object, and linking turns objects and libraries into an executable or shared library. This lesson concerns x86-64—the ISA also called AMD64 by AMD and Intel 64 by Intel. Microsoft’s “x64” labels generally refer to this same 64-bit x86 family and its Windows ABI, not to a distinct “x64 architecture.”
Ask a precise question first
“What did the compiler do?” is too broad. Better questions include: Which argument register does this target ABI assign? Is this access a load or an address calculation? Does this object still need a relocation? Where did the linker record an external dependency? Record the command line, target, and source alongside the output. Inlining, constant propagation, link-time optimization, sanitizers, and security options can eliminate or transform the source construct you expected to see.
Use a harmless owned sample such as:
extern int library_bias(void);
static int local_bias = 3;
int total(int value) {
return value + local_bias + library_bias();
}
Generate textual assembly with gcc -O2 -S -masm=intel sample.c -o sample.s or clang -O2 -S -masm=intel sample.c -o sample.s. The -S stage stops before assembly and linking, so it is excellent for correlating source and instructions. It is not a final binary: assembler directives, symbol visibility, and later linker relaxation still matter. Add -g for debug information when investigating source correspondence.
Read instructions in context
Start at a named function symbol, identify the ABI, and follow data flow to its return. Under SysV AMD64, the first integer argument is normally EDI/RDI; under Microsoft x64 it is normally ECX/RCX. A 32-bit register operation often reflects a 32-bit C type and zeroes the upper half of the associated 64-bit register. movsxd indicates signed widening. A cmp sets flags without retaining a subtraction result, and conditional branches use signed or unsigned flag interpretations. These facts are stronger evidence than instruction mnemonics viewed alone.
; illustrative SysV output for int plus_three(int x)
lea eax, [rdi + 3] ; computes low 32-bit result; no memory is dereferenced
ret ; EAX is the integer return register
Intel syntax places destination first; GNU tools may instead display AT&T syntax, with source first and register prefixes. Select a syntax deliberately rather than comparing textual order across modes. An instruction annotated as [rip + 0x0] in an object file is frequently RIP-relative data or a call placeholder whose final displacement is not known yet. Pair it with the relocation output before assigning a meaning.
What a relocatable object contains
An object file is not merely a bag of instructions. ELF objects commonly contain sections such as .text for code, .rodata for read-only data, .data for initialized writable data, .bss for zero-initialized storage, symbol tables, string tables, relocation sections, and optional DWARF debug sections. A section has flags, alignment, and an address only in the context of its eventual image. A symbol names a location, a section-relative entity, or an unresolved external reference; it can be local, global, weak, hidden, and more.
PE/COFF serves the analogous Windows ecosystem. COFF object files have sections, a symbol table, and relocation entries; a PE image adds headers, section mapping information, import and export structures, base relocations where applicable, and exception/unwind data. Do not assume section names or relocation names match ELF. The broad concept is shared: an instruction or data field needs a value that cannot be finalized until a later stage, so metadata tells the linker how to produce it.
| Question | ELF-oriented tool | PE/COFF-oriented tool |
|---|---|---|
| List sections | readelf -S file.o | dumpbin /headers file.obj |
| List symbols | readelf -s file.o or nm | dumpbin /symbols file.obj |
| Disassemble with relocations | objdump -dr -Mintel file.o | dumpbin /disasm file.obj, plus headers/relocations as available |
| LLVM alternative | llvm-objdump, llvm-readobj, and llvm-nm understand multiple formats. | |
Relocations connect names to bytes
A relocation record identifies a location to adjust, a relocation kind, a referenced symbol, and often an addend. On x86-64 ELF, common concepts include PC-relative references to nearby code or data, absolute references, GOT-relative accesses, and PLT-related external calls. Names such as R_X86_64_PC32 and R_X86_64_PLT32 describe ELF relocation semantics, not universal processor instructions. The linker resolves, redirects, or rejects them according to the link mode and symbol definitions.
For the sample, a call to library_bias in sample.o normally retains an unresolved symbol and a relocation. This does not mean the call target is zero; disassembly prints bytes before the linker applies their final meaning. A locally defined static variable may be referenced with a known section-relative relocation in the object, or optimized directly to a constant. At final link time, a symbol’s visibility and whether a shared object is involved determine whether a direct reference, GOT lookup, PLT stub, or another sequence is appropriate.
Position-independent code on x86-64 often uses RIP-relative instructions. The displacement is measured from the following instruction, allowing code to move without an absolute address embedded in every internal reference. PIC does not mean “no relocations”; dynamic linking, interposable symbols, and data references can still require tables and runtime fixups. Similarly, a position-independent executable (PIE) is an executable linked to be loadable at varying base addresses, while a shared library has additional export and dynamic-linking concerns.
Inspect deliberately, not by pattern matching
A useful command sequence for an object you compiled is:
cc -O2 -fPIC -g -c sample.c -o sample.o
readelf -h -S -s -r sample.o
objdump -dr -Mintel --source sample.o
nm -a sample.o
readelf -h identifies the format, class, machine, and endianness. Confirm it says ELF64 and x86-64 before applying this lesson. -S shows section flags and alignment; -s shows symbol binding and section association; -r shows relocations. objdump -d alone omits essential relocation context, so use -r. Source interleaving requires suitable debug information and source paths; absence of source does not indicate an error.
For a Windows-targeted object, use Microsoft’s documented dumpbin commands from a developer command prompt or LLVM’s format-neutral readers. Confirm the COFF machine field and inspect the symbol and relocation records. Do not rename an ELF file to .obj or assume its contents become PE/COFF; format and ABI arise from the target selected during compilation.
Linking and the final image
A static link copies or selects needed object code from archives and resolves symbols into one image. A dynamic link records dependencies and uses loader conventions. ELF dynamic executables often expose sections and segments related to the dynamic linker, GOT, PLT, and relocation tables. PE images use imports, an import address table, exports if provided, and base relocation information. Section views are useful to tool authors; the loader maps segments in ELF and mapped image sections in PE according to headers. Do not equate a section name with a runtime permission without checking the final image.
Linkers can discard unused sections, merge constants, fold identical functions, relax branches, synthesize startup code, and resolve local references. Consequently, the linked executable is allowed to differ materially from .s and .o. Compare each stage only after noting its purpose. Security mechanisms such as ASLR support, relocation read-only protection, stack protection, and control-flow features are part of the build design, not clutter to remove for a cleaner listing.
Symbol names also require restraint. C names are often emitted directly in ELF, while platform conventions, C++ name mangling, visibility, stripping, and linker aliases can change what a tool displays. A symbol table is not a complete source map, and an address printed by a disassembler is not automatically a runtime address in a PIE or shared object. Use debug information and the loader-aware tools appropriate to a binary you own when source-level attribution matters.
Finally, distinguish an object’s architecture from its host. A cross compiler can emit x86-64 ELF, x86-64 COFF, or another target on the same development machine. The object header, target triple, ABI, and linker selection establish the contract. Host assumptions are a common reason that otherwise careful instruction annotations assign the wrong argument registers or relocation interpretation.
Safe exercises
- Compile the sample at
-O0and-O2. List two transformations without calling either output “the real assembly.” - Find the relocation associated with
library_bias. State its symbol, relocation kind, and the instruction field it applies to. - Make
local_biasconst, then compare symbols and relocations. Explain any constant folding. - Build an object for a target you have installed and verify its format header before selecting ELF or PE/COFF documentation.
Use Registers, Addressing, and Position-Independent Code for the RIP-relative instruction mechanics, and ABI Practice to interpret call-boundary registers. The next lesson, x86-64 SIMD and Performance Fundamentals, applies the same inspect-and-measure discipline to vector code.
dispelled