From Program to Process: Compiling, Linking, ELF and Loading
Linux uses ELF (Windows: PE/COFF, macOS: Mach-O).
Concept
An object file contains machine code with unresolved symbols and relocations (placeholders the linker must patch).
The linker combines object files, resolves each symbol to one definition and fixes up addresses.
Static linking copies library code into the executable; dynamic linking records the needed shared libraries and lets the dynamic linker (ld-linux-x86-64.so.2) map them at load time. Shared libraries save disk and memory (one copy in RAM for all processes) and can be updated independently.
ELF: Executable and Linkable Format Linux uses ELF (Windows: PE/COFF, macOS: Mach-O). An ELF file has:
an ELF header (class, ISA, entry point), sections for the linker (.text code, .rodata constants, .data initialized globals, .bss zero-initialized globals, .symtab, .rel*),
program headers (segments) for the loader: which parts to map where, with which permissions (LOAD R E, LOAD RW).
Loading: execve execve makes the kernel: read the ELF headers, map the LOAD segments into a fresh address space, map the dynamic linker (INTERP), set up the stack (arguments, environment, auxiliary vector) and jump to the entry point. The dynamic linker then maps shared libraries, performs relocations and calls main via the C runtime startup code.
The Process Address Space With ASLR (address space layout randomization) the bases of these regions change on every run (Module 28).
flowchart LR
SRC["main.c, sensor.c"] --> CPP["Preprocessor"]
CPP --> CC["Compiler"]
CC --> AS["Assembler"]
AS --> OBJ["Object files .o (relocatable)"]
OBJ --> LD["Linker"]
LIB["Libraries: .a static, .so shared"] --> LD
LD --> EXE["Executable (ELF)"]
EXE --> LOAD["Loader: execve + dynamic linker"]
LOAD --> PROC["Process in memory"]high addresses stack (grows down) local variables, return addresses
...
shared libraries, vDSO
...
heap (grows up) malloc
.bss / .data globals
.rodata constants
low addresses .text codeWhy It Matters
"Undefined reference" linker errors, DLL/so not found errors, symbol clashes, startup performance, binary size, security hardening (PIE, RELRO, NX) and firmware images for microcontrollers are all linking and loading topics.
Syntax
Compile to object file Symbols: T defined in text, U undefined, D data, B bss readelf -h/-S/-l/-r
ELF header, sections, segments, relocations Shared libraries a program needs gcc -shared -fPIC Build a shared library cat /proc/<pid>/maps A running process's memory map
Example
9.1 Two source files, static vs dynamic vs shared-library linking
9.2 Where things live in a running process
#include <stdio.h>
#include "sensor.h"
int main(void)
{
printf("average = %.2f\n", average((double[]){12.4, 12.6, 12.5}, 3));
return 0;
}#include "sensor.h"
double average(const double *v, int n)
{
double s = 0;
for (int i = 0; i < n; i++) s += v[i];
return s / n;
}$ gcc -c main.c sensor.c
$ nm main.o
U __stack_chk_fail
U average
0000000000000000 T main
U printf
$ nm sensor.o
0000000000000000 T average
$ readelf -r main.o | grep -E 'average|printf'
00000000004f 000500000004 R_X86_64_PLT32 0000000000000000 average - 4
00000000006d 000600000004 R_X86_64_PLT32 0000000000000000 printf - 4
$ gcc main.o sensor.o -o app_dynamic && ./app_dynamic
average = 12.50
$ ldd app_dynamic | sed 's/ (0x[0-9a-f]*)//'
linux-vdso.so.1
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6
/lib64/ld-linux-x86-64.so.2
$ gcc -static main.o sensor.o -o app_static && ./app_static
average = 12.50
$ ls -l app_dynamic app_static | awk '{print $5, $9}'
16072 app_dynamic
900328 app_static
$ gcc -fPIC -shared sensor.c -o libsensor.so && gcc main.c -L. -lsensor -o app_shared && LD_LIBRARY_PATH=. ./app_shared
average = 12.50
$ readelf -h app_dynamic | grep -E 'Class|Type|Machine|Entry'
Class: ELF64
Type: DYN (Position-Independent Executable file)
Machine: Advanced Micro Devices X86-64
Entry point address: 0x1080
$ readelf
…#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int initialized = 42; /* .data */
int zeroed; /* .bss */
const char banner[] = "ro"; /* .rodata */
int main(void)
{
int local = 1; /* stack */
int *heap = malloc(sizeof *heap); /* heap */
printf("text (main) %p\n", (void *)main);
printf("rodata (banner) %p\n", (void *)banner);
printf("data (initialized)%p\n", (void *)&initialized);
printf("bss (zeroed) %p\n", (void *)&zeroed);
printf("heap (malloc) %p\n", (void *)heap);
printf("libc (printf) %p\n", (void *)printf);
printf("stack (local) %p\n\n", (void *)&local);
FILE *f = fopen("/proc/self/maps", "r"); /* the kernel's view of this process */
char line[512];
while (fgets(line, sizeof line, f)) {
char range[64], perms[8], path[256] = "";
sscanf(line, "%63s %7s %*s %*s %*s %255s", range, perms, path);
if (strstr(path, "m09_layout") || strstr(path, "[heap]") || strstr(path, "[stack]") ||
(strstr(path, "libc.so") && strcmp(perms, "r-xp") == 0) || strstr(path, "[vdso]"))
…Expected Result
main.o defines main (T) but leaves average and printf undefined (U); the relocations R_X86_64_PLT32 mark where the linker must patch the calls.
The dynamically linked program is about 16 KB; the static one about 900 KB because it contains the needed parts of the C library.
The PIE executable's entry point is 0x1080 (relative, relocated at load time); the segments show the classic permission split: code R E, constants R, data RW, and a non-executable stack (GNU_STACK RW).
The printed addresses and /proc/self/maps agree: code in the r-xp mapping of the executable, globals in its rw-p mapping, the heap just above it, the C library far higher, and the stack near the top of the user address space (0x7ffe…). The exact addresses change on every run (ASLR).
text (main) 0x5c10d13aa180
rodata (banner) 0x5c10d13ab115
data (initialized)0x5c10d13ad010
bss (zeroed) 0x5c10d13ad018
heap (malloc) 0x5c11067f92a0
libc (printf) 0x72b61fc60770
stack (local) 0x7ffe057c9674
5c10d13a9000-5c10d13aa000 r--p /tmp/osb/m09_layout
5c10d13aa000-5c10d13ab000 r-xp /tmp/osb/m09_layout
5c10d13ab000-5c10d13ac000 r--p /tmp/osb/m09_layout
5c10d13ac000-5c10d13ad000 r--p /tmp/osb/m09_layout
5c10d13ad000-5c10d13ae000 rw-p /tmp/osb/m09_layout
5c11067f9000-5c110681a000 rw-p [heap]
72b61fc28000-72b61fdbd000 r-xp /usr/lib/x86_64-linux-gnu/libc.so.6
72b61ff56000-72b61ff58000 r-xp [vdso]
7ffe057aa000-7ffe057cb000 rw-p [stack]Common Mistakes
Declaring a function in a header but not linking the object file that defines it ("undefined reference").
Defining the same global in two files (multiple definition) or relying on extern without a definition.
Forgetting -fPIC when building a shared library.
Shipping a program linked against a newer glibc than the target system has.
Confusing sections (link-time view) with segments (load-time view).
Best Practices
Keep one definition per symbol; mark internal functions static.
Prefer dynamic linking for system libraries, static linking for self-contained tools and embedded firmware.
Build with hardening flags (-fPIE -pie -Wl,-z,relro,-z,now), which most distributions enable by default.
Use nm, readelf and ldd before guessing at link or load errors.
Interview Relevance
High: "what does the linker do?", "static vs dynamic linking?", "what is in .bss vs .data?", "what happens when you run ./a.out?", "what is PIC/PIE?".
Practice Questions
Add a second definition of average in main.c and read the linker error.
Build libsensor.a with ar rcs and link against it.
Use readelf -S to find the size of .text in app_static.
Run m09_layout three times and compare the addresses.
Quick Quiz
Q1. (Multiple choice) In nm output, U average means: A) Unused B) Undefined here, must be provided by another object or library C) User function D) Uninitialized data Answer: B (verified in main.o).
Q2. (Predict the output) Which is bigger, the static or the dynamic executable of the same program?
Answer: The static one — about 900 KB vs 16 KB (verified).
Q3. (True/False) A global variable declared int zeroed; occupies space in the executable file for its value.
Answer: False. .bss only records the size; the loader provides zero-filled memory.
Q4. (Find the bug) ./app_shared: error while loading shared libraries: libsensor.so: cannot open shared object file.
Answer: The dynamic linker doesn't search the current directory; install the library in a standard path, set LD_LIBRARY_PATH, or link with an rpath.
Coding Challenge
Write a program that takes the addresses of a string literal, a static local variable, a malloc block and an ordinary local variable, and checks that they are ordered as the process layout predicts: rodata < data < heap < stack.
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
int main(void)
{
const char *literal = "rodata";
static int static_local = 7;
int *heap = malloc(16);
int local = 0;
uintptr_t a = (uintptr_t)literal, b = (uintptr_t)&static_local, c = (uintptr_t)heap, d = (uintptr_t)&local;
printf("rodata < data: %s\n", a < b ? "yes" : "no");
printf("data < heap: %s\n", b < c ? "yes" : "no");
printf("heap < stack: %s\n", c < d ? "yes" : "no");
free(heap);
return 0;
}