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:
#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:
xxd -g 1 hello.c # group by single bytes
# Output00000000: 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("hello00000030: 2c 20 77 6f 72 6c 64 5c 6e 22 29 3b 0a 20 20 72 , world\n");. r00000040: 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:
printf '\x23\n'#
printf '\x69\n'iSo 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:
gcc -o hello hello.c # Output is a hello binary fileThis 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:
./hellohello, worldHardware
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.
| Storage | Time analogy | Cooking analogy |
|---|---|---|
| Register | 1 second | Already on his hand |
| L1 | 3–5 seconds | On the cutting board |
| L2 | ~15 seconds | Arm’s reach on the counter |
| L3 | ~1 minute | Behind him on the back counter / shelf |
| RAM | ~5–10 minutes | In the pantry |
| NVMe SSD | ~2–5 days | At the grocery store nearby |
| HDD | ~6 months | On 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
Related Notes
While working through this chapter, I these two notes which are timeless concepts:
- Amdahl’s Law - Understanding the fundamental limit of parallelization
- Concurrency and Parallelism - The difference between doing multiple things at once vs. simultaneously