From Source Code to a Running Process
The journey a program takes from a text file you wrote to a living process the CPU is executing
You write a file called hello.c, type a couple of commands, and then run ./a.out — and something you called a "program" suddenly becomes a "process" that is alive, using memory, printing to your terminal. Most courses treat that moment as magic: source goes in, output comes out. This chapter is about everything that happens in between, because that in-between is exactly where the operating system earns its keep.
There are three distinct worlds to keep straight, and the whole chapter hangs off them. Build time is when your source is turned into a finished executable file on disk — this is the job of the compiler toolchain, and the OS is barely involved. Load time is the instant you run the file: the OS reads that executable and lays it out in memory. Run time is everything after: your code executes as a process, asking the kernel for services as it goes.
We are going to walk that path in order, and we will get our hands dirty with real tools — gcc broken into its four stages, readelf and file to X-ray an executable, ldd to see its library dependencies, size to measure its segments. By the end, the phrase "a running process" will not be a black box; it will be a picture in your head of segments mapped into an address space, with a stack, a heap, and a program counter sitting at an entry point the loader chose.
The gap between a .c file and a live process
Start from what you can actually see on a terminal. You have a text file, hello.c, that is just characters — the CPU cannot execute characters. After a build step you have a file called a.out, which is binary and executable but still just sitting on disk, doing nothing. Then you type ./a.out, and now there is a process: an entry in the OS process table, memory allocated to it, a slice of CPU time scheduled for it. Three very different things, easy to blur together, and this chapter keeps them apart.
The single most useful mental model is three time zones. At build time the compiler toolchain transforms source into a machine-code executable — the OS is not really involved, this is just programs processing files. At load time you ask the OS to run that file, and the OS loader reads it and builds a process image in memory. At run time your instructions actually execute on the CPU, and whenever they need something only the kernel can do — memory, files, the screen — they make a system call.
- Program
- A passive executable file sitting on disk — bytes and instructions, but not running.
- Process
- A program in execution: a live instance with its own memory, registers, and OS bookkeeping.
- Build time
- When source code is compiled and linked into an executable file. The OS is barely involved.
- Load time
- When you launch the executable and the OS maps it into memory to create a process.
- Run time
- When the process actually executes, requesting kernel services via system calls as it goes.
Keep this straight: A program is a recipe on a card; a process is the meal being cooked. One recipe can be cooking in three different kitchens at once — three processes from one file — which is exactly what happens when you open three terminals and run the same binary.
The four build stages
When you run gcc hello.c, it looks like one step, but it is really four programs run back to back, each feeding the next. Understanding the four stages is the difference between "the compiler is angry at me" and knowing exactly which stage failed and why. The stages are: preprocess, compile, assemble, link — and each one produces a file you can actually stop at and inspect.
- Preprocess: the C preprocessor handles lines starting with # — it pastes in #include headers and expands #define macros, producing pure C with no directives left. Output: a .i file.
- Compile: the compiler proper turns that C into assembly language for your specific CPU architecture. Output: a .s file, still human-readable text.
- Assemble: the assembler turns assembly text into machine-code object bytes with a symbol table. Output: a .o object file — binary, but not yet runnable.
- Link: the linker stitches your object files and libraries together, resolves references between them, and produces the final executable. Output: a.out.
gcc -E hello.c -o hello.i # 1. preprocess only → expanded C
gcc -S hello.i -o hello.s # 2. compile → assembly text
gcc -c hello.s -o hello.o # 3. assemble → object code
gcc hello.o -o a.out # 4. link → executable
./a.out # and run itThe flags are worth memorising because they map one-to-one onto the stages: -E stops after preprocessing, -S stops after compiling to assembly, -c stops after assembling to an object file, and no flag at all runs the whole chain including the link. Plain gcc hello.c does all four in one go and quietly deletes the intermediate files — the stages are still there, just hidden.
SWE angle: This is why "undefined reference to ..." is a linker error, not a compiler error: each .c file compiles fine on its own, but the linker is the first stage that has to see all the files at once and match up who calls what. Knowing the stage tells you where to look.
Linking: static vs dynamic
The linker has one core job: resolve symbols. Your code calls printf, but your object file does not contain printf — it just has a note saying "there is a thing called printf out there, fill in its address later". The linker finds where printf actually lives (in the C library) and patches every such reference. The big question is when and how that library code gets attached, and there are two answers.
Static linking copies the needed library code directly into your executable at build time. The result is one large, self-contained file that carries everything it needs. Dynamic linking instead leaves only a reference in your executable; the actual library — a shared object, .so on Linux, .dll on Windows — stays separate and is loaded and connected at load/run time by the dynamic linker. Almost every program you run day to day is dynamically linked.
gcc -static hello.c -o hello_static # bake libraries in
gcc hello.c -o hello_dynamic # default: dynamic
ldd hello_dynamic
# linux-vdso.so.1 (0x00007fff...)
# libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f...)
# /lib64/ld-linux-x86-64.so.2 (0x00007f...)
ldd hello_static
# not a dynamic executable- Symbol
- A named entity — a function or global variable — that the linker must match between where it is defined and where it is used.
- Static library (.a)
- An archive of object files; the linker copies the pieces you use into your executable at build time.
- Shared library (.so / .dll)
- A library that lives as its own file and is loaded once at run time, shared among many programs.
- ldd
- A tool that lists the shared libraries an executable depends on — your window into dynamic linking.
The trade in one line: Static: bigger file, no run-time surprises, nothing to install. Dynamic: tiny file, shared in memory across processes, but it must find the right .so at run time. We come back to this trade-off in depth once we can see the memory picture.
The executable format (ELF)
That a.out is not a random blob — it follows a strict format so the OS knows how to read it. On Linux and most Unix systems that format is ELF, the Executable and Linkable Format. An ELF file begins with a header that says "I am ELF, here is my architecture, here is the entry point address where execution should begin", followed by tables describing the rest of the file. Windows uses its own equivalent, PE (Portable Executable), but the ideas map across almost exactly.
- .text — the machine-code instructions of your program; read-only and executable.
- .rodata — read-only data such as string literals and const globals.
- .data — global and static variables that have an explicit initial value.
- .bss — global and static variables that start at zero; it takes up no bytes in the file (more on this shortly).
- .symtab — the symbol table mapping names to addresses, used by the linker and debuggers.
file a.out
# a.out: ELF 64-bit LSB pie executable, x86-64, dynamically linked, ...
readelf -h a.out # the ELF header
# Magic: 7f 45 4c 46 02 01 01 00 ... ("\x7f E L F")
# Type: DYN (Position-Independent Executable)
# Machine: Advanced Micro Devices X86-64
# Entry point address: 0x1060Notice the "sections vs segments" distinction, because it trips people up. Sections (.text, .data, and friends) are the fine-grained, link-time view — useful to the linker and the debugger. Segments are the coarse, load-time view: the OS loader does not care about individual sections, it groups them into a few segments and maps each into memory with the right permissions (executable, writable, read-only). One file, two views of the same bytes.
The .bss trick: A global array of a million zeroed integers costs almost nothing on disk: .bss stores only a size, not the zeros. The loader allocates and zero-fills that space in memory at load time. Zero-initialised data is free in the file and paid for only in RAM.
Loading: from file to memory
When you type ./a.out, the shell does not become your program — it asks the kernel to run it, via the exec family of system calls. The remarkable thing about exec is that it does not create a new process; it replaces the contents of the calling process. The shell first forks a copy of itself, and that copy then calls exec, which throws away its own program image and loads yours in its place. (We will study fork and exec properly in the next part; here we care about the loading half.)
Loading means the OS loader reads the ELF file and builds the process address space. It maps the loadable segments into memory at their prescribed addresses with the right permissions, allocates and zero-fills the .bss region, sets up an initial stack containing the command-line arguments (argv) and environment variables (envp), and finally sets the program counter to the entry point from the ELF header. At that point the CPU starts executing your code.
If the executable is dynamically linked, there is one more actor: the dynamic linker/loader, ld.so on Linux. The ELF header names it as an "interpreter", so the kernel hands control to ld.so first. It finds and maps the required shared libraries (the ones ldd listed), patches up the addresses so your calls to printf actually reach libc, and only then jumps into your program. Modern systems do much of this lazily, resolving each function the first time it is called rather than all at once.
- exec()
- A system call that replaces the current process image with a new program, keeping the same process ID.
- Loader
- The OS component that maps an executable’s segments into memory and prepares it to run.
- Entry point
- The address, recorded in the ELF header, where the CPU begins executing the loaded program.
- Dynamic linker (ld.so)
- The interpreter that loads shared libraries and resolves their symbols at load/run time.
Why demand paging matters here: The loader usually does not copy the whole file into RAM up front. It memory-maps the file and lets pages fault in on first use, so a large binary starts fast and only the code you actually run gets loaded. Launching is lazy by design.
The process address space in memory
Once loaded, a process sees a clean, private range of memory addresses — its virtual address space — and that space has a classic layout you should be able to draw from memory. From low addresses upward: the text segment (your code), then read-only data, then initialised globals, then the BSS. Above those sits the heap, which grows upward as you allocate. At the very top sits the stack, which grows downward. Between heap and stack lives the memory-mapped region where shared libraries and large mmap allocations land.
- Text
- The program’s machine instructions. Read-only and executable, so it can be shared safely between processes.
- Data / BSS
- Global and static variables — initialised ones in .data, zero-initialised ones in .bss.
- Heap
- Dynamically allocated memory (malloc / new); grows upward toward higher addresses as you request more.
- Stack
- Per-thread memory for function calls — local variables, return addresses, saved registers; grows downward.
- Memory-mapped region
- Where shared libraries and file mappings sit, in the gap between heap and stack.
size a.out
# text data bss dec hex filename
# 1837 608 8 2453 995 a.out
# text = code + read-only data
# data = initialised globals (occupies file bytes)
# bss = zero-initialised globals (costs RAM, not file bytes)The two ends growing toward each other is a deliberate, elegant design. The heap and stack start far apart and grow into the space between them, so a program can use a lot of one and a little of the other without deciding the split in advance. If they ever collide you get the classic failures — a stack overflow when recursion runs away, or a failed malloc when the heap is exhausted. Each thread gets its own stack; everything else in this picture is shared across the process’s threads.
Every process gets its own map: These are virtual addresses. Two processes can both think their code lives at 0x1060, and the OS, with help from the hardware MMU, quietly maps those identical virtual addresses to different physical RAM. Isolation and the illusion of a private machine, from one mechanism.
Static vs dynamic, and why it matters
Now that we can see memory, the static-vs-dynamic choice stops being academic. The headline win of dynamic linking is memory sharing. Because the text segment of a shared library is read-only, the OS can load libc.so once into physical RAM and map that single copy into every process that uses it. A hundred running programs share one copy of the C library. Statically linked binaries cannot do this — each carries its own private copy, so a hundred programs means a hundred copies in RAM.
- Binary size: dynamic executables are small; static ones bundle every library and can be tens of megabytes.
- RAM across processes: dynamic shares one copy of each .so among all users; static duplicates it in every process.
- Security updates: patch a bug in libc.so once and every dynamically linked program is fixed on next launch; static binaries must each be rebuilt and redeployed.
- Portability / containers: a static binary runs on a machine with no libraries installed at all — which is exactly why they are loved for minimal container images.
The container world sharpens the trade-off. A statically linked Go binary can ship in a FROM scratch image with literally nothing else — no distro, no libc, a handful of megabytes total, and no "works on my machine" library-version drift. That simplicity and reproducibility often outweighs the lost RAM sharing, because in a container you may be running just one program anyway, so there is little to share with.
How to actually decide: Many independent programs on a shared host (a normal Linux desktop or server) → dynamic wins on RAM and patching. A single self-contained service shipped as an image → static wins on size, simplicity, and reproducibility. The right answer depends on what you are optimising for.
Recap: the full journey & what a process now is
Let us stitch the whole path into one sentence you can replay. Your source is preprocessed, compiled to assembly, assembled to object code, and linked into an ELF executable (build time). You run it, and the OS loader — with the dynamic linker if needed — maps that ELF’s segments into a fresh virtual address space, sets up the stack with your arguments, and jumps to the entry point (load time). From there your instructions execute, the heap and stack grow toward each other, and system calls reach into the kernel whenever you need it (run time).
So what is a process, precisely? It is not the file on disk — that is the program. A process is that file brought to life: a private virtual address space holding text, data, heap, and stack; a set of CPU register values including the program counter marking where it is; open files and other resources; and an entry in the kernel’s process table so the OS can track and schedule it. The file is the frozen recipe; the process is everything the OS wraps around it to make it run.
- Build time turns source into a self-describing ELF file with sections and an entry point.
- Load time turns that file into an address space and a runnable process image.
- Run time is the process executing, calling the kernel for anything it cannot do alone.
- A process = address space + registers + OS resources + a program in mid-execution.
Where we go next: We now have exactly one process, sitting in memory, ready to run. The next part asks the harder questions: how does the OS juggle hundreds of these at once, decide who runs when, and switch between them so fast it looks simultaneous? That is the process model and scheduling — and it starts from the process we just built.