0 Setting up the development environment

Every listing in this book was produced by running the commands you see, on the tools described in this chapter. If your tools differ, the output you get will differ too: addresses move, instructions change, and a reader who cannot match what is on the page to what is on the screen quickly loses confidence. The first edition of this book was written against Ubuntu 16.04 and gcc 5.4; readers who tried to follow it a few years later found that the same command produced a different program, and had no way to know whether they or the book were at fault. This chapter exists so that this never happens to you. Spend half an hour here before starting chapter 1, and the rest of the book will behave exactly as printed.

0.1 The tools

We need surprisingly few tools to write an operating system, and all of them are free software that has been around for decades:

The book targets 32-bit x86. On a 64-bit Linux this means gcc must be able to produce 32-bit code, which most distributions package separately under a name such as multilib. This is the single most common installation problem, so check it first (see the smoke test below).

0.2 Option A: install the tools on Linux

On Debian or Ubuntu:

$ sudo apt install build-essential gcc-multilib nasm gdb qemu-system-x86 make git

On Fedora:

$ sudo dnf install gcc glibc-devel.i686 libgcc.i686 nasm gdb qemu-system-x86 make git

On Arch Linux, after enabling the multilib repository in /etc/pacman.conf:

$ sudo pacman -S base-devel lib32-glibc lib32-gcc-libs nasm gdb qemu-system-x86 make git

The exact versions do not matter much for Part I and Part II, with one important exception discussed in the next section: modern compilers produce position-independent executables by default, and the book’s commands include flags to turn that off. If your machine is not an x86 computer at all, for example a laptop with an ARM processor, your gcc cannot produce x86 code and you should use option B.

The repository of the book ships a container image definition, tools/Dockerfile, that installs exactly the tool versions used to produce the listings. It works on Linux, on Windows (through WSL 2 and Docker Desktop) and on macOS (Docker Desktop, including Apple silicon machines, where the x86 tools are emulated). This is also what the continuous integration of the repository runs, so if the book builds and boots there, it will build and boot for you.

Build the image once:

$ git clone https://github.com/tuhdo/os01.git
$ cd os01
$ docker build -t os01 tools/

Then start a shell in it whenever you want to work, with the repository mounted at /work:

$ docker run --rm -it --user "$(id -u):$(id -g)" --security-opt seccomp=unconfined -v "$PWD":/work -w /work os01

The --user option makes the files you create belong to you rather than to root; the -v option shares the current directory with the container; --security-opt seccomp=unconfined lets gdb turn off address-space randomization for the programs it debugs in chapter 6 (Docker’s default system-call filter forbids it, and gdb would print a warning at every run). Everything else in the book is the same whether you are inside the container or on a Linux installation of your own.

The versions inside the image at the time of writing:

tool version
gcc 14.2.0 (Debian)
binutils 2.44
nasm 2.16.03
gdb 16.3
QEMU 10.0
make 4.4.1

0.4 Why the book’s gcc commands carry extra flags

Throughout Part I you will compile small C programs and look at the machine code gcc produced for them. The commands look like this:

$ gcc $BOOKFLAGS hello.c -o hello

where BOOKFLAGS is a shell variable that the container already defines, and that you should define yourself if you installed the tools natively (put the line in your ~/.bashrc):

$ export BOOKFLAGS="-m32 -no-pie -fno-pie -fno-asynchronous-unwind-tables -fcf-protection=none -O0"

That is a lot of flags for a Hello World, and each one is there for a reason. Here is what happens without them. Take this program:

hello.c

#include <stdio.h>

int main(int argc, char *argv[])
{
    printf("Hello World!\n");
    return 0;
}

Compile it the simple way and disassemble main (the objdump command is explained in chapter 4; for now just look at the shape of the output):

$ gcc -m32 hello.c -o hello
$ objdump -M intel -d hello

0000118d <main>:
    118d:   8d 4c 24 04             lea    ecx,[esp+0x4]
    1191:   83 e4 f0                and    esp,0xfffffff0
    1194:   ff 71 fc                push   DWORD PTR [ecx-0x4]
    1197:   55                      push   ebp
    1198:   89 e5                   mov    ebp,esp
    119a:   53                      push   ebx
    119b:   51                      push   ecx
    119c:   e8 28 00 00 00          call   11c9 <__x86.get_pc_thunk.ax>
    11a1:   05 53 2e 00 00          add    eax,0x2e53
    11a6:   83 ec 0c                sub    esp,0xc
    11a9:   8d 90 14 e0 ff ff       lea    edx,[eax-0x1fec]
    11af:   52                      push   edx
    11b0:   89 c3                   mov    ebx,eax
    11b2:   e8 89 fe ff ff          call   1040 <puts@plt>
    ...

Now compile with the book’s flags:

$ gcc -m32 -no-pie -fno-pie -fno-asynchronous-unwind-tables -fcf-protection=none -O0 hello.c -o hello
$ objdump -M intel -d hello

08049166 <main>:
 8049166:   8d 4c 24 04             lea    ecx,[esp+0x4]
 804916a:   83 e4 f0                and    esp,0xfffffff0
 804916d:   ff 71 fc                push   DWORD PTR [ecx-0x4]
 8049170:   55                      push   ebp
 8049171:   89 e5                   mov    ebp,esp
 8049173:   51                      push   ecx
 8049174:   83 ec 04                sub    esp,0x4
 8049177:   83 ec 0c                sub    esp,0xc
 804917a:   68 08 a0 04 08          push   0x804a008
 804917f:   e8 bc fe ff ff          call   8049040 <puts@plt>
 8049184:   83 c4 10                add    esp,0x10
 8049187:   b8 00 00 00 00          mov    eax,0x0
 804918c:   8b 4d fc                mov    ecx,DWORD PTR [ebp-0x4]
 804918f:   c9                      leave
 8049190:   8d 61 fc                lea    esp,[ecx-0x4]
 8049193:   c3                      ret

The two programs do the same thing, but the second one is what the rest of this book expects you to see:

When a listing needs debugging information, -g is added explicitly. When a listing was produced with other flags, the command is printed next to it. If you ever get different output from what is printed, the first thing to check is the flags, the second is the tool versions in the table above.

0.5 Smoke test

Before moving on, verify that everything works end to end. The repository contains the code of every chapter under code/. From the top of the repository (inside the container if you chose option B):

$ make -C code/chapter7/os
$ make -C code/chapter7/os test

The first command assembles the bootloader of chapter 7, creates a floppy-disk image and writes the bootloader into its first sector. The second boots that image in QEMU without a display, attaches gdb to the emulated machine and checks that the CPU reaches the code that the bootloader loaded. You should see:

boot-test: ok, stopped at 0x500

Then try the interactive version. In one terminal:

$ make -C code/chapter7/os qemu

A QEMU window opens and stays black: the machine is stopped at its first instruction, waiting for a debugger. In a second terminal:

$ cd code/chapter7/os
$ gdb

The .gdbinit file in that directory connects to QEMU and sets breakpoints, exactly as chapter 7 will explain. Type c to let the machine run, and the QEMU window shows the BIOS booting from the floppy. Type q to leave gdb, and close QEMU. If any of these steps fails, fix it now; the error messages of a missing 32-bit library or a missing QEMU are far easier to understand here than halfway through chapter 8.

If gdb refuses to load the .gdbinit file, it is because recent versions only auto-load scripts from directories you have declared safe. Add this line to ~/.gdbinit (the container already has it):

add-auto-load-safe-path /

0.6 A note on Windows and macOS

The book uses Linux because its tools are the ones the x86 world was built with, and because the hosted programs in Part I are ELF files, the same format our kernel will use. Windows produces PE files and macOS produces Mach-O files, so even the compiler steps of chapter 4 would look different there. The container gives you a complete Linux environment in a few minutes; use it rather than trying to replicate the tools natively.

Exercise 0.1. Run gcc -v on your machine and find the --enable-default-pie option in its configuration line. Then compile hello.c once with and once without -no-pie -fno-pie, and run file hello on each result. One is reported as pie executable, the other as executable.

Exercise 0.2. The linker has a built-in default script that it uses when none is given. Display it with ld --verbose and find the line that places the first byte of code at 0x08048000 (32-bit) or 0x400000 (64-bit). You will write your own version of this script in chapter 8.

Exercise 0.3. Run qemu-system-i386 -machine help. The book uses the q35 machine, described in chapter 3. Find it in the list, together with the older pc machine that QEMU uses by default.

0.7 Check your understanding

  1. Why does the book compile every hosted program with -no-pie -fno-pie? What does position independence buy a hosted program, and why is it in our way?

  2. The two listings of main do the same thing, yet the first one calls __x86.get_pc_thunk.ax. What does that call compute, and why does the second listing not need it?

  3. Your machine is 64-bit. Why does the book insist on -m32 for the hosted programs of Part I rather than teaching 64-bit code from the start?

  4. What is the difference between the roles of QEMU and gdb in the smoke test? What would you lose with only one of them?

  5. Why does the container run with --security-opt seccomp=unconfined? What goes wrong in chapter 6 without it?

  6. A listing you reproduce shows different addresses from the book. In what order should you check things, and why is “the book is wrong” the last hypothesis?

  7. gcc exists on Windows and on macOS. Why can the book’s steps still not be followed natively there?