使用简介

设置目标

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。

扩展阅读

Last change: 2024-08-17, commit: 5b84259