如果您是一名程序员,并且想在您的软件中加入特定功能,您首先会考虑实现它的方法——例如编写一个方法、定义一个类或创建新的数据类型。然后,您用编译器或解释器可以理解的语言编写实现代码。但是,如果编译器或解释器不理解您心目中的指令,即使您确信自己做的一切都正确,该怎么办?如果软件在大多数时候运行良好,但在某些情况下会导致错误,该怎么办?在这些情况下,您必须知道如何正确使用调试器来查找问题的根源。
GNU 项目调试器(GDB)是查找程序错误的一个强大工具。它通过跟踪程序执行期间内部发生的情况,帮助您揭示错误或崩溃的原因。
本文是关于 GDB 基本用法的实战教程。要跟随示例进行操作,请打开命令行并克隆此存储库
git clone https://github.com/hANSIc99/core_dump_example.git
快捷方式
GDB 中的每个命令都可以缩短。例如,info break
(显示设置的断点)可以缩短为 i break
。您可能会在其他地方看到这些缩写,但在本文中,我将写出完整的命令,以便清楚地了解使用了哪个功能。
命令行参数
您可以将 GDB 附加到每个可执行文件。导航到您克隆的存储库,并通过运行 make
进行编译。您现在应该有一个名为 coredump 的可执行文件。(有关更多信息,请参阅我关于 创建和调试 Linux 转储文件 的文章。)
要将 GDB 附加到可执行文件,请输入:gdb coredump
。
您的输出应如下所示

(Stephan Avenwedde, CC BY-SA 4.0)
它显示未找到调试符号。
调试信息是目标文件(可执行文件)的一部分,包括数据类型、函数签名以及源代码和操作码之间的关系。此时,您有两个选择
- 继续调试汇编代码(请参阅下面的“不使用符号进行调试”)
- 使用下一节中的信息编译调试信息
编译调试信息
要在二进制文件中包含调试信息,您必须重新编译它。打开 Makefile 并删除第 9 行的井号 (#
)
CFLAGS =-Wall -Werror -std=c++11 -g
g
选项告诉编译器包含调试信息。运行 make clean
,然后运行 make
并再次调用 GDB。您应该获得此输出并可以开始调试代码

(Stephan Avenwedde, CC BY-SA 4.0)
额外的调试信息将增加可执行文件的大小。在本例中,它使可执行文件的大小增加了 2.5 倍(从 26,088 字节增加到 65,480 字节)。
通过键入 run -c1
,使用 -c1
开关启动程序。程序将启动并在到达 State_4
时崩溃

(Stephan Avenwedde, CC BY-SA 4.0)
您可以检索有关程序的其他信息。命令 info source
提供有关当前文件的信息

(Stephan Avenwedde, CC BY-SA 4.0)
- 101 行
- 语言:C++
- 编译器(版本、调整、架构、调试标志、语言标准)
- 调试格式:DWARF 2
- 没有可用的预处理器宏信息(当使用 GCC 编译时,只有在 使用
-g3
标志编译 时宏才可用)。
命令 info shared
打印动态库列表,其中包含程序启动时加载的虚拟地址空间中的地址,以便程序可以执行

(Stephan Avenwedde, CC BY-SA 4.0)
如果您想了解 Linux 中的库处理,请参阅我的文章 如何在 Linux 中处理动态库和静态库。
调试程序
您可能已经注意到,您可以使用 run
命令在 GDB 中启动程序。run
命令接受命令行参数,就像您从控制台启动程序一样。-c1
开关将导致程序在 stage 4 崩溃。要从头开始运行程序,您不必退出 GDB;只需再次使用 run
命令。如果不使用 -c1
开关,程序将执行无限循环。您必须使用 Ctrl+C 停止它。

(Stephan Avenwedde, CC BY-SA 4.0)
您也可以逐步执行程序。在 C/C++ 中,入口点是 main
函数。使用命令 list main
打开显示 main
函数的源代码部分

(Stephan Avenwedde, CC BY-SA 4.0)
main
函数在第 33 行,因此通过键入 break 33
在此处添加断点

(Stephan Avenwedde, CC BY-SA 4.0)
通过键入 run
运行程序。正如预期的那样,程序在 main
函数处停止。键入 layout src
以并行显示源代码

(Stephan Avenwedde, CC BY-SA 4.0)
您现在处于 GDB 的文本用户界面 (TUI) 模式。使用向上和向下箭头键滚动浏览源代码。
GDB 突出显示要执行的行。通过键入 next
(n),您可以逐行执行命令。如果您未指定新命令,GBD 将执行上一个命令。要单步执行代码,只需按 Enter 键即可。
有时,您会注意到 TUI 的输出会有些损坏

(Stephan Avenwedde, CC BY-SA 4.0)
如果发生这种情况,请按 Ctrl+L 以重置屏幕。
使用 Ctrl+X+A 可以随意进入和退出 TUI 模式。您可以在手册中找到 其他键绑定。
要退出 GDB,只需键入 quit
。
观察点
此示例程序的核心部分是由状态机在无限循环中运行组成。变量 n_state
是一个简单的枚举,用于确定当前状态
while(true){
switch(n_state){
case State_1:
std::cout << "State_1 reached" << std::flush;
n_state = State_2;
break;
case State_2:
std::cout << "State_2 reached" << std::flush;
n_state = State_3;
break;
(.....)
}
}
您希望在 n_state
设置为值 State_5
时停止程序。为此,请在 main
函数处停止程序,并为 n_state
设置观察点
watch n_state == State_5
仅当所需变量在当前上下文中可用时,使用变量名设置观察点才有效。
当您通过键入 continue
继续程序执行时,您应该获得如下输出

(Stephan Avenwedde, CC BY-SA 4.0)
如果您继续执行,当观察点表达式求值为 false
时,GDB 将停止

(Stephan Avenwedde, CC BY-SA 4.0)
您可以为一般值更改、特定值以及读取或写入访问指定观察点。
更改断点和观察点
键入 info watchpoints
以打印先前设置的观察点列表

(Stephan Avenwedde, CC BY-SA 4.0)
删除断点和观察点
如您所见,观察点是编号的。要删除特定的观察点,请键入 delete
,后跟观察点的编号。例如,我的观察点的编号为 2;要删除此观察点,请输入 delete 2
。
注意: 如果您使用 delete
而不指定编号,则所有观察点和断点都将被删除。
断点也适用相同的情况。在下面的屏幕截图中,我添加了几个断点,并通过键入 info breakpoint
打印了它们的列表

(Stephan Avenwedde, CC BY-SA 4.0)
要删除单个断点,请键入 delete
,后跟其编号。或者,您可以通过指定其行号来删除断点。例如,命令 clear 78
将删除在第 78 行设置的断点编号 7。
禁用或启用断点和观察点
您可以禁用断点或观察点,而不是删除它,方法是键入 disable
,后跟其编号。在下面,断点 3 和 4 被禁用,并在代码窗口中标记为减号

(Stephan Avenwedde, CC BY-SA 4.0)
也可以通过键入类似 disable 2 - 4
的内容来修改一系列断点或观察点。如果要重新激活这些点,请键入 enable
,后跟它们的编号。
条件断点
首先,通过键入 delete
删除所有断点和观察点。您仍然希望程序在 main
函数处停止,但是,不是指定行号,而是通过直接命名函数来添加断点。键入 break main
以在 main
函数处添加断点。
键入 run
以从头开始执行,程序将在 main
函数处停止。
main
函数包含变量 n_state_3_count
,当状态机命中状态 3 时,该变量会递增。
要根据 n_state_3_count
的值添加条件断点,请键入
break 54 if n_state_3_count == 3

(Stephan Avenwedde, CC BY-SA 4.0)
继续执行。程序将在状态机执行三次后在第 54 行停止。要检查 n_state_3_count
的值,请键入
print n_state_3_count

(Stephan Avenwedde, CC BY-SA 4.0)
使断点成为条件断点
也可以使现有断点成为条件断点。使用 clear 54
删除最近添加的断点,并通过键入 break 54
添加一个简单的断点。您可以通过键入以下内容使此断点成为条件断点
condition 3 n_state_3_count == 9
3
指的是断点编号。

(Stephan Avenwedde, CC BY-SA 4.0)
在其他源文件中设置断点
如果您的程序由多个源文件组成,则可以通过在行号之前指定文件名来设置断点,例如 break main.cpp:54
。
捕捉点
除了断点和观察点之外,您还可以设置捕捉点。捕捉点适用于程序事件,例如执行系统调用、加载共享库或引发异常。
要捕捉用于写入 STDOUT 的 write
系统调用,请输入
catch syscall write

(Stephan Avenwedde, CC BY-SA 4.0)
每次程序写入控制台输出时,GDB 都会中断执行。
在手册中,您可以找到涵盖 断点、观察点和捕捉点 的完整章节。
评估和操作符号
变量值的打印使用 print
命令完成。一般语法为 print <expression> <value>
。可以通过键入以下内容修改变量的值
set variable <variable-name> <new-value>.
在下面的屏幕截图中,我给变量 n_state_3_count
赋值为 123。

(Stephan Avenwedde, CC BY-SA 4.0)
/x
表达式以十六进制打印值;使用 &
运算符,您可以打印虚拟地址空间内的地址。
如果您不确定某个符号的数据类型,可以使用 whatis
查找

(Stephan Avenwedde, CC BY-SA 4.0)
如果您想列出 main
函数作用域中可用的所有变量,请键入 info scope main

(Stephan Avenwedde, CC BY-SA 4.0)
DW_OP_fbreg
值指的是基于当前子例程的堆栈偏移量。
或者,如果您已在函数内部并想列出当前堆栈帧上的所有变量,则可以使用 info locals

(Stephan Avenwedde, CC BY-SA 4.0)
查看手册以了解有关 检查符号 的更多信息。
附加到正在运行的进程
命令 gdb attach <process-id>
允许您通过指定进程 ID (PID) 附加到已在运行的进程。幸运的是,coredump
程序将其当前 PID 打印到屏幕上,因此您不必手动使用 ps 或 top 查找它。
启动 coredump 应用程序的一个实例
./coredump

(Stephan Avenwedde, CC BY-SA 4.0)
操作系统给出 PID 2849
。打开一个单独的控制台窗口,移动到 coredump 应用程序的源目录,并附加 GDB
gdb attach 2849

(Stephan Avenwedde, CC BY-SA 4.0)
当您附加 GDB 时,GDB 会立即停止执行。键入 layout src
和 backtrace
以检查调用堆栈

(Stephan Avenwedde, CC BY-SA 4.0)
输出显示进程在执行 std::this_thread::sleep_for<...>(...)
函数时中断,该函数在 main.cpp
的第 92 行被调用。
一旦您退出 GDB,该进程将继续运行。
您可以在 GDB 手册中找到有关 附加到正在运行的进程 的更多信息。
在堆栈中移动
通过使用 up
两次在堆栈中向上移动到 main.cpp
,返回到程序

(Stephan Avenwedde, CC BY-SA 4.0)
通常,编译器会为每个函数或方法创建一个子例程。每个子例程都有自己的堆栈帧,因此在堆栈帧中向上移动意味着在调用堆栈中向上移动。
您可以在手册中找到更多关于堆栈求值的信息。
指定源文件
当附加到已运行的进程时,GDB 将在当前工作目录中查找源文件。或者,您可以使用 directory
命令手动指定源目录。
评估转储文件
阅读创建和调试 Linux 转储文件以获取有关此主题的信息。
TL;DR(太长不看)
- 我假设您正在使用最新版本的 Fedora
- 使用
-c1
开关调用 coredump:coredump -c1
(Stephan Avenwedde, CC BY-SA 4.0)
- 使用 GDB 加载最新的转储文件:
coredumpctl debug
- 打开 TUI 模式并输入
layout src

(Stephan Avenwedde, CC BY-SA 4.0)
backtrace
的输出显示崩溃发生在距离 main.cpp
五个堆栈帧的位置。输入以直接跳转到 main.cpp
中错误的代码行

(Stephan Avenwedde, CC BY-SA 4.0)
查看源代码表明程序试图释放一个并非由内存管理函数返回的指针。这会导致未定义的行为并导致 SIGABRT
。
不使用符号进行调试
如果没有可用的源文件,事情会变得非常困难。当尝试解决逆向工程挑战时,我第一次体验到这一点。 了解一些汇编语言的知识也很有用。
查看此示例的工作原理。
转到源目录,打开 Makefile,并像这样编辑第 9 行
CFLAGS =-Wall -Werror -std=c++11 #-g
要重新编译程序,运行 make clean
,然后运行 make
并启动 GDB。该程序不再具有任何调试符号来引导您查看源代码。

(Stephan Avenwedde, CC BY-SA 4.0)
info file
命令显示二进制文件的内存区域和入口点

(Stephan Avenwedde, CC BY-SA 4.0)
入口点与 .text
区域的开头相对应,其中包含实际的操作码。要在入口点添加断点,输入 break *0x401110
,然后通过输入 run
开始执行

(Stephan Avenwedde, CC BY-SA 4.0)
要在特定地址设置断点,请使用解引用运算符 *
指定它。
选择反汇编器风格
在深入研究汇编之前,您可以选择要使用的汇编风格。GDB 的默认风格是 AT&T,但我更喜欢 Intel 语法。使用以下命令更改它:
set disassembly-flavor intel

(Stephan Avenwedde, CC BY-SA 4.0)
现在打开汇编窗口和寄存器窗口,输入 layout asm
和 layout reg
。您现在应该看到类似这样的输出

(Stephan Avenwedde, CC BY-SA 4.0)
保存配置文件
虽然您已经输入了很多命令,但您实际上还没有开始调试。如果您正在大量调试应用程序或尝试解决逆向工程挑战,将您的 GDB 特定设置保存在文件中可能会很有用。
此项目 GitHub 存储库中的配置文件 gdbinit
包含最近使用的命令
set disassembly-flavor intel
set write on
break *0x401110
run -c2
layout asm
layout reg
set write on
命令使您能够在执行期间修改二进制文件。
退出 GDB 并使用配置文件重新打开它:gdb -x gdbinit coredump
。
读取指令
应用 c2
开关后,程序将崩溃。程序在入口函数处停止,因此您必须写入 continue
以继续执行

(Stephan Avenwedde, CC BY-SA 4.0)
idiv
指令执行整数除法,被除数在 RAX
寄存器中,除数作为参数指定。商加载到 RAX
寄存器中,余数加载到 RDX
中。
从寄存器概览中,您可以看到 RAX
包含 5,因此您必须找出存储在堆栈位置 RBP-0x4
的值。
读取内存
要读取原始内存内容,您必须指定比读取符号更多的参数。当您在汇编输出中向上滚动一点时,您可以看到堆栈的划分

(Stephan Avenwedde, CC BY-SA 4.0)
您最感兴趣的是 rbp-0x4
的值,因为这是存储 idiv
参数的位置。从屏幕截图中,您可以看到下一个变量位于 rbp-0x8
,因此 rbp-0x4
处的变量是 4 字节宽。
在 GDB 中,您可以使用 x
命令来检查任何内存内容
x/
< 可选参数n
f
u
> < 内存地址addr
>
可选参数
n
:重复计数(默认值:1)指的是单位大小f
:格式说明符,类似于 printf 中的格式说明符u
:单位大小b
:字节h
:半字(2 字节)w
:字(4 字节)(默认值)g
:巨字(8 字节)
要打印出 rbp-0x4
处的值,输入 x/u $rbp-4

(Stephan Avenwedde, CC BY-SA 4.0)
如果您牢记这种模式,则检查内存就很简单了。查看手册中检查内存部分。
操纵汇编代码
算术异常发生在子例程 zeroDivide()
中。当您使用向上箭头键向上滚动一点时,您可以找到此模式
0x401211 <_Z10zeroDividev> push rbp
0x401212 <_Z10zeroDividev+1> mov rbp,rsp
这称为函数序言
- 调用函数的基指针 (
rbp
) 存储在堆栈上 - 堆栈指针 (
rsp
) 的值加载到基指针 (rbp
)
完全跳过此子例程。您可以使用 backtrace
检查调用堆栈。您只比 main
函数提前一个堆栈帧,因此您可以使用单个 up
返回到 main

(Stephan Avenwedde, CC BY-SA 4.0)
在您的 main
函数中,您可以找到此模式
0x401431 <main+497> cmp BYTE PTR [rbp-0x12],0x0
0x401435 <main+501> je 0x40145f <main+543>
0x401437 <main+503> call 0x401211<_Z10zeroDividev>
仅当 jump equal (je)
的计算结果为 true
时,才会进入子例程 zeroDivide()
。您可以轻松地将其替换为 jump-not-equal (jne)
指令,该指令的操作码为 0x75
(前提是您在 x86/64 架构上;在其他架构上,操作码是不同的)。通过输入 run
重新启动程序。当程序在入口函数处停止时,通过输入以下内容来操纵操作码:
set *(unsigned char*)0x401435 = 0x75
最后,输入 continue
。程序将跳过子例程 zeroDivide()
,并且不会再崩溃。
结论
您可以在许多集成开发环境 (IDE) 中找到在后台运行的 GDB,包括 Qt Creator 和 VSCodium 的 Native Debug 扩展。

(Stephan Avenwedde, CC BY-SA 4.0)
了解如何利用 GDB 的功能很有用。通常,并非所有 GDB 的功能都可以从 IDE 中使用,因此从命令行使用 GDB 的经验对您有利。
评论已关闭。