跳到主要内容

1. GDB简介

GDB是GNU调试器(GNU Debugger)的简称,是最为广泛使用的调试器之一。

当使用QEMU时,可以在启动时添加-g port来使用指定一个调试端口,这样启动qemu之后,可以在另一个控制台中启动一个gdb,连接qemu的端口进行调试。

在X86环境中使用GDB,需要使用交叉编译工具链中的gdb,如riscv64-unknown-linux-gnu-gdb命令,如果是在riscv板子上,则可以直接使用gdb命令。

2. 实例

第一次使用qemu需安装,命令如下。

root@k1:~# sudo apt install qemu-user

例如,对于以下程序

#include <stdio.h>

int a = 1;
int b = 2;
int main()
{
printf("result:%d\n", a + b);
}

正常编译之后,直接运行和基于QEMU运行的结果:

root@k1:~# gcc ./test.c
root@k1:~# ./a.out
result:3
root@k1:~# qemu-riscv64 ./a.out
result:3

以qemu为例,当需要调试时,先在第一个控制台执行

root@k1:~# qemu-riscv64 -g 1234 ./a.out

此时不会有任何输出,因为qemu正在等待端口1234的调试连接。

在第二个控制台中可以使用gdb进行调试。首先启动gdb:

root@k1:~# gdb
GNU gdb (Bianbu 15.0.50.20240403-0ubuntu1bb2) 15.0.50.20240403-git
Copyright (C) 2024 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://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 "riscv64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word".

在gdb中用本地端口1234连接qemu,连接成功可以看到程序处于最开始的_start()中


(gdb) target remote :1234
Remote debugging using :1234
Reading /root/a.out from remote target...
warning: File transfers from remote targets can be slow. Use "set sysroot" to access files locally instead.
Reading /root/a.out from remote target...
Reading symbols from target:/root/a.out...
Reading /usr/lib/debug/.build-id/f9/2ea78bf585b638f4c402c411673b2afe986bca.debug from remote target...
(No debugging symbols found in target:/root/a.out)
Reading /lib/ld-linux-riscv64-lp64d.so.1 from remote target...
Reading /lib/ld-linux-riscv64-lp64d.so.1 from remote target...
Reading symbols from target:/lib/ld-linux-riscv64-lp64d.so.1...
Reading symbols from /usr/lib/debug/.build-id/43/090b53f2c5c7985b5f86262b141ba5abe33b8a.debug...
BFD: warning: system-supplied DSO at 0x3f850bb000 has a corrupt string table index
0x0000003f8448defc in _start () from target:/lib/ld-linux-riscv64-lp64d.so.1

此时可以使用各种gdb命令,例如打断点,查看汇编,内存,寄存器等。

例如我们希望看看main里面具体的执行过程,可以先把断点打在main函数,再运行,则程序会在进入main之后命中断点停住。

(gdb) b main
Breakpoint 1 at 0x3f850bc684
(gdb) c
Continuing.
Reading /lib/riscv64-linux-gnu/libc.so.6 from remote target...

Breakpoint 1, 0x0000003f850bc684 in main ()

此时可以看看对应的汇编指令,也可以看看寄存器的值。

(gdb) disassemble
Dump of assembler code for function main:
0x0000003f850bc668 <+0>: addi sp,sp,-16
0x0000003f850bc66a <+2>: sd ra,8(sp)
0x0000003f850bc66c <+4>: sd s0,0(sp)
0x0000003f850bc66e <+6>: addi s0,sp,16
0x0000003f850bc670 <+8>: auipc a5,0x2
0x0000003f850bc674 <+12>: addi a5,a5,-1640 # 0x3f850be008 <a>
0x0000003f850bc678 <+16>: lw a4,0(a5)
0x0000003f850bc67a <+18>: auipc a5,0x2
0x0000003f850bc67e <+22>: addi a5,a5,-1646 # 0x3f850be00c <b>
0x0000003f850bc682 <+26>: lw a5,0(a5)
=> 0x0000003f850bc684 <+28>: addw a5,a5,a4
0x0000003f850bc686 <+30>: sext.w a5,a5
0x0000003f850bc688 <+32>: mv a1,a5
0x0000003f850bc68a <+34>: auipc a0,0x0
0x0000003f850bc68e <+38>: addi a0,a0,38 # 0x3f850bc6b0
0x0000003f850bc692 <+42>: jal 0x3f850bc5a0 <printf@plt>
0x0000003f850bc696 <+46>: li a5,0
0x0000003f850bc698 <+48>: mv a0,a5
0x0000003f850bc69a <+50>: ld ra,8(sp)
0x0000003f850bc69c <+52>: ld s0,0(sp)
0x0000003f850bc69e <+54>: addi sp,sp,16
0x0000003f850bc6a0 <+56>: ret
End of assembler dump.
(gdb) p $a4
$1 = 1
(gdb) p $a5
$2 = 2

通过以上的汇编指令,对应源码,我们可以看到当前正要执行的是一条addw加法指令,对应到源码中应该是对应a+b的操作。我们打印下两个源寄存器a4, a5,也确实符合我们的判断,里面保存的是值1和2。

为了实验,我们可以尝试修改一下寄存器的值,例如将a4寄存器的值改掉

(gdb) set $a4=100
(gdb) p $a4
$3 = 100

接下来可以单步运行,观察每一步的变化

(gdb) p $a5
$4 = 2
(gdb) si
(gdb) p $a5
$5 = 102

可以看到由于我们修改了a4寄存器的值,则执行addw加法指令之前,a4的值是100,a5的值是2,单步执行addw加法指令之后,a5寄存器保存的值是102了。 让我们结束调试,让程序全速运行起来

(gdb) c
Continuing.
[Inferior 1 (process 3972) exited normally]

可以看到gdb全速运行之后,程序执行结束,正常退出了。

此时切换回运行qemu的窗口,可以看到最终打印的值是102,说明我们gdb的改动确实作用到了程序的运行结果上了

root@k1:~# qemu-riscv64 -g 1234 ./a.out
result:102

对于不基于QEMU,直接在RISC-V环境中运行的程序,最简单的调试方法是直接用gdb启动该程序,在gdb中使用r命令让程序开始运行,必要的时候r命令后可以跟上需要传递给被调试程序的参数。

root@k1:~# gdb ./a.out
GNU gdb (Bianbu 15.0.50.20240403-0ubuntu1bb2) 15.0.50.20240403-git
Copyright (C) 2024 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://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 "riscv64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./a.out...
(No debugging symbols found in ./a.out)
(gdb) b main
Breakpoint 1 at 0x684
(gdb) r
Starting program: /root/a.out
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/riscv64-linux-gnu/libthread_db.so.1".

Breakpoint 1, 0x0000002aaaaaa684 in main ()
(gdb)

另一种方式是借助gdbserver,第一次使用需安装,命令如下。

root@k1:~# sudo apt install gdbserver

先使用gdbserver启动程序,并指定端口。

root@k1:~# gdbserver localhost:1234 ./a.out
Process ./a.out created; pid = 4191
Listening on port 1234
Remote debugging from host 127.0.0.1, port 41362

此时在另一个控制台则可以跟上文的QEMU例子中一样使用gdb连接调试了。

3. 常用GDB命令

以下为一些常用gdb命令举例:

加载elf文件

加载文件:file <文件>

加载要调试的文件后gdb可以解析出其中的符号,随后即可基于符号调试

操作寄存器

查看通用寄存器: i r

查看所有寄存器: i all-registers

设置a5寄存器的值:set $a5=0x100

断点控制

基于地址设置软件断点,例如设置到0x12345678处:b *0x12345678

基于符号设置软件断点,例如设置到函数foo处:b foo

查看所有断点: i b

查看第n个断点: i b n

删除所有断点:d

删除第n个断点:d n

使能第n个断点:en n

无效第n个断点:dis n

程序执行控制

停止运行:Ctrl+C

继续运行:c

单步执行一行C代码,碰到函数时进入函数:s

单步执行一行C代码,不进入函数:n

单步执行一行机器码,碰到函数时进入函数:si

单步执行一行机器码,不进入函数:ni

查看线程和栈

查看线程:i thread

切换到n号线程:thread n

查看调用栈:bt

切换栈帧:frame

查看程序

列出源程序:list

列出反汇编:disassemble

操作变量

查看变量var或者表达式var: p var

用16进制格式查看变量var或者表达式var: p/x var

操作内存

查看地址0x1000处的16个以十六进制显示的8字节数据:x /16g 0x1000

查看地址0x1000处的16条指令:x /16i 0x1000

设置地址的值:set *0x1000=0x100