All Articles

Reading xv6 OS Seriously to Fully Understand the Kernel – GDB Debug Environment Setup

This page has been machine-translated from the original page.

I have been reading xv6 OS, inspired by First OS Code Reading – Learning Kernel Mechanisms with UNIX V6.

I want to get better at reverse engineering and to deepen my knowledge of kernels and operating systems.

Understanding the Linux Kernel was quite heavy going, so I was looking for somewhere lighter to start. That is when I learned that UNIX V6 has a total code size of around 10,000 lines — just barely comprehensible for a human being — and I became interested.

However, UNIX V6 itself does not run on x86 CPUs, so I decided to read the source code of kash1064/xv6-public: xv6 OS, which is a fork of xv6 OS, an x86-architecture port of UNIX V6.

In the previous article, we read through the xv6 OS build and boot process.

I was eager to start reading the kernel proper, but there were parts I could not understand just by reading the code. So I decided to set up a debugging environment first to help deepen my understanding.

Table of Contents

Debugging xv6 OS with QEMU-GDB

The following article was helpful as a reference for the basic procedure.

Reference: Creating a debug environment for xv6 - Qiita

In my environment I wanted the QEMU console to appear in a separate GUI window, so unlike the article above I am using qemu-gdb rather than qemu-nox-gdb.

Connecting the debugger is very straightforward — simply run the following command:

# Run in the same directory as the Makefile
make qemu-gdb

Next, open another terminal and enter the following commands to enable debugging:

# Specify the kernel binary as the debug target in gdb
gdb kernel

# Remote debug via gdb
target remote localhost:26000

First, make qemu-gdb builds xv6 OS and then invokes qemu-system-i386 -serial mon:stdio -drive file=fs.img,index=1,media=disk,format=raw -drive file=xv6.img,index=0,media=disk,format=raw -smp 2 -m 512 -S -gdb tcp::26000.

The QEMU option arguments are examined one by one below.

QEMU Option Arguments During Debugging

The option arguments used when running make qemu-gdb are as follows:

Option Argument Purpose
-serial <dev> Redirect the virtual serial device to the host.
With mon:stdio, both the console and the QEMU monitor are displayed in the terminal.
-drive <options> Add a new block device or interface.
Here, xv6.img and fs.img are each loaded as a disk.
-smp <cpus> Emulate SMP (multi-processor system) using the specified number of CPUs.
-m <MB or GB> Specify the memory size at virtual machine startup (default unit: MB).
A prefix such as 1G can be used to specify gigabytes.
-S Do not use the CPU at startup (i.e., halt immediately after power-on and wait for a GDB connection).
-gdb <tcp::port> Accept GDB connections on the specified protocol and port.

Reference: Invocation — QEMU documentation

Reference: QEMU Options Cheat Sheet for Kernel Debugging - Qiita

Trying the Debugger

Symbol information for the xv6 kernel is stored in kernel.sym at build time.

Looking up the address of a symbol on the GDB side gives the same result:

$ info address main
Symbol "main" is a function at address 0x80103040.

Set a breakpoint at the main function:

$ main
$ c
Continuing.
[----------------------------------registers-----------------------------------]
EAX: 0x80103040 --> 0xfb1e0ff3 
EBX: 0x10094 --> 0x0 --> 0xf000ff53 
ECX: 0x0 --> 0xf000ff53 
EDX: 0x1f0 --> 0xf000ff53 
ESI: 0x10094 --> 0x0 --> 0xf000ff53 
EDI: 0x0 --> 0xf000ff53 
EBP: 0x7bf8 --> 0x0 --> 0xf000ff53 
ESP: 0x8010b5c0 --> 0x0 --> 0xf000ff53 
EIP: 0x80103040 --> 0xfb1e0ff3
EFLAGS: 0x86 (carry PARITY adjust zero SIGN trap interrupt direction overflow)
[-------------------------------------code-------------------------------------]
   0x80103034 <mpenter+20>:	call   0x801027a0 <lapicinit>
   0x80103039 <mpenter+25>:	call   0x80102fe0 <mpmain>
   0x8010303e:	xchg   ax,ax
=> 0x80103040 <main>:	endbr32 
   0x80103044 <main+4>:	lea    ecx,[esp+0x4]
   0x80103048 <main+8>:	and    esp,0xfffffff0
   0x8010304b <main+11>:	push   DWORD PTR [ecx-0x4]
   0x8010304e <main+14>:	push   ebp
[------------------------------------stack-------------------------------------]
0000| 0x8010b5c0 --> 0x0 --> 0xf000ff53 
0004| 0x8010b5c4 --> 0x0 --> 0xf000ff53 
0008| 0x8010b5c8 --> 0x0 --> 0xf000ff53 
0012| 0x8010b5cc --> 0x0 --> 0xf000ff53 
0016| 0x8010b5d0 --> 0x0 --> 0xf000ff53 
0020| 0x8010b5d4 --> 0x0 --> 0xf000ff53 
0024| 0x8010b5d8 --> 0x0 --> 0xf000ff53 
0028| 0x8010b5dc --> 0x0 --> 0xf000ff53 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Thread 1 hit Breakpoint 1, main () at main.c:19

In my environment I have gdb-peda enabled, so quite a bit of information is displayed.

This completes the setup for debugging the kernel.

Summary

I originally planned to use bochs, but the troubleshooting did not go well, so I decided to use GDB for debugging instead.

This approach turned out to be better in the end since it is easier to configure and I am more comfortable with it.

This time I will really, truly start reading the kernel proper.