Runtime#
The “runtime” of a C program is the chain that turns source
text into a running process: preprocess, compile,
assemble, link, load. There is no virtual machine;
the binary build talks directly to libc and the
kernel. The C runtime support library (crt0, libc) sets
up argc / argv / envp, calls main, and cleans
up on exit.
c`` files and the running process. For the toolchain
(gcc / clang / ld / ar / nm), see
Tooling.
The build pipeline#
Stage |
What happens |
|---|---|
Preprocess |
|
Compile |
Translation unit becomes assembly. Output: |
Assemble |
Assembly becomes machine code. Output: |
Link |
|
The operator usually drives all four stages with one cc
invocation.
$ cc -o scan main.c scan.c # all stages
$ cc -E -o main.i main.c # preprocess only
$ cc -S -o main.s main.c # to assembly
$ cc -c -o main.o main.c # to object
$ cc -o scan main.o scan.o -lpthread # link
Translation units#
A translation unit is one .c file plus everything it
#include``s. Each translation unit compiles independently to
one ``.o file; the linker joins them.
Symbols defined in a translation unit are visible to the linker
unless marked static (which gives them internal linkage).
extern declarations promise the linker the symbol lives
somewhere else.
/* api.h */
extern int counter; /* declaration */
int counter_value(void); /* declaration */
/* impl.c */
int counter = 0; /* definition */
int counter_value(void) { return counter; }
/* main.c */
#include "api.h"
int main(void) { return counter_value(); }
Linking#
The linker resolves every undefined symbol against the object files and libraries on its command line.
Static linking: every
.ois copied into the binary. No runtime dependencies; large binary; recompile to update.Dynamic linking: the binary records a list of shared objects (
.soon Linux,.dylibon macOS,.dllon Windows) the loader resolves at startup.
$ cc -static -o scan main.c # static
$ cc -o scan main.c -lcurl # dynamic; needs libcurl.so
-l<name> looks for lib<name>.so / lib<name>.a;
-L<dir> adds a search directory; -I<dir> adds an
include search path.
Linker order matters with static libraries: each .a must
appear after the .o that uses it.
Loaders and ld.so#
When the kernel runs an ELF binary on Linux, the dynamic loader
(ld-linux.so) maps the program plus its required shared
objects into memory, runs crt0 (which sets up argv /
environ), then jumps to main.
$ ldd ./scan # see required .so's
linux-vdso.so.1 (0x...)
libcurl.so.4 => /lib/x86_64-linux-gnu/libcurl.so.4 (0x...)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x...)
LD_LIBRARY_PATH adds search directories for the loader;
LD_PRELOAD forces a shared object to load first (useful
for shimming).
Build against the kernel’s libc by default
(glibc on most distros). For maximum portability across
hosts and for static binaries, target musl (e.g.
musl-gcc or zig cc -target x86_64-linux-musl).
The C ABI#
The Application Binary Interface specifies how functions pass arguments, where they put return values, how they preserve registers, and how structs are laid out. On x86-64 Linux it is the System V AMD64 ABI; on Windows it is the Microsoft x64 calling convention; on ARM64 it is AAPCS64.
The C ABI is the lingua franca every language interoperates
through: Rust extern "C", Python ctypes, Go cgo,
JavaScript WASM imports, all aim at the same calling
convention.
__attribute__#
GCC / Clang attributes change how a function or variable is compiled or linked. Go-tos.
Attribute |
Effect |
|---|---|
|
run before |
|
run after |
|
function never returns ( |
|
align the type / variable to |
|
no padding between struct fields (use carefully). |
|
linker can override with a non-weak symbol. |
C23 standardises a subset as [[noreturn]], [[deprecated]],
[[nodiscard]].
Memory layout#
A typical Unix process layout, low to high address.
Region |
Notes |
|---|---|
|
executable code. Read-only. |
|
constants and string literals. Read-only. |
|
initialised globals / statics. Read-write. |
|
zero-initialised globals / statics. Allocated at startup. |
heap |
|
stack |
per-thread; locals, return addresses. Grows down on x86. |
kernel |
mapped at the top; user code cannot read. |
readelf -l ./binary or /proc/<pid>/maps shows the
actual map.
Make and CMake#
make is the operator’s incremental build driver. A minimal
Makefile.
CC := cc
CFLAGS := -Wall -Wextra -O2 -g
LDLIBS := -lpthread
OBJS := main.o scan.o
TARGET := scan
$(TARGET): $(OBJS)
$(CC) $(OBJS) -o $@ $(LDLIBS)
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f $(TARGET) $(OBJS)
cmake generates a Makefile (or ninja / Visual Studio
project) from a portable description; use it
when the project must build on more than one platform.