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 ?? ()
(gdb)

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
(gdb)

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
Continuing.

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

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
(gdb)

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