Runtime#

The “runtime” for an assembly program is the assemble, link, load chain plus the ABI that fixes the boundaries: how argc / argv reach _start, how libc’s crt0 calls main, how shared libraries are loaded by ld.so, how position-independent code addresses data and functions through the GOT and PLT.

For the toolchain commands themselves, see Tooling.

Pipeline#

Stage

What happens

assemble

.asm / .s becomes .o (an ELF object). NASM (nasm -felf64) for Intel syntax; as (or gcc wrapping it) for GAS.

link

one or more .o plus libraries become an executable or shared library. ld directly or gcc / clang as the linker driver.

load

kernel’s execve maps the ELF segments into memory, hands control to _start (or the dynamic loader for dynamically-linked binaries).

A minimal sequence.

$ nasm -felf64 hello.asm -o hello.o
$ ld -o hello hello.o                    # freestanding
$ ./hello

$ gcc -nostartfiles -o hello hello.o     # with libc, no crt0
$ gcc -o hello hello.o                    # with libc + crt0 (calls main)

ELF#

An ELF binary on Linux has two views: sections (used by the linker; .text, .data, .rodata, .bss, .symtab, .rela.text, .eh_frame) and segments (used by the loader; LOAD, DYNAMIC, INTERP, PHDR, GNU_RELRO).

readelf -l ./binary shows the segments; readelf -S shows the sections; objdump -d disassembles .text.

$ readelf -h ./hello                     # header
$ readelf -S ./hello                     # sections
$ readelf -l ./hello                     # segments / program headers
$ objdump -d ./hello                     # disassembly

Entry points#

A freestanding ELF binary’s entry point is the symbol _start (by convention; the linker accepts any name via -e). The kernel sets up the stack with argc, argv, envp, auxv (see CLI) and jumps.

A libc-linked binary’s entry point is still _start (provided by crt0), which sets up the C runtime (heap, TLS, stdio buffers, __libc_start_main) and then calls main.

kernel execve
    -> _start (crt1.o)
        -> __libc_start_main
            -> __libc_init_first
            -> _init (constructors)
            -> main(argc, argv, envp)
            -> _fini (destructors)
            -> exit(status)

Static vs dynamic linking#

  • Static: every required .o is copied into the binary. No runtime dependencies; portable across hosts that share an ABI. -static flag.

  • Dynamic: the binary records required .so names; the dynamic loader (ld.so) maps them in at startup. Smaller binaries; binary depends on the host’s libc version.

$ gcc -static -o hello hello.c
$ gcc -o hello hello.c                   # dynamic

$ ldd ./hello                            # see required .so's
$ readelf -d ./hello                     # see DYNAMIC entries

PIE and PIC#

  • PIE (Position-Independent Executable): the binary can load at any base address; ASLR randomises the base each run. Modern Linux distros build PIE by default.

  • PIC (Position-Independent Code): the code does not assume a fixed load address; mandatory for .so shared libraries.

PIC and PIE require RIP-relative addressing on x86-64 (mov rax, [rel msg] in NASM, leaq msg(%rip), %rax in GAS).

; PIC-safe: RIP-relative
lea     rsi, [rel msg]
mov     rax, qword [rel counter]

; Not PIE-safe: absolute address
mov     rsi, msg                         ; relocates; fails under PIE

GOT and PLT#

When the dynamic loader resolves a function from a shared library, the call goes through the PLT (Procedure Linkage Table) which jumps through the GOT (Global Offset Table). First call: stub resolves via ld.so and patches GOT. Subsequent calls: direct jump through GOT.

call    printf@plt              ; in the operator's code
printf@plt:
    jmp     qword [rel printf@got]   ; through GOT
    push    0                         ; reloc index
    jmp     PLT0                       ; resolver

For data, use the GOT directly through @GOTPCREL references (in GAS) or NASM’s wrt ..got / wrt ..gotpcrel. The compiler emits these for the operator in compiled code; pure assembly rarely needs them.

Relocations#

An object file’s instructions that reference external symbols carry relocations, requests for the linker to fill in the right address. readelf -r ./file.o shows them.

Reloc type (x86-64)

Meaning

R_X86_64_PC32

32-bit PC-relative; for near calls and jumps

R_X86_64_PLT32

32-bit PC-relative to PLT entry

R_X86_64_GOTPCREL

32-bit PC-relative to GOT entry

R_X86_64_64

64-bit absolute; not PIE-safe

R_X86_64_32 / R_X86_64_32S

32-bit absolute (zero / sign extended)

If the linker rejects an absolute reloc in a PIE build, the operator switches to a RIP-relative form.

TLS (thread-local storage)#

On x86-64 Linux, TLS is accessed through the fs segment register; per-thread variables live at negative offsets from [fs:0]. The compiler emits this for __thread variables; hand-written assembly uses the same pattern.

mov     rax, qword [fs:0x28]    ; stack canary (classic example)

ARM64 TLS uses mrs x0, tpidr_el0.

ABI summary#

Memorise three:

  • System V AMD64 (Linux / macOS / BSD on x86-64). See Functions.

  • Microsoft x64 (Windows on x86-64).

  • AAPCS64 (ARM64 everywhere).

These determine register usage in calls, return values, stack alignment, and which registers are caller-vs callee-saved.

Sections and segments#

The operator’s main sections.

Section

Use

.text

executable code. R-X.

.rodata

read-only data (string literals, lookup tables). R.

.data

initialised read-write data. RW.

.bss

zero-initialised data; takes no file space. RW.

.eh_frame / .eh_frame_hdr

DWARF unwind information.

.symtab / .dynsym

symbol tables.

.rela.text / .rela.dyn

relocations.

The loader groups sections into segments by permission; .text and .rodata go into the R-X LOAD segment, .data and .bss into the RW LOAD segment.

References#