Familiarity with your environment is crucial for productive development and debugging. This page gives a brief overview of the JOS environment and useful GDB and QEMU commands. Don’t take our word for it, though. Read the GDB and QEMU manuals. These are powerful tools that are worth knowing how to use.
You’ll use two sets of tools in this class: an x86 emulator, QEMU, for running your kernel; and a compiler toolchain, including assembler, linker, C compiler, and debugger, for compiling and testing your kernel.
If you work on
attu, we have set up these tools there. Run the
following command or add it to your shell’s startup file
.bashrc for bash), and you’re all set:
If you want to work on your own Linux, a recent version of Debian/Ubuntu/Fedora/Arch should work. We do not recommend using the CSE VM (CentOS), as its toolchains are out-of-date (unless you want to build them from source code yourself).
It is possible to install the toolchains on macOS (tested on 10.12 Sierra). We provide Homebrew formulas to simplify the building process. First, install Homebrew. If you already have QEMU installed, please uninstall it first. Then, type the following commands:
Wait a while and you should be good to go.
If you want to build the toolchains yourself, read on.
QEMU is a modern and fast PC emulator. Unfortunately, QEMU’s debugging facilities, while powerful, are somewhat immature, so we highly recommend you use our patched version of QEMU instead of the stock version that may come with your distribution.
To build your own patched version of QEMU, you may need to install the SDL development libraries to get a graphical VGA window. On Debian/Ubuntu, this is the libsdl-dev package.
Add --prefix=PREFIX to
if you don’t want to install QEMU to the default path,
For JOS, you’ll need a compiler toolchain that generates code for 32-bit x86 in the ELF binary format. Modern Linux/BSD x86 distributions already provide a suitable compiler toolchain. You should be all set if you use one of these distributions.
On 64-bit machines, you may need to install a 32-bit support library.
The symptom is that linking fails with error messages like
__udivdi3 not found and
__muldi3 not found.
On Debian/Ubuntu, try this to fix the problem:
Some labs need the
gccgo compiler. Install the corresponding package
gcc-go) if you don’t have it.
GDB is your friend. Use make qemu-gdb (or the make qemu-gdb-nox variant) to make QEMU wait for GDB to attach. See the GDB reference below for some commands that are useful when debugging kernels.
If you’re getting unexpected interrupts, exceptions, or triple
faults, you can ask QEMU to generate a detailed log of interrupts
To debug virtual memory issues, try the QEMU monitor commands info mem (for a high-level overview) and info pg (for lots of detail). Note that these commands only display the current page table.
(Lab 4+) To debug multiple CPUs, use GDB’s thread-related commands like thread and info threads.
GDB also lets you debug user environments, but there are a few things you need to watch out for, since GDB doesn’t know that there’s a distinction between multiple user environments, or between user and kernel.
You can start JOS with a specific user environment using
make run-name (or you can edit
To make QEMU wait for GDB to attach,
use the make run-name-gdb variant.
You can symbolically debug user code, just like you can kernel code,
but you have to tell GDB which symbol table to use with the
symbol-file command, since it can only use one symbol
table at a time. The provided
.gdbinit loads the kernel symbol
obj/kern/kernel. The symbol table for a user environment
is in its ELF binary, so you can load it using symbol-file
obj/user/name. Don’t load symbols from any
files, as those haven’t been relocated by the linker (libraries are
statically linked into JOS user binaries, so those symbols are
already included in each user binary). Make sure you get the right
user binary; library functions will be linked at different EIPs in
different binaries and GDB won’t know any better!
(Lab 4+) Since GDB is attached to the virtual machine as a whole, it sees clock interrupts as just another control transfer. This makes it basically impossible to step through user code because a clock interrupt is virtually guaranteed the moment you let the VM run again. The stepi command works because it suppresses interrupts, but it only steps one assembly instruction. Breakpoints generally work, but watch out because you can hit the same EIP in a different environment (indeed, a different binary altogether!).
GNUmakefile includes a number of phony targets for running
JOS in various ways. All of these targets configure QEMU to listen
for GDB connections (the
*-gdb targets also wait for this connection).
To start once QEMU is running, simply run make gdb from your
We provide a
.gdbinit file that automatically points GDB at QEMU,
loads the kernel symbol file, and switches between 16-bit and 32-bit
mode. Exiting GDB will shut down QEMU.
make qemu, but run with only the serial console. To exit, press Ctrl-a x. This is particularly useful over SSH connections because the VGA window consumes a lot of bandwidth.
make qemu, but rather than passively accepting GDB connections at any time, this pauses at the first machine instruction and waits for a GDB connection.
GNUmakefile also accepts a few useful variables:
When building JOS, the makefile produces some additional output files that may prove useful while debugging:
obj/user/hello.asm, etc: assembly code listings for the bootloader, kernel, and user programs.
obj/user/hello.sym, etc: symbol tables for the kernel and user programs.
obj/user/hello, etc.: linked ELF images of the kernel and user programs; these contain symbol information that can be used by GDB.
See the GDB manual for a full guide to GDB commands. Here are some particularly useful commands for JOS, some of which don’t typically come up outside of OS development.
eflags, and the segment selectors. For a much more thorough dump of the machine register state, see QEMU’s own
$eipas addr will display the instructions at the current instruction pointer.
obj/kern/kernel. If the machine is running user code, say
hello.c, you can switch to the
hellosymbol file using
QEMU represents each virtual CPU as a thread in GDB, so you can use all of GDB’s thread-related commands to view or manipulate QEMU’s virtual CPUs.
QEMU includes a built-in monitor that can inspect and modify the machine state in useful ways. To enter the monitor, press Ctrl-a c in the terminal running QEMU. Press Ctrl-a c again to switch back to the serial console.
For a complete reference to the monitor commands, see the QEMU manual. Here are some particularly useful commands:
info registers: display a full dump of the machine’s internal register state. In particular, this includes the machine’s hidden segment state for the segment selectors and the local, global, and interrupt descriptor tables, plus the task register. This hidden state is the information the virtual CPU read from the GDT/LDT when the segment selector was loaded. Here’s the CS when running in the JOS kernel in lab 1 and the meaning of each field:
CS =0008 10000000 ffffffff 10cf9a00 DPL=0 CS32 [-R-]
CS =0008: the visible part of the code selector. We’re using segment 0x8. This also tells us we’re referring to the global descriptor table (0x8&4=0), and our CPL (current privilege level) is 0x8&3=0.
10000000: the base of this segment. Linear address = logical address +
ffffffff: the limit of this segment. Linear addresses above
0xffffffffwill result in segment violation exceptions.
10cf9a00: the raw flags of this segment, which QEMU helpfully decodes for us in the next few fields.
DPL=0: the privilege level of this segment. Only code running with privilege level 0 can load this segment.
CS32: this is a 32-bit code segment. Other values include
DSfor data segments (not to be confused with the
LDTfor local descriptor tables.
[-R-]:: this segment is read-only.
info mem: (Lab 2+) display mapped virtual memory and permissions. For example,
ef7c0000-ef800000 00040000 urw efbf8000-efc00000 00008000 -rw
tells us that the
0x00040000 bytes of memory from
0xef800000 are mapped read/write and user-accessible, while the
0xefc00000 is mapped read/write, but
(Lab 2+) display the current page table structure. The output is
info mem, but distinguishes page directory entries and
page table entries and gives the permissions for each separately.
Repeated PTE’s and entire page tables are folded up into a single
line. For example,
VPN range Entry Flags Physical page [00000-003ff] PDE -------UWP [00200-00233] PTE[200-233] -------U-P 00380 0037e 0037d 0037c 0037b 0037a .. [00800-00bff] PDE ----A--UWP [00800-00801] PTE[000-001] ----A--U-P 0034b 00349 [00802-00802] PTE -------U-P 00348
This shows two page directory entries, spanning virtual addresses
Both PDE’s are present, writable, and user and the second PDE is
also accessed. The second of these page tables maps three pages,
spanning virtual addresses
0x00802fff, of which
the first two are present, user, and accessed and the third is
only present and user. The first of these PTE’s maps physical
QEMU also takes some useful command line arguments, which can be
passed into the JOS makefile using the
make QEMUEXTRA='-d int' ...:
log all interrupts, along with a full register dump, to
You can ignore the first two log entries, “SMM: enter” and “SMM:
after RMS”, as these are generated before entering the boot loader.
After this, log entries look like:
4: v=30 e=0000 i=1 cpl=3 IP=001b:00800e2e pc=00800e2e SP=0023:eebfdf28 EAX=00000005 EAX=00000005 EBX=00001002 ECX=00200000 EDX=00000000 ESI=00000805 EDI=00200000 EBP=eebfdf60 ESP=eebfdf28 ...
The first line describes the interrupt. The
4: is just a log
v gives the vector number in hex. e gives the
i=1 indicates that this was produced by an
instruction (versus a hardware interrupt). The rest of the line
should be self-explanatory. See
info registers for a description
of the register dump that follows.