使用简介
设置目标
gdb支持调试本地运行的进程,也支持通过网络等方式远程调试其他机器上运行的进程。在 Linux机器上调试本地进程时,gdb依赖ptrace这个syscall,它从操作系统层面为gdb控制其 他进程的运行提供了基础支持。一般而言,出于安全性考虑,各个Linux发行版都对ptrace syscall的调用进行了程度不等的权限控制。例如只允许通过ptrace调试子进程等。
启动程序为子进程
tip
gdb <program>
其中, <program>
为需要被调试的程序名,可以是一个完整的可执行文件路径,也可以
只提供程序自身的可执行文件的名称。对于后一种情况,gdb会搜索$PATH环境变量来
找到可执行文件的实际路径
执行该命令后,会进入gdb命令行。此时,gdb并不会立即开始执行被调试的程序。但此 时gdb已经载入了可执行文件中的符号表和调试信息(如有),可以在实际执行程序前
就设置一些断点等,以便调试程序运行早期的代码或不接收输入的程序等等。 确认完成准备工作后,在gdb命令行中执行 run 命令,作为gdb的子进程运行被调试程 序。
Attach 进入子进程
tip
sudo gdb -p <pid>
important
一般而言,在常见Linux发行版上,直接attach到运行中进程可能需要root权限。
远程调试
gdb支持通过网络、串口等调试其他机器上运行的程序。这里我们以网络远程调试为例
说明远程调试的基本原理。如果想使用网络远程调试,需要在实际运行被调试程序的机
器上启动一个gdbserver,或任何实现了gdbserver协议的代理程序(以下统称为
gdbserver)。gdb将会连接到这个gdbserver,并通过网络向gdbserver发送命令,以
及通过gdbserver,读取运行中程序的信息等。在这个架构下,实际控制程序运行的是
gdbserver,而不是gdb,但gdbserver又受到gdb的控制。远程调试的主要优点是提供
了一种通用的将调试和程序的运行解耦的方法,而不是必须将被调试程序作为gdb的子
进程运行。例如,远程调试可以允许程序在其他机器上运行,这对于一些必须在内网中
运行或是依赖特殊硬件等的程序很有用;此外,远程调试基于gdbserver协议,任何程序
只要实现了该协议,都可以“表现为”一个gdbserver,如此,程序可以主动将自己的一些
内部信息暴露给gdb。理论上而言,这可以用于实现Python、Go等由虚拟机运行的程序
的调试(虽然实际上这些语言的调试不是这么实现的),也可以用于调试由qemu运行
的程序(详见下文)。
使用 target remote <ip|domain name>:<port>
,即可将gdb的调试目标设为远程的
gdbserver。
如果通过gdb进行远程调试,在运行gdb时是否还需要指定 <program>
?事实上,答案
并非是完全不需要。如果只想进行纯粹的汇编级调试,的确可以不需要指定
<program>
,但如果想要进行源码级调试,则仍需要指定 <program>
。在这种情况
下, <program>
的主要作用是让gdb读取其中的符号表和调试信息。gdb不会直接从
gdbserver中读取这些信息,因为gdbserver不一定能提供这些信息,gdb只会和
gdbserver交换汇编级的信息,它依赖本地 <program>
文件中的符号表和调试信息将源
码级信息翻译回汇编级信息。此外,还需要注意,在触发一个断点后,如果希望看到当
前所执行到的程序源代码,则还需要本地保存了程序的源代码。这是因为,调试信息
(以ELF所使用的DWARF格式为例)只保存了某条指令所对应的源代码的文件路径与行
数,并没有直接保存源代码,gdb所做的工作是根据调试信息中的路径和行数,读取本
地的代码文件并显示相应源代码。如果本地相应路径没有源代码文件或内容有误,那么
gdb将无法正确显示源代码。
如果运行gdb时没有指定 <program>
,还可以通过 add-symbol-file 命令指定
<program>
。如果程序不是在本地编译的,那么源代码的绝对路径可能与本地保存源代
码的路径不同,可以用 set-substitute-path 命令进行替换。
断点控制
- break
<expr>
: 在<expr>
处创建一个断点。<expr>
是一个最终可以求值为某条指令的地址的表达式。例如,如果想直接在某个地址处设置断点,可以写成 *<address>
,如果被调试程序有符号表,可以直接使用函数名。如果被调试程序有调试信息,可以指定在某个源文件的某一行设置断点,甚至可以使用更复杂的C表达式,更详细的信息可以参考手册。但需要注意,不论使用何种表达式,最终其实都是求值到一条指令的地址,gdb只是利用调试信息并帮助程 序员简化了这一步骤。 - info breakpoints : 列出当前的所有断点
- disable/enable
<NUM>
: 禁用/启用编号为<NUM>
的断点,断点编号可以通过前述 info 命令查看。 - delete
<NUM>
: 删除编号为<NUM>
的断点。
执行控制
- 不输入任何命令,直接按下回车键:重复上一条命令
- 在触发断点后,可以通过下列命令控制如何恢复程序的执行
- continue : 恢复程序执行,直到触发下一个断点
- continue
<NUM>
: 恢复程序执行,且忽略此断点<NUM>
次 - kill : 终止程序执行
- quit : 退出gdb
- stepi : 执行下一条机器指令,随后继续暂停执行(单步调试)
- stepi
<NUM>
: 执行接下来<NUM>
条指令 - step : 执行下一条语句,这属于源代码级调试,需要调试信息
- nexti : 执行下一条指令,且不跟踪(step through)函数调用。与 stepi 不同,如果当前指
- 令是一条 bl 等指令,那么 stepi 会在被调用函数的第一条指令暂停(step in),而 nexti
- 会在 bl 指令之后的那条指令,即被调用函数返回后的那条指令上暂停。
- nexti
<NUM>
, next : 与 stepi , step 类似,只是不跟踪函数调用。 - finish : 恢复执行直到当前被调用的函数返回
显示信息
-
backtrace : 显示栈跟踪信息,可以看到当前函数是如何一步步被调用到的。但需要注意,由 于编译器的优化等因素,如果没有调试信息,栈跟踪信息可能是不准确的,甚至无法提供栈跟 踪信息。
-
print
<mods>
<expr>
: 打印表达式<expr>
执行的值。其中<expr>
可以是寄存器,如 $pc , $sp , $x0 ,也可以是C表达式等。如果没有调试信息,则所使用的C表达式不能涉及到程序中 变量的值,但仍可以使用常量或寄存器中的值,例如,可以直接将某个内存地址作为数字打 印: print *(int *)0x1234 ,或使用寄存器中的值参与运算等: print $x0+$x1 。<mods>
是可选的修饰符,可以用来控制打印的格式,详细信息可以参考手册。表达式求值 时,寄存器、变量、内存中的值都基于触发断点时程序的状态。 -
x/NFU
<address>
: 打印内存中指定长度的内容。与 print 命令不同,该命令只能打印内存 中的内容,且不需要如print一样将内存地址转换为指针并求值。这是因为此命令只是将内存 视为字节数组,打印出其中给定数目的字节的内容,而不对字节内容做任何解释。相反, print 在打印指针求值表达式时,会根据指针的类型信息和<mods>
来计算需要打印多少字 节,且会对多字节的内容进行解释,例如将它们显示为一个完整的整数,而非整数在内存中的 各个字节。此命令的修饰符中, N 表示需要打印的单元数目, U 的取值可以为 b , h , w , g ,分 别表示一个单元大小为1,2,4,8字节。 F 表示每个单元的打印格式,如 i 表示作为指令反 汇编, x 表示十六进制, d 表示十进制等,详细信息可以查看手册。 -
display
<mods>
<expr>
: 在每次程序暂停执行时,自动根据<mods>
打印<expr>
的值,适 用于一些频繁更改的表达式。利用这条命令可以在单步调试时自动查看所需关注的值,而无需 反复输入print命令。 -
info display : 列出所有的自动打印。
-
delete display
<NUM>
: 删除编号为<NUM>
的自动打印
TUI
note
TUI(text UI)是gdb内置的高级命令行界面功能。TUI在gdb内部引入了类似窗口的概念,允许用户在使用gdb命令的同时查看被调试程序的汇编指令、源代码等,也允许用户设置自定义的UI布局,提高工作效率。
- lay asm : 进入TUI默认汇编布局,此布局下会同时开启一个gdb命令窗口以及一个汇编指令 窗口,用户可以直观看到当前正在执行的汇编指令。
- lay src : 进入TUI默认源代码布局,此布局下会同时开启gdb命令窗口和源代码窗口。需要 具备调试信息和源代码文件。
- tui disable : 退出TUI。
扩展阅读
- ptrace syscall
- gdb手册(如想深入了解gdb或参考命令的详细用法,非常建议阅读,此文档只是给出了一个非常简要的介绍):TUI: https://sourceware.org/gdb/onlinedocs/gdb/TUI.html
- TUI