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

#include files copied in; #define macros expanded; #if directives evaluated. Output: a translation unit (cpp foo.c > foo.i).

Compile

Translation unit becomes assembly. Output: foo.s.

Assemble

Assembly becomes machine code. Output: foo.o (an ELF object file on Linux).

Link

.o files plus libraries combine into an executable or a library. Output: a.out / libfoo.so.

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 .o is copied into the binary. No runtime dependencies; large binary; recompile to update.

  • Dynamic linking: the binary records a list of shared objects (.so on Linux, .dylib on macOS, .dll on 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

__attribute__((constructor))

run before main.

__attribute__((destructor))

run after main returns.

__attribute__((noreturn))

function never returns (abort, exit).

__attribute__((aligned(N)))

align the type / variable to N bytes.

__attribute__((packed))

no padding between struct fields (use carefully).

__attribute__((weak))

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

.text

executable code. Read-only.

.rodata

constants and string literals. Read-only.

.data

initialised globals / statics. Read-write.

.bss

zero-initialised globals / statics. Allocated at startup.

heap

malloc / free allocations. Grows up.

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.

References#