Debugging PC Boot Sectors

Written by Gwen Weinholt on 2016-11-09

Recently while working on Zabavno the emulation was going wrong somewhere in a boot sector and I needed to check what it was actually supposed to be doing. This is pretty easy to do with QEMU as a remote target in gdb, but it can be tricky to get started.

In gdb there is support for something called remote debugging, which uses a simple protocol that allows gdb to inspect the state of the target program. This is a pretty neat trick, since it lets gdb talk with programs that are even running on other machines. All it needs is a remote stub accessible over a TCP connection or even a serial port.

There are a few remote stubs available in the gdb distribution that you can link with your program. But the protocol itself is not very complicated and can even be implemented in a few hundred lines of assembly. It is a packet based protocol with checksums, one-byte commands and simple responses.

Fortunately QEMU already implements this protocol. The command line argument -gdb dev or the shortcut argument -s can be used to enable the stub. The latter uses TCP port 1234, which may or may not be suitable for your environment.

Open up a terminal and start QEMU with a gdb stub on TCP port 1234, booting the floppy image fdboot.img:

qemu-system-x86_64 -s -fda fdboot.img -boot a

(Don’t use KVM, breakpoints don’t seem to work properly with it). A graphical window will be created and the system will start up. In another terminal start gdb and attach it to the target:

$ gdb
GNU gdb (Debian 7.7.1+dfsg-5) 7.7.1
(gdb) target remote localhost:1234
Remote debugging using localhost:1234
0x0000a17e in ?? ()

The output from gdb is not very enlightening at this point. That’s because it doesn’t know that the target is actually running 16-bit real-address mode code (QEMU forgot to tell it). In this mode most instructions have a slightly different meaning compared to 32-bit and 64-bit mode, so the disassembly will be different. And furthermore the instruction pointer is not simply $pc anymore, the code segment has to be taken into account. The $cs register contains the start of the code segment shifted right by four bits.

The following commands switch gdb to 16-bit code, sets it to display the next instruction and for good measure switches to Intel’s own assembly syntax:

(gdb) set architecture i8086
warning: A handler for the OS ABI "GNU/Linux" is not built into this configuration
of GDB.  Attempting to continue with the default i8086 settings.

The target architecture is assumed to be i8086
(gdb) set disassembly-flavor intel
(gdb) display/i $cs*16+$pc
(gdb) target remote localhost:1234
Remote debugging using localhost:1234
0x00001362 in ?? ()
1: x/i $cs*16+$pc
   0x101352:    sti

Looks like the target has gotten pretty far away from the boot sector by now, judging by the instruction pointer. PC boot sectors are loaded at the address 0000:7C00. Set a breakpoint at that address:

(gdb) b *0x7c00
Breakpoint 1 at 0x7c00

Now the target needs to be rebooted. This can be done in a number of ways. It can be done manually to switching to the QEMU window and pressing Ctrl-Alt-2 to get the QEMU monitor, typing the command system_reset and switching back with Ctrl-Alt-1. Another option is to have the target jump to the BIOS reset vector F000:FFF0:

(gdb) set $cs = 0xf000
(gdb) set $pc = 0xfff0

When the target resumes execution it will reset, go through the BIOS initialization and then jump to the start of the boot sector. Type c to continue:

(gdb) c

Breakpoint 1, 0x00007c00 in ?? ()
1: x/i $cs*16+$pc
=> 0x7c00:      jmp    0x7c3e

The boot sector can now be debugged normally:

(gdb) si
0x00007c3e in ?? ()
1: x/i $cs*16+$pc
=> 0x7c3e:      cli
(gdb) x/5i $cs*16+$pc
=> 0x7c3e:      cli
   0x7c3f:      cld
   0x7c40:      xor    ax,ax
   0x7c42:      mov    ds,ax
   0x7c44:      mov    bp,0x7c00
(gdb) info registers
eax            0xaa55   43605
ecx            0x0      0
edx            0x0      0
ebx            0x0      0
esp            0x6f2c   0x6f2c
ebp            0x0      0x0
esi            0x0      0
edi            0x0      0
eip            0x7c3e   0x7c3e
eflags         0x202    [ IF ]
cs             0x0      0
ss             0x0      0
ds             0x0      0
es             0x0      0
fs             0x0      0
gs             0x0      0

A summary of commands (remove the prompt and the comments before using):

$ qemu-system-x86_64 -s -fda fdboot.img -boot a
(gdb) target remote localhost:1234
(gdb) set architecture i8086  # 16-bit mode
(gdb) set disassembly-flavor intel
(gdb) display/i $cs*16+$pc  # show next instruction
(gdb) b *0x7c00             # breakpoint at boot sector
(gdb) set $cs = 0xf000
(gdb) set $pc = 0xfff0      # reboot
(gdb) c                     # continue execution
(gdb) si                    # step instruction
(gdb) x/5i $cs*16+$pc       # disassemble five instrs
(gdb) info registers        # show all registers