📜 ⬆️ ⬇️

Using the GDB Debugger to the Maximum

In our daily work, like everyone, it takes a lot to use the debugger. Due to the specifics of the work: (OS development, use of virtualization technologies like Intel-VT, etc.) we often need to use a debugger to work with specific cases: debugging the kernel loader code, debugging the virtual machine loaders, as well as providing the opportunity debug your own OS. It is these special cases that are so pathetically named in the title “to the maximum”.

To solve all these problems (and of course, many others) we use gdb. It is possible to use such shells as DDD, but personally I prefer to use cgdb as the optimal choice, especially for the case of working with the ssh debugger.
In this article, we will discuss how gdb can be used to debug boot sector code and bootloaders.

At the beginning of development of any new OS, it is required to ensure the presence of a banal loader for this OS. The “truth of life” of the OS developer lies primarily in the fact that the processor starts to execute the boot sector code slice after the BIOS Post in Real Mode. At this point, no OS has yet been loaded (only 512 bytes of the boot sector), while for the full debugger support we need to implement a special module in the OS kernel (more on this in Part 2). The question arises: how to debug the boot sector code and bootloader? After all, before you start working with the main OS debugger (through a special module), you need to load the “system loader” into RAM, load the kernel and its basic modules, basic hardware initialization (to work with gdb you need at least Serial Port), and only then working with a full OS debugger becomes possible.
This problem can be solved, as it turns out, quite simply: you need to start loading the OS inside some virtual machine that supports the built-in debugging functions.
')
As an example, we will describe the use of qemu:
1. Start the virtual machine (for example, using a simple boot from a floppy disk):
qemu -fda ./boot.fdd -s -S -vnc none &
(We start the qemu virtual machine that will boot from a floppy disk whose image is in the “./boot.fdd” file; “-s –S” means that the machine will start in suspended mode and in debug mode; “-vnc none” means that the machine will start without an active terminal (that is, in the background - especially convenient when working through ssh, and to debug the bootloader and boot sector you rarely need to see the computer screen); “&” - we run the virtual machine in the background).
2. Now, run the gdb debugger itself:
$ gdb
GNU gdb (Ubuntu/Linaro 7.2-1ubuntu11) 7.2
Copyright (C) 2010 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-linux-gnu".
For bug reporting instructions, please see:
<www.gnu.org/software/gdb/bugs>.
(gdb)

3. In the gdb debugger, connect to the qemu port:
(Gdb) target remote localhost: 1234

By the way, this port is visible in netstat:
$ netstat –tlpn
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0: 1234 0.0.0.0:* LISTEN 4014/qemu


Pay attention to the address from which we start debugging:
(gdb) target remote localhost: 1234
Remote debugging using localhost: 1234
0x0000fff0 in ?? ()
(gdb)


From this address, any processor starts its operation at power on. In other words, we were right at the beginning of the BIOS (inside the virtual machine, of course).
4. Next, we need to prepare gdb for debugging Real Mode code:
(gdb) set arch i8086
The target architecture is assumed to be i8086
(gdb)


5. Now we need to put the first breakpoint at the beginning of our boot sector:
(gdb) break * 0x7c00
Breakpoint 1 at 0x7c00
(gdb)


(The BIOS will load the first 512 bytes from the specified device (from a floppy disk) into memory at 0x7c00 and transfer control to this memory area).

6. Jump to our boot sector:
(gdb) c
Continuing.

Breakpoint 1, 0x00007c00 in ?? ()
(gdb)


7. Done! You can debug our code!
There are a number of debugging debugging code features that you have to work with.
First, you need to take into account the specifics of addressing data in RealMode mode. Any information is addressed as segment: offset, and the address can be calculated as: segment * 16 + offset. This means that in order to read the first 10 code instructions, starting with the current instruction, you need to use the command: (gdb) x/10i $cs*16+$eip . The disadvantage in this case is that the current gdb instruction shows without taking into account the base of the code segment:
(gdb) x/10i $cs*16+$eip
0x80000: jmp 0x90013
0x80002: (bad)
0x80003: mov $0x8,%di
0x80006: add $0xff,%al
0x80008: aas
0x80009: add %al,(%bx,%si)
0x8000b: add %al,(%bx,%si)
0x8000d: add %al,(%bx,%si)
0x8000f: add %al,(%bx,%si)
0x80011: add %al,(%bx,%si)
(gdb)


While the instruction address is equal to:
Program received signal SIGTRAP, Trace/breakpoint trap.
0x00000000 in ?? ()
(gdb)


Secondly, you have to manually set and remove breakpoints. This means that if you set a breakpoint to the address:
(gdb) break *0x80017
Breakpoint 4 at 0x800 17

... then hit him ..:
(gdb) c
Continuing.

Program received signal SIGTRAP, Trace/breakpoint trap.
0x000000 17 in ?? ()
(gdb)


... and then you will make stepi, then you will again get to breakpoint, instead of following the instructions and moving to the next one:
Program received signal SIGTRAP, Trace/breakpoint trap.
0x000000 17 in ?? ()
(gdb) stepi
0x000000 17 in ?? ()
(gdb)


Therefore, the debugging process might look something like this:
1. somewhere in the bootloader code:
Program received signal SIGTRAP, Trace/breakpoint trap.
0x00000023 in ?? ()
(gdb) x/10i $cs*16+$eip
0x80023: mov %ebx,0xb
0x80028: movl $0x0,0xf
0x80031: call 0x80c15
0x80034: or %ax,%ax
0x80036: je 0x80087
0x80038: call 0x809af
0x8003b: call 0x80193
0x8003e: or %ax,%ax
0x80040: je 0x80268
0x80042: mov $0x3f4,%dx
(gdb)

2. we set breakpoint, cut after call:
(gdb) break *0x8003e
Breakpoint 6 at 0x8003e


3. run the program before this breakpoint:
(gdb) c
Continuing.

Program received signal SIGTRAP, Trace/breakpoint trap.
0x0000003e in ?? ()
(gdb)

4. remove breakpoint:
(gdb) delete 6
(gdb)

5. check the return value:
(gdb) print /x $ax
$2 = 0x8000
(gdb)

6. take a step further:
(gdb) stepi
0x00000040 in ?? ()
(gdb)


And this is how you can read the value of one of the parameters of the function:
(gdb) x/1w $ss*16+0x12
0x70012: 0x00b8fa00
(gdb)


We often use this method. However, it is necessary to use it in conjunction with a couple of others (for example, debugging a virtual machine in a virtual machine), which complicates the work a little, but the essence remains the same.

Source: https://habr.com/ru/post/141067/


All Articles