Chapter 1 - A Tour of Computer Systems

What happens when we compile and run the hello world program?

Raw content of a program

I’m faily confident I could not talk for 10 hours non-stop about all the things that happen when you write and execute a single program like this:

hello.c
#include <stdio.h>
int main() {
printf("hello, world\n");
return 0;
}

But I do know that on disk, this is all just a stream of bits. Every character, every space, every newline—it’s all encoded as bytes. To inspect its actual content, we can use xxd:

Inspecting hello.c content
xxd -g 1 hello.c # group by single bytes
# Output
00000000: 23 69 6e 63 6c 75 64 65 20 3c 73 74 64 69 6f 2e #include <stdio.
00000010: 68 3e 0a 0a 69 6e 74 20 6d 61 69 6e 28 29 20 7b h>..int main() {
00000020: 0a 20 20 70 72 69 6e 74 66 28 22 68 65 6c 6c 6f . printf("hello
00000030: 2c 20 77 6f 72 6c 64 5c 6e 22 29 3b 0a 20 20 72 , world\n");. r
00000040: 65 74 75 72 6e 20 30 3b 0a 7d 0a eturn 0;.}.

The first byte is 23 (hex), the second is 69, and so on. This uses ASCII encoding, where each byte represents a character. To see what these hex values actually mean:

From hex to ASCII
printf '\x23\n'
#
printf '\x69\n'
i

So 23 is # and 69 is i. Which is exactly how our program starts with #include.

Other programs execute this text file to generate a program from it

To turn this into an executable, we need to translate this program from “human form” to low-level machine instructions for our target platform. This generates an executable object program. We can do this with gcc:

Compiling our program
gcc -o hello hello.c # Output is a hello binary file

This one single command is actually doing 4 things:

  • Preprocessing: Expands macros and includes header files
  • Compiling: Translates C code to assembly language
  • Assembling: Converts assembly to machine code (object files)
  • Linking: Combines object files and libraries into the final executable

The book is going to cover these steps in much more detail later. For now, I’m just acknowledging that this is what happens, even though I don’t fully understand the details yet.

To run our program:

Running the hello program
./hello
hello, world

Hardware

Buses

Buses are like the highways where information travels between the CPU, memory, I/O devices, and other components. They typically transfer information in fixed-size chunks of bytes called a word. The word size is fundamental to how a computer works.

Most modern computers are 64-bit systems, which means they have a word size of 8 bytes (64 bits). So on each transfer, the bus can move up to 64 bits at a time. This is where terms like “32-bit” and “64-bit” come from—they refer to the word size of the system.

Main Memory

Main memory (RAM) holds both the program and the data it manipulates. I like to think of memory as a long stream of bytes, where each byte has its own unique address. The addresses start at 0 and keep going up sequentially.

Processor

The processor is the hardware that executes instructions stored in main memory. It has a Program Counter (PC), which is like an arrow pointing to the instruction it’s currently executing. Once it finishes executing an instruction, the PC moves to point at the next instruction, and the cycle continues. This fetch-execute cycle is the fundamental operation of a CPU.

Cache is king

During program execution, there’s a lot of data moving around. First, we load our program from disk into RAM. Then, instructions are copied from memory to the processor. As the processor runs, it also needs to fetch data from memory constantly.

Here’s the thing: due to physical constraints (the actual distance on the circuit board), it takes time for the processor to reach out to RAM. So modern processors have smaller, faster memories built right into the chip itself: L1, L2, and L3 caches. These are much closer to the CPU cores and thus much faster.

When the processor needs something, it follows a hierarchy: first it checks its registers, then L1 cache, then L2, then L3, then main memory, and finally disk. Each level takes progressively more time to access. I heard a good cooking analogy once that I love it and really hits home on how stupid we’re sometimes.

StorageTime analogyCooking analogy
Register1 secondAlready on his hand
L13–5 secondsOn the cutting board
L2~15 secondsArm’s reach on the counter
L3~1 minuteBehind him on the back counter / shelf
RAM~5–10 minutesIn the pantry
NVMe SSD~2–5 daysAt the grocery store nearby
HDD~6 monthsOn a cargo ship from another country

Operating System

Our program doesn’t interact directly with memory, files, etc. There’s an operating system that manages these resources and provides abstractions for us to use. These abstractions are pure gold:

  • Processes: abstraction of processor, RAM, and disk
  • Files: abstraction of disk (I/O devices)
  • Virtual Memory: abstraction of RAM and disk

Processes

When running a program, the Operating System provides the illusion that this is the only program running. It has exclusive access to memory and disk, running one instruction after the other.

On a single-core CPU, each process fully uses the CPU for a small amount of time, then the OS scheduler grants the CPU access to another process. This gives the illusion that multiple things are running at the same time. This is concurrency in action. The OS is managing multiple processes concurrently, even though only one is actually executing at any given moment.

Files

A file is just a sequence of bits. All I/O devices are modeled as files in Unix. This abstraction allows us to use simple directives to manipulate I/O devices while being unaware of their specific technology. For example, I can create a file without knowing anything about the disk technology—whether it’s an SSD, HDD, or even a network-mounted filesystem.

Virtual Memory

Virtual memory provides an illusion to each process as if that process had exclusive access to memory. It’s divided into virtual address spaces to separate:

  • Program code and data: The actual program instructions and static data
  • Heap: Dynamically allocated memory
  • Shared libraries: Code that can be shared across processes
  • Stack: Function call frames and local variables
  • Kernel virtual memory: Protected memory for the operating system

While working through this chapter, I these two notes which are timeless concepts:

Back to articles