Osdev-Notes

Debugging

GDB

Remote Debugging

First thing Qemu needs to be launched telling it to accepts connections from gdb, it needs the parameters: -s and -S added to the command, where:

target remote localhost:1234

And then we can load the symbols (if we have compiled our os with debugging symbols):

symbol-file path/to/kernel.bin

Useful Commands

Below a list of some useful gdb commands

Both commands accept format specifiers which change the output. For example p/x i will print i, formatted as hexadecimal number. There are a number of other ones /i will format the output as cpu instructions, /u will output unsigned, and /d signed decimal numbers. /c will interpret an ASCII character, and /s will interpret it as a null terminated ASCII string (just a c string).

The format specifier can be prefixed with a number of repeats. For example if we want to examine 10 instructions at address 0x1234, we could do: x/10i 0x1234, and gdb would show us that (i is the identifier for the instruction format), this is pretty useful if we want to look at raw areas of memory. In case we need to print raw memory insted we can use x/10xb 0x1234(where x is the format (hexadecimal) and b the size (bytes).

How Did I Get Here?

Here a collection of useful command to keep track of the call stack.

Breakpoints

A breakpoint can be set in a variety of ways! The command is b/break symbol, where symbol can be a number of things:

Breakpoints can be enabled/disabled at runtime with enable x/disable x where x is the breakpoint number (displayed when we first set it).

Breakpoints can also take conditions if we’re trying to debug a commonly-run function. The syntax follows a c-like style, and is pretty forgiving. For example: break main if i == 0 would break at the function main() whenever the variable i is equal to 0. This syntax supports all sorts of things, like casts and working with pointers.

Breakpoints can also be issued contextually too! If we’re at a breakpoint main.c:123, we can simply use b 234 to break at line 234 in the same file.

It is possible at any time to print the list of breakpoints using the command: info breakpoint

And finally breakpoints can be deleted as well using delete [breakpoints]

It’s worth noting if debugging a kernel running with kvm, is not possible to use software breakpoints (above) like normal. GDB does support hardware breakpoints using hb instead of b for above, although their functionality can be limited, depending on what the hardware supports. Best to do serious debugging without kvm, and only use hardware debugging when absolutely necessary.

In case we want to watch the behaviour of a variable, and interrupt the code every time the variable changes, we can use watchpoints they are similar to breakpoints, but instead of being set for lines of code or functions, they are set to watch variable behaviour.

For example imagine to have the following simple function:

int myVar2 = 3;
int test_function() {
    int myVar = 0;
    myVar = 5;
    myVar2 = myVar
    return myVar;
}

If we want to interrupt the execution when myVar2 changes value this can be easily done with:

watch myVar2

As soon as myVar2 changes from 0 to 5, the execution will stop. This works pretty well for global variables. But what about local variables? Like myVar, the workflow is pretty similar but to catch the watchpoint we need first to set a breakpoint when the variable is in-scope (inside the test_function).

We can use conditions on watchpoint too, in the same way they are used for breakpoints.

Variables

While debugging with gdb, we can change the value of the variables in the code being executed. To do that we just need the command:

set variable_name=value

where variable_name is a variable present in the code being debugged. This is extermely useful in the cases where we want to test some edge cases, that are hard to reproduce.

TUI - Text User Interface

This area of gdb is hilariously undocumented, but still really useful. It can be entered in a number of ways:

Tui layouts can be switched at any time, or we can return to our regular shell at any time using tui disable, or exiting gdb.

The layouts offer little interaction besides the usual terminal in/out, but can be useful for quickly referencing things, or examining exactly what instructions are running.

If we are using debian, we most-likely need to install the gdb package, because by default gdb-minimal is being installed, which doesn’t contain the TUI.

Currently these are the type of tui layouts available in gdb:

When in a view with multiple windows, the command focus xyz can be used to change which window has the current focus. Most key combinations are directed to the currently focused window, so if something isn’t working as expected, that might be why. For example to get back the focus to the command view just type: focus cmd

Virtual Box

Useful Commands

vboxmanage list vms

It will show for every virtual machine, its label and its UUID

virtualboxvm --startvm vmname

The virtual machine name, or its uuid can be used.

Debugging a Virtual Machine

To run a VM with debug two things are needed:

virtualboxvm --startvm vmname --debug

this will open the Virtual Machine with the Debugger command line and ui.

Qemu

Qemu Interrupt Log

If using qemu, a good idea is to dump registers when an exception occurs, we just need to add the following option to qemu command:

qemu -d int

Sometime could be needed to avoid the emulator restart on triple fault, in this case to catch the “offending” exception, just add:

qemu -d int -no-reboot

While debugging with gdb, we may want to keep qemu hanging after a triple fault (when the cpu should reset), to do some more investigation, in this case we need to add also -no-shutdown (along with) -no-reboot

Qemu Monitor

Qemu monitor is a tool used to send complex commands to the qemu emulator, is useful to for example add/remove media images to the system, freeze/unfreeze the VM, and to inspect the state of the virtual machine without using an external debugger.

One way to start Qemu monitor on a unix system is using the following parameter when starting qemu:

qemu-system-i386 [..other params..] -monitor unix:qemu-monitor-socket,server,nowait

then on another shell, on the same folder where we started the emulator launch the following command:

socat -,echo=0,icanon=0 unix-connect:qemu-monitor-socket

This will prompt with a shell similar to the following:

username@host:~/yourpojectpath/$ socat -,echo=0,icanon=0 unix-connect:qemu-monitor-socket
QEMU 6.1.0 monitor - type 'help' for more information
(qemu)

From here is possible to send commands directly to the emulator, below a list of useful commands:

Info mem & Info tlb

These commands are very useful when we need to debug memory related issues, the first command info mem will print the list of active virtual memory mappings, the output format depends on the architecture, for exmple on x86-64, it will be similar to the following:

info mem
ffff800000000000-ffff800100491000 0000000100491000 -rw
ffff800100491000-ffff800100498000 0000000000007000 -r-
ffff800100498000-ffff80010157a000 00000000010e2000 -rw
ffffffff80000000-ffffffff80057000 0000000000057000 -r-
ffffffff80057000-ffffffff8006b000 0000000000014000 -rw

Where every line describes a single virtual memory mapping. The fields are (ordered left to right): base address, limit, size and the three common flags (user, read, write).

The other command, info tlb, shows the state of the translation lookaside buffer. In qemu this is shown as individual address translations, and can be quite verbose. An example of what the output might look like is shown below:

info tlb
ffffffff80062000: 000000000994a000 XG------W
ffffffff80063000: 000000000994b000 XG------W
ffffffff80064000: 000000000994c000 XG--A---W
ffffffff80065000: 000000000994d000 XG-DA---W
ffffffff80066000: 000000000994e000 XG-DA---W
ffffffff80067000: 000000000994f000 XG-DA---W
ffffffff80068000: 0000000009950000 XG-DA---W
ffffffff80069000: 0000000009951000 XG-DA---W
ffffffff8006a000: 0000000009952000 XG-DA---W

In this case the line contains: virtualaddress: physicaladdress flags. The command is not available on all architectures, so if developing on an architecture different from x86-64 it could not be present.

Debugcon

Qemu (and several other emulators - bochs for example) support something called debugcon. It’s an extremely simple protocol, similar to serial - but with no config, where anything written to port 0xE9 in the VM will appear byte-for-byte at where we tell qemu to put it. Same is true with input (although this is quite buggy, best to use serial for this). To enable it in qemu add this to the qemu flags -debugcon where. Where can be anything really, a log file for example. We can even use -debugcon /dev/stdout to have the output appear on the current terminal.

It’s worth noting that because this is just a binary stream, and not a serial device emulation, its much faster than usual port i/o. And there’s no state management or device setup to worry about.