Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Assembly (x86-64, AArch64, RISC-V)  Module 1 – Start here: what assembly is and how to run it

From source to executable: objects and linking

Trace one program from source to a running executable: assemble, link and load. Inspect sections, symbols and relocations with otool and llvm-objdump.

  • Beginner
  • 10 minutes
  • Examples run with Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 10 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Trace one program through the assembler, the object file, the linker and the loader
  • Inspect object sections, symbols and relocations with otool, nm and llvm-objdump
  • Compare ELF, Mach-O and PE/COFF as the three containers assembly code lives in
  • Explain entry points and how the kernel's loader starts a program

Before you start

What you’ll learn · Before you start

What you will learn

  • Trace one program through the assembler, the object file, the linker and the loader
  • Inspect object sections, symbols and relocations with otool, nm and llvm-objdump
  • Compare ELF, Mach-O and PE/COFF as the three containers assembly code lives in
  • Explain entry points and how the kernel's loader starts a program

Before you start

On this page

A one-instruction assembly file is not a program the kernel can run. Four stages sit between the text you type and the process that executes: the assembler turns source into a relocatable object, the linker merges objects into an executable, the kernel’s loader maps that executable’s pages into a process, and the CPU starts fetching from the entry point. Each stage writes a file or a memory image the next one reads, and each file is a container with its own shape: Mach-O on Apple platforms, ELF on Linux and most Unix systems, PE/COFF on Windows.

This lesson follows one AArch64 hello program end to end on an Apple silicon Mac, then opens the x86-64 and RISC-V objects of the same idea with llvm-objdump and llvm-readelf. Along the way the sections, the symbol table and the relocations stop being folklore and become bytes you can point at. Running the x86-64 and RISC-V programs needs Linux on their own ISA or an emulator, so for those two the lesson reads the objects instead of running them.

assembly language: A human-readable spelling of a processor's instructions, one line per instruction, written to the rules of a single ISA. An assembler turns the text into the matching bytes; a program for one ISA does not run on another. More in “What assembly language is and why it matters” is text. The assembler: A program that reads assembly-language text and writes the matching bytes to an object file, with any labels, constants and directives the source used. clang, nasm and GNU as are three widely used assemblers. More in “What assembly language is and why it matters” reads it and writes an object file: The file an assembler or compiler writes for one source file. It holds the machine-code bytes in named sections, a symbol table and the relocations the linker needs to patch addresses.: a container holding the machine-code bytes of your instructions, the data you declared, a symbol table: The list of labels an object file defines or references, with each label's name, the section it belongs to and the offset within that section. The linker reads it to match references between objects. that names every label, and a list of relocation: An entry beside a section's bytes that says which bytes the linker must patch, with which symbol's address, under which formula. Needed whenever the assembler cannot know a final address yet. that mark the bytes it could not fill in because they need addresses the whole program does not have yet. One source file becomes one object; nothing is runnable yet.

The linker: The tool that merges one or more object files into one executable or shared library. It concatenates sections, picks final addresses, resolves symbols and patches the bytes the assembler marked with relocations. picks up one or more objects, merges their section: A named region of an object or executable file, such as .text, .rodata, .data or .bss. The linker merges matching sections across objects and the loader groups them into segments at run time. (all .text bytes together, all .data bytes together, and so on), decides what virtual address every section will live at, patches each relocation with the real address and writes the result into one executable file. Where the object had holes, the executable has committed values; where the object said “this symbol is defined somewhere else”, the executable has resolved it or recorded that the dynamic linker will.

When you run the executable, the kernel’s loader: The kernel code that reads an executable's load commands or program headers, maps each segment into the new process with the right permissions and jumps to the entry-point address. reads its load commands or program headers, maps each loadable segment into the process’s address space with the right permissions and jumps to the entry point: The address where the kernel's loader starts a program: the value the executable's header names (e_entry in an ELF header, LC_MAIN in a Mach-O file). The Linux convention is the symbol _start; a C program's main runs later, called by the startup code. More in “First program: return an exit status” address stored in the file. From that moment it is the CPU, not any tool, that runs the instructions.

Interactive · stage by stage Source to running process in four stages Follow one hello program from text to a running process, with the file each stage writes next to it.
Source to running process in four stages, step 6 of 6. Showing Source file (hello.s), Object file (hello.o), Executable file (hello), Running process. In focus: Running process.Source file (hello.s)text, written by handObject file (hello.o)bytes with holesExecutable file (hello)bytes, address-readyRunning processpages mapped, PC at entry

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The source. hello.s is text in the assembler's syntax. It names registers, instructions and labels, and declares a string in .cstring. Nothing in it is a byte of machine code yet.
  2. Assembler runs. clang -c calls the integrated assembler, which turns hello.s into hello.o. The object holds the __text bytes, the __cstring data and a symbol table that names _main and msg.
  3. Holes the linker will fill. The object leaves zeros where a reference needs an address, with a relocation beside each. On AArch64 Mach-O the adrp and add instructions carry @PAGE and @PAGEOFF relocations for msg.
  4. Linker runs. clang -o calls ld-27037.1, which merges the object with the Mach-O start stub, lays sections out in __PAGEZERO and __TEXT segments, patches every hole and writes load commands.
  5. The executable on disk. The 16 KiB file holds the Mach-O header, the load commands, the __text bytes and the __cstring data. _main sits at virtual address 0x100000328.
  6. Loader runs. On exec the kernel reads the load commands, maps the __TEXT pages into the process at vmaddr 0x100000000 and jumps to _main. From here the CPU runs the instructions themselves.

The AArch64 hello below walks all four stages on Apple silicon (clang 21.0.0, ld-27037.1 on macOS 26.7.1): the assembler turns hello.s into hello.o, the linker merges it with the Mach-O start stub into hello, and the kernel starts _main with the shell waiting on the exit status.

A 7-exit AArch64 Mach-O program S · aarch64/hello.s
// AArch64 Mach-O program: writes "hello from objects and linking\n" through _write and exits 7.
// Enters at _main because clang's start code jumps to it; w0's value on ret becomes the exit status.
// adrp/add with @PAGE/@PAGEOFF forms the address of the string without any absolute relocation,
// which Apple's linker refuses in code (ld-27037.1 says 'Found illegal text-relocations').
.globl _main
.align 2
_main:
    adrp    x1, msg@PAGE
    add     x1, x1, msg@PAGEOFF
    mov     x0, #1                 // stdout
    mov     x2, #31                // bytes to write (30 chars + the \n)
    mov     x16, #4                // SYS_write on Darwin (unsupported interface, kept here to show the raw trap)
    svc     #0x80
    mov     w0, #7                 // exit status
    ret
.section __TEXT,__cstring
msg:
    .asciz  "hello from objects and linking\n"
hello: assemble, inspect, link, run Python · aarch64_pipeline_session.py
"""Assemble, inspect and run the AArch64 Mach-O hello program.

Shows every stage of the pipeline natively on Apple silicon (clang 21.0.0, ld-27037.1): the assembler turns hello.s into
hello.o, the linker turns hello.o into the executable hello, and the kernel's loader maps its __TEXT segment and
jumps to _main. The otool, nm and size calls read the ELF-less Mach-O container: its header, section table and
symbol table.
"""

import pathlib
import sys

sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session

session.run(
    """
# Assemble hello.s and keep the relocatable object around for inspection.
clang -c aarch64/hello.s -o aarch64/hello.o
# The Mach-O header of the object (filetype 1 = MH_OBJECT, four load commands).
otool -h aarch64/hello.o
# The symbol table of the object: _main is defined, msg is a file-local label.
nm aarch64/hello.o
# Link into an executable (clang runs ld under the hood with the Mach-O start stub).
clang -o aarch64/hello aarch64/hello.s
# The executable now has load commands the loader reads to map its __TEXT segment.
otool -l aarch64/hello | sed -n '1,30p'
# size reads the segments the loader will map; __TEXT holds code and read-only data.
size aarch64/hello
# Finally run it: it writes 31 bytes to stdout and exits with status 7.
./aarch64/hello; echo "exit=$?"
"""
)

Output

# Assemble hello.s and keep the relocatable object around for inspection.
$ clang -c aarch64/hello.s -o aarch64/hello.o
# The Mach-O header of the object (filetype 1 = MH_OBJECT, four load commands).
$ otool -h aarch64/hello.o
aarch64/hello.o:
Mach header
      magic  cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
 0xfeedfacf 16777228          0  0x00           1     4        360 0x00000000
# The symbol table of the object: _main is defined, msg is a file-local label.
$ nm aarch64/hello.o
0000000000000000 T _main
0000000000000000 t ltmp0
0000000000000020 s ltmp1
0000000000000020 s msg
# Link into an executable (clang runs ld under the hood with the Mach-O start stub).
$ clang -o aarch64/hello aarch64/hello.s
# The executable now has load commands the loader reads to map its __TEXT segment.
$ otool -l aarch64/hello | sed -n '1,30p'
aarch64/hello:
Load command 0
      cmd LC_SEGMENT_64
  cmdsize 72
  segname __PAGEZERO
   vmaddr 0x0000000000000000
   vmsize 0x0000000100000000
  fileoff 0
 filesize 0
  maxprot 0x00000000
 initprot 0x00000000
   nsects 0
    flags 0x0
Load command 1
      cmd LC_SEGMENT_64
  cmdsize 232
  segname __TEXT
   vmaddr 0x0000000100000000
   vmsize 0x0000000000004000
  fileoff 0
 filesize 16384
  maxprot 0x00000005
 initprot 0x00000005
   nsects 2
    flags 0x0
Section
  sectname __text
   segname __TEXT
      addr 0x0000000100000328
      size 0x0000000000000020
# size reads the segments the loader will map; __TEXT holds code and read-only data.
$ size aarch64/hello
__TEXT	__DATA	__OBJC	others	dec	hex
16384	0	0	4294983680	4295000064	100008000
# Finally run it: it writes 31 bytes to stdout and exits with status 7.
$ ./aarch64/hello; echo "exit=$?"
hello from objects and linking
exit=7

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 aarch64_pipeline_session.py

The otool -h line reports the Mach-O magic 0xfeedfacf (a 64-bit Mach-O), filetype 1 (an object file, MH_OBJECT in Apple’s mach-o/loader.h) and four load commands. nm lists _main as a global text symbol (the upper-case T) and msg as a file-local symbol (the lower-case s). After the link, otool -l shows two LC_SEGMENT_64 load commands: __PAGEZERO, a zero-permission 4 GiB guard at virtual address 0, and __TEXT starting at 0x100000000, 16 KiB wide, holding the code and the string. The size line confirms the 16,384-byte __TEXT segment. The run then prints the message and the shell reports exit=7, exactly the value _main returned in w0.

Checkpoint

Check: Four stages: assemble, link, load, run

  1. Question 1 of 2 In the pipeline from source to a running process, which tool writes the executable file the loader will map?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The linker

    The assembler writes an object with relocation entries beside the bytes it could not fill in. The linker merges objects, picks final virtual addresses, patches the relocations and writes the executable the loader will map.

  2. Question 2 of 2 In the recorded AArch64 run, hello.o reports filetype 1 (MH_OBJECT) with 4 load commands, while hello reports two LC_SEGMENT_64 commands (__PAGEZERO and __TEXT). Which stage changed the filetype and added __PAGEZERO?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The linker

    Only the linker touches the executable's load commands. The assembler stopped at MH_OBJECT with the raw text, and the loader reads what the linker wrote.

Inside an object: sections, symbols, relocations

An object file carries three groups of information the linker needs. The first is the raw bytes grouped into named sections. .text holds code the CPU will execute. .rodata (or __cstring on Mach-O) holds strings and constants the program only reads. .data holds writable initialised data. .bss holds writable data that starts as zero and takes no space in the file; the loader allocates it at run time. The assembler chose the section for each byte from the directives you wrote: .text, .section __TEXT,__cstring, .data and so on.

The second group is the symbol table. Every label that the linker might need to resolve to an address goes here, with its name, the section it belongs to and the offset within that section. A global symbol can be used by other objects; a file-local symbol stays inside this file so the linker never confuses two private labels of the same name.

The third group is the list of relocations. Each one says: “in this section, at this offset, patch so many bytes with the address of this symbol, using this formula.” The formulas differ by ISA: an x86-64 call to a nearby function needs a 32-bit PC-relative displacement (R_X86_64_PC32); a RISC-V reference to a global uses a pair, R_RISCV_PCREL_HI20 on an auipc and R_RISCV_PCREL_LO12_I on the following addi or load; an AArch64 Mach-O adrp/add sequence carries page and page-offset relocations. The assembler writes zeros into the instruction bytes and leaves the relocation entry beside them; the linker rewrites the zeros when it knows the final addresses.

The x86-64 session below shows this exactly. The source reads a 64-bit global value through a RIP-relative mov and exits with its low byte. Apple clang’s x86-64 cross backend assembles it to an ELF relocatable object, which llvm-objdump reads byte by byte.

x86-64 Linux source: load a global, exit with it S · x86_64/exit_val.s
    .text
    .intel_syntax noprefix
    .globl _start
# A tiny x86-64 Linux program that reads a 64-bit value from .data, exits with its low byte.
# This file is assembled into an ELF object and inspected; running it needs x86-64 Linux,
# so the session stops at inspection.
_start:
    mov rax, [rip + value]
    mov rdi, rax
    mov rax, 60                 # SYS_exit
    syscall
    .section .note.GNU-stack,"",@progbits
    .data
    .globl value
value:
    .quad 42
Reading the x86-64 object with llvm-readelf and llvm-objdump Python · x86_64_inspect_session.py
"""Assemble an x86-64 Linux object natively and read every part the linker needs.

Apple clang targets x86_64-linux-gnu as a cross backend, so it writes an ELF relocatable object (filetype REL) on
macOS without any emulator. llvm-readelf and llvm-objdump read the bytes of the object; nothing executes foreign code.
Running the linked program needs x86-64 Linux (or qemu-user on Linux), so this session stops at the inspection stage.
"""

import pathlib
import sys

sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session

session.run(
    """
# Assemble the x86-64 source into an ELF relocatable object.
clang -c -target x86_64-linux-gnu x86_64/exit_val.s -o x86_64/exit_val.o
# The ELF header: this is a relocatable file for x86-64, no entry point yet.
/opt/homebrew/opt/llvm/bin/llvm-readelf -h x86_64/exit_val.o | sed -n '1,12p'
# The sections of the object. .text holds code, .data holds the global, .rela.text holds relocations.
/opt/homebrew/opt/llvm/bin/llvm-objdump -h x86_64/exit_val.o
# One relocation lives in .rela.text: at offset 3 of _start, patch a 32-bit PC-relative value for `value`.
/opt/homebrew/opt/llvm/bin/llvm-objdump -r x86_64/exit_val.o
# Disassembled _start: the four bytes after `48 8b 05` are 00 00 00 00 until the linker writes the real offset.
/opt/homebrew/opt/llvm/bin/llvm-objdump -d x86_64/exit_val.o
# The symbol table: _start and value are both global, each at offset 0 of their own section.
/opt/homebrew/opt/llvm/bin/llvm-objdump -t x86_64/exit_val.o
"""
)

Output

# Assemble the x86-64 source into an ELF relocatable object.
$ clang -c -target x86_64-linux-gnu x86_64/exit_val.s -o x86_64/exit_val.o
# The ELF header: this is a relocatable file for x86-64, no entry point yet.
$ /opt/homebrew/opt/llvm/bin/llvm-readelf -h x86_64/exit_val.o | sed -n '1,12p'
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
# The sections of the object. .text holds code, .data holds the global, .rela.text holds relocations.
$ /opt/homebrew/opt/llvm/bin/llvm-objdump -h x86_64/exit_val.o

x86_64/exit_val.o:	file format elf64-x86-64

Sections:
Idx Name            Size     VMA              Type
  0                 00000000 0000000000000000
  1 .strtab         0000003f 0000000000000000
  2 .text           00000013 0000000000000000 TEXT
  3 .rela.text      00000018 0000000000000000
  4 .note.GNU-stack 00000000 0000000000000000
  5 .data           00000008 0000000000000000 DATA
  6 .symtab         00000048 0000000000000000
# One relocation lives in .rela.text: at offset 3 of _start, patch a 32-bit PC-relative value for `value`.
$ /opt/homebrew/opt/llvm/bin/llvm-objdump -r x86_64/exit_val.o

x86_64/exit_val.o:	file format elf64-x86-64

RELOCATION RECORDS FOR [.text]:
OFFSET           TYPE                     VALUE
0000000000000003 R_X86_64_PC32            value-0x4
# Disassembled _start: the four bytes after `48 8b 05` are 00 00 00 00 until the linker writes the real offset.
$ /opt/homebrew/opt/llvm/bin/llvm-objdump -d x86_64/exit_val.o

x86_64/exit_val.o:	file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <_start>:
       0: 48 8b 05 00 00 00 00         	movq	(%rip), %rax            # 0x7 <_start+0x7>
       7: 48 89 c7                     	movq	%rax, %rdi
       a: 48 c7 c0 3c 00 00 00         	movq	$0x3c, %rax
      11: 0f 05                        	syscall
# The symbol table: _start and value are both global, each at offset 0 of their own section.
$ /opt/homebrew/opt/llvm/bin/llvm-objdump -t x86_64/exit_val.o

x86_64/exit_val.o:	file format elf64-x86-64

SYMBOL TABLE:
0000000000000000 g       .text	0000000000000000 _start
0000000000000000 g       .data	0000000000000000 value

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 x86_64_inspect_session.py

Two things are worth staring at. First, the single line of .rela.text: offset 3, type R_X86_64_PC32, value value-0x4. Offset 3 is where the 4-byte displacement of the mov rax, [rip + value] instruction begins; the -0x4 accounts for RIP being past the end of the 7-byte instruction when the CPU reads the operand. Second, the disassembly shows 48 8b 05 00 00 00 00 for that line: three opcode bytes followed by four zeros. The linker will overwrite those four zeros with the signed displacement from the end of the instruction to value once it has placed the sections at their final virtual addresses.

RISC-V shows the same idea with a twist: because auipc and addi cannot each hold a full 32-bit immediate, the ISA splits a PC-relative address across two instructions, and the object carries two relocations plus two R_RISCV_RELAX markers that let the linker shrink the pair into one gp-relative load.

RISC-V Linux source: lla then ld S · riscv/exit_val.s
    .text
    .globl _start
# A tiny RISC-V Linux program that loads a 64-bit value from .data and exits with its low byte.
# Assembled into an ELF object and inspected; running it needs RISC-V Linux.
_start:
    lla     a0, value           # local la: auipc + addi, no GOT
    ld      a0, 0(a0)
    li      a7, 93              # SYS_exit on riscv Linux
    ecall
    .data
    .globl value
value:
    .quad 42
Reading the RISC-V object with llvm-objdump Python · riscv_inspect_session.py
"""Assemble a RISC-V Linux object natively and read its relocations.

Apple clang has no RISC-V backend, so this uses Homebrew's clang-23 (LLVM 23.1.2) through /opt/homebrew/opt/llvm/bin.
Nothing executes RISC-V code: llvm-objdump reads the bytes of the object. Running the linked program needs RISC-V
Linux (or qemu-riscv64 on Linux), so this session stops at inspection.
"""

import pathlib
import sys

sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session

session.run(
    """
# Assemble a RISC-V ELF relocatable object with the LLVM 23 cross backend.
/opt/homebrew/opt/llvm/bin/clang-23 -c -target riscv64-linux-gnu riscv/exit_val.s -o riscv/exit_val.o
# The ELF header: a relocatable file for the RISC-V machine type.
/opt/homebrew/opt/llvm/bin/llvm-readelf -h riscv/exit_val.o | sed -n '1,12p'
# RISC-V objects carry a .riscv.attributes section that lists the ISA extensions the file was built for.
/opt/homebrew/opt/llvm/bin/llvm-objdump -h riscv/exit_val.o
# Two PC-relative relocations form the classic auipc + addi pair that reaches `value`.
# R_RISCV_RELAX markers tell the linker it may shrink the pair into one gp-relative load.
/opt/homebrew/opt/llvm/bin/llvm-objdump -r riscv/exit_val.o
# Disassembled _start: the auipc immediate and the addi immediate are both 0 until the linker patches them.
/opt/homebrew/opt/llvm/bin/llvm-objdump -d riscv/exit_val.o
"""
)

Output

# Assemble a RISC-V ELF relocatable object with the LLVM 23 cross backend.
$ /opt/homebrew/opt/llvm/bin/clang-23 -c -target riscv64-linux-gnu riscv/exit_val.s -o riscv/exit_val.o
# The ELF header: a relocatable file for the RISC-V machine type.
$ /opt/homebrew/opt/llvm/bin/llvm-readelf -h riscv/exit_val.o | sed -n '1,12p'
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           RISC-V
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
# RISC-V objects carry a .riscv.attributes section that lists the ISA extensions the file was built for.
$ /opt/homebrew/opt/llvm/bin/llvm-objdump -h riscv/exit_val.o

riscv/exit_val.o:	file format elf64-littleriscv

Sections:
Idx Name              Size     VMA              Type
  0                   00000000 0000000000000000
  1 .strtab           000000a7 0000000000000000
  2 .text             00000012 0000000000000000 TEXT
  3 .rela.text        00000060 0000000000000000
  4 .data             00000008 0000000000000000 DATA
  5 .riscv.attributes 00000066 0000000000000000
  6 .symtab           000000a8 0000000000000000
# Two PC-relative relocations form the classic auipc + addi pair that reaches `value`.
# R_RISCV_RELAX markers tell the linker it may shrink the pair into one gp-relative load.
$ /opt/homebrew/opt/llvm/bin/llvm-objdump -r riscv/exit_val.o

riscv/exit_val.o:	file format elf64-littleriscv

RELOCATION RECORDS FOR [.text]:
OFFSET           TYPE                     VALUE
0000000000000000 R_RISCV_PCREL_HI20       value
0000000000000000 R_RISCV_RELAX            *ABS*
0000000000000004 R_RISCV_PCREL_LO12_I     .Lpcrel_hi0
0000000000000004 R_RISCV_RELAX            *ABS*
# Disassembled _start: the auipc immediate and the addi immediate are both 0 until the linker patches them.
$ /opt/homebrew/opt/llvm/bin/llvm-objdump -d riscv/exit_val.o

riscv/exit_val.o:	file format elf64-littleriscv

Disassembly of section .text:

0000000000000000 <_start>:
       0: 00000517     	auipc	a0, 0x0
       4: 00050513     	mv	a0, a0
       8: 6108         	ld	a0, 0x0(a0)
       a: 05d00893     	li	a7, 0x5d
       e: 00000073     	ecall

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 riscv_inspect_session.py

The disassembled auipc has immediate 0x0 and the following mv holds a0, a0: both placeholders until the linker patches them with the real upper 20 bits and lower 12 bits of the displacement to value. The .riscv.attributes section is a RISC-V-specific record of the ISA extensions the file was built for, so the linker can refuse to mix objects that need extensions a target CPU does not have.

Tip

objdump -r and llvm-objdump -r both print relocations. Add -d to see them beside the disassembly, so each instruction shows next to the hole the linker will fill in it.

Checkpoint

Check: Inside an object: sections, symbols, relocations

  1. Question 1 of 2 In the recorded x86-64 object, llvm-objdump showed 48 8b 05 00 00 00 00 for the first instruction, with a R_X86_64_PC32 relocation at offset 3 pointing to value-0x4. Why are those four bytes zero?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The assembler does not know the final address of value yet, so it writes zeros and leaves a relocation beside them for the linker to fill in.

    The assembler writes the opcode bytes (48 8b 05) and leaves four zero bytes where the 32-bit PC-relative displacement to value will go. The R_X86_64_PC32 relocation at offset 3 is the linker's note that these are the bytes to patch; -0x4 adjusts for RIP being past the end of the instruction.

  2. Question 2 of 2 Which of the following live in a relocatable ELF object file, and are read by the linker rather than by the loader?

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • The section header table
    • The symbol table (.symtab)
    • The relocation entries (.rela.text, .rela.data...)

    The linker reads sections, symbols and relocations. The loader reads program headers, which only EXEC and DYN files have; a REL file carries no PT_LOAD segments.

Three containers: ELF, Mach-O, PE/COFF

The same instructions sit inside different containers depending on the operating system. Each container has a header that identifies it, a table of chunks and conventions the linker and the loader agree on.

ELF is specified by the generic System V ABI and used by Linux, FreeBSD, OpenBSD, Solaris, Haiku and most other Unix systems. An ELF file starts with the four magic bytes 0x7f E L F, says whether it is 32- or 64-bit, little- or big-endian, and names its type: REL for a relocatable object, EXEC for a non-position-independent executable, DYN for a position-independent executable or shared library, CORE for a core dump. It has a section header table that the linker reads and a program header table that the loader reads. The two tables describe the same bytes at different granularities: sections for linking, segments for loading.

Mach-O is Apple’s container, documented in the headers at /usr/include/mach-o/ and in Apple’s archive documentation. A Mach-O file begins with a magic word (0xfeedfacf for the 64-bit form seen above), identifies its CPU type (16777228 is CPU_TYPE_ARM64) and filetype, and then lists a sequence of load commands rather than two separate tables. Each LC_SEGMENT_64 describes a segment and its sections, so one list serves both the linker and the loader. Mach-O does not have .rodata; its equivalent is __TEXT,__cstring for C strings and __DATA,__const for other read-only data.

PE/COFF is the Windows container, specified by Microsoft’s PE format document. A PE file starts with the DOS MZ stub, which contains a pointer to the PE\0\0 signature that begins the real header. After that header come a COFF file header, an optional header (which is anything but optional for executables), a section table and the sections themselves. Function imports and exports are handled through tables rather than through ELF-style dynamic relocations.

One ISA, several containers

The AArch64 instructions in hello.s would assemble to the same bytes on Linux, where they would sit inside an ELF file instead of Mach-O. The container is a wrapper; the ISA decides the bytes.

A quick table of what is called what:

Idea ELF Mach-O PE/COFF
Magic bytes \x7fELF 0xfeedface or 0xfeedfacf MZ...PE\0\0
Code section .text __TEXT,__text .text
Read-only data .rodata __TEXT,__cstring, __DATA,__const .rdata
Writable data .data, .bss __DATA,__data, __DATA,__bss .data, .bss
Linker view section headers load commands section table
Loader view program headers load commands section table
Linker ld (gnu-style) via cross-link ld-27037.1 (Apple) link.exe on Windows

Entry point and the loader

An executable has one entry-point address, written in its header, and the loader jumps there after mapping the process. The address depends on how you linked.

A program linked with the C runtime (what clang -o prog file.c and the AArch64 example above do) picks up a tiny start stub from the libSystem or libc crt files. The stub’s address is the entry point. The stub sets up argc, argv and envp, calls main (or _main, since Mach-O symbols carry a leading underscore), and passes the integer main returned to _exit. That is why the AArch64 program returned 7 in w0 and the shell read exit=7.

A program linked with ld and no C runtime has _start (or whichever symbol is marked with .globl _start) as its entry point, and must make its own exit system call because there is no runtime to return to. first-program-exit-code of this module walks through that style for all three ISAs; exit_val.s above is the x86-64 version.

The loader itself is a kernel feature on each platform. On Linux the kernel’s execve reads the ELF header, maps the PT_LOAD segments with the right page permissions, chooses whether to run the dynamic linker named by PT_INTERP and finally transfers control to the entry point. On macOS execve reads the Mach-O header, follows the LC_SEGMENT_64 and LC_LOAD_DYLINKER load commands and jumps to the entry point named by LC_MAIN (or LC_UNIXTHREAD on older binaries). On Windows the CreateProcess API ends up in the ntdll loader, which maps the PE sections and starts the thread at the address in the optional header’s AddressOfEntryPoint.

Two numbers from the AArch64 run anchor this: otool -l reported __PAGEZERO as a 4 GiB region at virtual address 0x0 with maxprot = 0x00000000, so any dereference of a small address traps at once, and __TEXT as a 16 KiB region at 0x0000000100000000 with initprot = 0x00000005 (read and execute). The entry address of _main, 0x0000000100000328, is just inside __TEXT, which is why mapping that one segment was enough to let the CPU start fetching.

Checkpoint

Check: Entry point and the loader

  1. Question 1 of 2 A program is built with clang -o prog file.s and the shell reports exit=7 after running it. Where did execution begin?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: In the C runtime's start stub, which then called _main and passed its return value to _exit

    When you link with the compiler driver, crt1.o's start stub is the entry point. It calls _main (or main on ELF), takes the return value as the process's exit status and passes it to _exit, which is why w0 of 7 became exit=7 at the shell.

  2. Question 2 of 2 otool -l on the linked executable reported __PAGEZERO at vmaddr 0 with vmsize 0x100000000 and maxprot = 0x00000000. What does this segment do?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It reserves the low 4 GiB of the address space with no permissions, so a NULL or small-integer dereference traps at once.

    __PAGEZERO is a Mach-O convention: a 4 GiB segment at virtual address 0 with no permissions, so dereferencing a NULL pointer or a small integer traps immediately rather than reading or writing live memory.

Practise

Be the detective: three real listings from llvm-readelf and llvm-objdump are below. For each one, decide whether the file is a relocatable object, a non-PIE executable or a position-independent executable; give its entry-point address, or say that it has none; and name one undefined symbol the file still needs. The questions after the listings check each call with its reason.

Listing A: llvm-readelf -h and llvm-objdump -t, trimmed Text · listings/listing_a.txt
Type:                              REL (Relocatable file)
Machine:                           Advanced Micro Devices X86-64
Entry point address:               0x0

SYMBOL TABLE:
0000000000000000 g       .text    0000000000000000 _start
0000000000000000 *UND*            0000000000000000 printf
0000000000000000 g       .data    0000000000000000 value
Listing B: llvm-readelf -h and llvm-readelf -d, trimmed Text · listings/listing_b.txt
Type:                              DYN (Position-Independent Executable file)
Machine:                           AArch64
Entry point address:               0x6a4

Dynamic section at offset 0x1dc8 contains 25 entries:
  Tag        Type           Name/Value
  0x0000...  (NEEDED)       Shared library: [libc.so.6]
Listing C: llvm-readelf -h, trimmed Text · listings/listing_c.txt
Type:                              EXEC (Executable file)
Machine:                           RISC-V
Entry point address:               0x100b0

Write your three answers down before you open the questions. The Type field decides the first part, the entry-point address the second, and only a symbol table can answer the third, so Listing C cannot.

Practice that adapts to you

Practise this lesson

7 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 7 What does the assembler write when you run clang -c hello.s -o hello.o?

    Choose one answer.

    Show the answer: Question 1 of 7

    Answer: A relocatable object file with raw section bytes, a symbol table and relocation entries

    -c tells clang to run through the integrated assembler and stop. The result is a relocatable object: section bytes with holes where addresses need to go, named symbols and relocation entries for the linker.

  2. Question 2 of 7 Which stage decides the virtual address at which the __TEXT segment of the AArch64 hello sits?

    Choose one answer.

    Show the answer: Question 2 of 7

    Answer: The linker, when it writes the executable

    The linker fixes vmaddr for each segment and writes it into the load commands. The loader uses that value directly (an ASLR slide may offset it on some platforms, but the choice still comes from the linker).

  3. Question 3 of 7 An ELF file reports Type: DYN (Position-Independent Executable file) and Entry point address: 0x6a4. What is it?

    Choose one answer.

    Show the answer: Question 3 of 7

    Answer: A position-independent executable, which the dynamic linker loads at a chosen base address

    The ELF type field distinguishes REL (object), EXEC (non-PIE executable), DYN (position-independent executable or shared library) and CORE (core dump). An entry-point address on a DYN file is the shape of a PIE.

  4. Question 4 of 7 Which of the following is NOT part of an ELF file?

    Choose one answer.

    Show the answer: Question 4 of 7

    Answer: Load commands

    Mach-O uses one list of load commands that serve both the linker and the loader. ELF splits this into two tables: section headers for the linker, program headers for the loader.

  5. Question 5 of 7 Which three things sit at the very start of a Windows PE file, in order?

    Choose one answer.

    Show the answer: Question 5 of 7

    Answer: The DOS MZ stub, a pointer to the PE signature, and the PE\0\0 signature itself

    A PE file begins with the DOS MZ stub (a tiny 16-bit program that prints This program cannot be run in DOS mode), which contains the file offset of the PE\0\0 signature. The COFF file header follows the signature.

  6. Question 6 of 7 The symbol table of a relocatable object shows printf as *UND* and _start in .text. What must the linker do to resolve printf?

    Choose one answer.

    Show the answer: Question 6 of 7

    Answer: Find another input (an object or a library) that defines printf, or record a dynamic reference to the C library

    The linker scans the other objects and the libraries you pass; the first definition of printf it finds wins. With a shared libc, it records a dynamic reference (an entry in the GOT/PLT or the IAT) and lets the loader's dynamic linker fill in the real address at run time.

  7. Question 7 of 7 Why does the AArch64 hello return from _main and exit with status 7, while first-program-exit-code's _start version must make its own exit system call?

    Choose one answer.

    Show the answer: Question 7 of 7

    Answer: The hello was linked with the C runtime, which supplies a start stub that calls _main and passes its return value to _exit; _start has no caller to return to.

    Linking with clang attaches the libSystem start stub as the entry point. The stub sets up argc/argv/envp, calls _main, takes w0 as the integer return, and passes it to _exit. A program linked bare with ld starts at _start, which must set up registers and invoke the exit syscall itself (svc #0 on Linux arm64, svc #0x80 on Darwin).

Interview questions

Warm-up: what is the job of the linker, in one sentence, and what does it need from each object file?

The linker merges object files into one executable or shared library: it concatenates matching sections, chooses one final virtual address for every section, resolves every symbol reference and patches the bytes the assembler left as zeros using each object’s relocation entries. From each object it needs the raw section bytes, the symbol table (so it knows which labels this object defines and which it uses) and the relocation entries (so it knows which bytes to patch and how). An object with no symbol table and no relocations is already linked; an object with unresolved symbols and no matching definition anywhere is a link error.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    You run clang -c hello.s -o hello.o and the file is 1,544 bytes. You then run clang -o hello hello.s and the file is 33,544 bytes. Walk through what happens between the two commands and account for the extra bytes.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    An AArch64 Mach-O object has an adrp and add pair that forms the address of msg, with a @PAGE relocation on the adrp and a @PAGEOFF relocation on the add. Why does one address need two instructions and two relocations, and what would go wrong if the linker patched them independently?

    Model answer, Pro. First step and follow-ups

Flashcards · 6 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What are the four stages between an assembly source file and a running process?

    Assemble: the assembler writes an object file with holes in it. Link: the linker lays the sections out and fills the holes. Load: the loader maps the segments into memory and jumps to the entry point. Run: the processor executes the code.

  2. What three things does an object file hand to the linker?

    The raw bytes of each named section, a symbol table listing every label it defines or uses, and relocation entries naming which bytes to patch with which symbol's address.

  3. Why does an assembler leave relocations instead of final addresses?

    While it writes one object it cannot know where the linker will place each section, or where symbols from other objects will end up. A relocation records the bytes to patch, the symbol and the formula, and the linker applies it once the addresses are fixed.

  4. Which container formats wrap the same instruction bytes on Linux, macOS and Windows, and how do they differ?

    Linux and most Unix systems use ELF, with a section header table for the linker and a program header table for the loader. Apple uses Mach-O, whose single list of load commands serves both. Windows uses PE/COFF: the file opens with a small DOS stub that points to the PE signature, where the real header begins.

  5. Where does a program start, and how does linking a C runtime change that?

    At the entry-point address named in the executable's header. With a C runtime the entry is the runtime's start stub, which sets things up and then calls main; a bare _start program has no caller and must make the exit system call itself.

  6. Why can llvm-objdump and llvm-readelf show x86-64 and RISC-V objects faithfully on an Apple silicon Mac?

    They only read the bytes of the file and never execute them, so the host's own ISA does not matter. Running those programs is different: that needs a Linux system of the right ISA, or an emulator.

Key takeaways

  • Four distinct stages sit between source and a running process: the assembler writes an object with holes, the linker patches the holes and lays sections out, the loader maps the segments and jumps to the entry point, and the CPU runs the code.
  • An object file carries three things the linker needs: raw bytes in named sections, a symbol table of every label the linker might resolve, and relocation entries that say which bytes to patch with which symbol’s address.
  • ELF, Mach-O and PE/COFF are three different containers around the same instruction bytes: Linux and most Unix use ELF with a section header table and a program header table, Apple uses Mach-O with one list of load commands, and Windows uses PE/COFF after a DOS stub.
  • The entry point is a single address in the executable’s header; linking with a C runtime makes the start stub the entry point and calls main, while a bare _start program must make its own exit system call.
  • Running inspection commands on a Mac is enough to read x86-64 and RISC-V objects faithfully, because llvm-objdump and llvm-readelf read bytes rather than execute foreign code; running the resulting programs needs Linux on the matching ISA, or an emulator.

Terms in this lesson

ELF
The Executable and Linkable Format of the generic System V ABI, used by Linux and most Unix systems. It has a section header table for the linker and a program header table for the loader.
linker
The tool that merges one or more object files into one executable or shared library. It concatenates sections, picks final addresses, resolves symbols and patches the bytes the assembler marked with relocations.
loader
The kernel code that reads an executable's load commands or program headers, maps each segment into the new process with the right permissions and jumps to the entry-point address.
Mach-O
Apple's container format for object files, executables and shared libraries, documented by Apple. It replaces ELF's two tables with one list of load commands that serve both the linker and the loader.
object file
The file an assembler or compiler writes for one source file. It holds the machine-code bytes in named sections, a symbol table and the relocations the linker needs to patch addresses.
relocation
An entry beside a section's bytes that says which bytes the linker must patch, with which symbol's address, under which formula. Needed whenever the assembler cannot know a final address yet.
section
A named region of an object or executable file, such as .text, .rodata, .data or .bss. The linker merges matching sections across objects and the loader groups them into segments at run time.
symbol table
The list of labels an object file defines or references, with each label's name, the section it belongs to and the offset within that section. The linker reads it to match references between objects.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.