TranslateProject/translated/tech/20210107 A hands-on tutorial for using the GNU Project Debugger.md
Maisie-x e29730625e
Maisie x 提交译文 (#26518)
* Update 20210107 A hands-on tutorial for using the GNU Project Debugger.md

* Update 20210107 A hands-on tutorial for using the GNU Project Debugger.md

* Update 20210107 A hands-on tutorial for using the GNU Project Debugger.md

* Update 20210107 A hands-on tutorial for using the GNU Project Debugger.md

* Update 20210107 A hands-on tutorial for using the GNU Project Debugger.md

* Update 20210107 A hands-on tutorial for using the GNU Project Debugger.md

* Update 20210107 A hands-on tutorial for using the GNU Project Debugger.md

* Update 20210107 A hands-on tutorial for using the GNU Project Debugger.md

* Rename sources/tech/20210107 A hands-on tutorial for using the GNU Project Debugger.md to translated/tech/Update 20210107 A hands-on tutorial for using the GNU Project Debugger.md

* Rename Update 20210107 A hands-on tutorial for using the GNU Project Debugger.md to 20210107 A hands-on tutorial for using the GNU Project Debugger.md
2022-07-20 17:24:55 +08:00

23 KiB
Raw Blame History

手把手教你使用 GNU 调试器

GNU 调试器是一个发现程序缺陷的强大工具。

magnifying glass on computer screen, finding a bug in the code

图片来源Opensource.com

如果您是一个想在您的软件增加某些功能的程序员您首先考虑实现它的方法例如写一个method、定义一个class或者创建新的数据类型。然后您用一种编译器或解释器可以理解的编程语言来实现这个功能。但是如果您觉得您所有代码都正确但是编译器或解释器依然无法理解您的指令怎么办如果软件大多数情况下都运行良好但是在某些环境下出现缺陷怎么办这种情况下您得知道如何正确使用调试器找到问题的根源。

GNU调试器(GDB) 是一个发现项目缺陷的强大工具。它通过追踪程序运行过程中发生了什么来帮助您发现程序错误或崩溃的原因。

本文是GDB使用的基础教程。请跟随示例打开命令行并克隆此仓库

git clone https://github.com/hANSIc99/core_dump_example.git

快捷方式

GDB的每条命令都可以缩短。例如info break ,表示设置断点,可以被缩短为 i break。您可能在其他地方看到过这种缩写,但在本文中,为了清晰展现使用的函数,我将所写出所有命令。

命令行参数

您可以将GDB附加到每个可执行文件。进入您克隆的仓库core_dump_example运行 make进行编译。您现在能看到一个名为coredump的可执行文件。(更多信息,请参考我的文章 创建和调试Linux的dump文件 。)

要将GDB附加到执行文件请输入: gdb coredump

您的输出应如下所示:

gdb coredump output

返回结果显示没有找到调试符号。

调试信息是目标文件(可执行文件)的组成部分,调试信息包括数据类型、函数签名、源代码和操作码之间的关系。此时,您有两种选择:

  • 继续调试程序集(参见下文无符号调试
  • 使用调试信息进行编译,参见下一节内容

使用调试信息进行编译

为了在二进制文件中包含调试信息,您必须重新编译。打开Makefile删除第9行的(#) 标签后结果如下:

CFLAGS =-Wall -Werror -std=c++11 -g

g表示编译器包含调试信息。运行make clean,接着运行 make然后再次调用GDB。您得到如下输出后就可以调试代码了

GDB output with symbols

新增的调试信息会增加可执行文件的大小。在这种情况下执行文件增加了2.5倍从26,088 字节 增加到 65,480 字节)。

输入run -c1,使用-c1开关启动程序。当程序为 State_4 时,程序将启动并崩溃:

gdb output crash on c1 switch

您可以检索有关程序的其他信息, info source命令提供了当前文件的信息:

gdb info source output

  • 101 行
  • 语言: C++
  • 编译器(版本、调优、架构、调试标志、语言标准)
  • 调试格式:DWARF 2
  • 没有预处理器宏指令(使用 GCC 编译时,宏仅在 使用 -g3 标志编译 时可用)。

info shared命令在启动时加载的虚拟地址空间中打印动态库列表及动态库地址,以便程序运行:

gdb info shared output

如果你想了解Linux库处理请参见我的文章 如何在Linux中处理动态库和静态库

调试程序

您可能已经注意到,您可以在 GDB 中使用 run 命令启动程序。 run 命令接受命令行参数,就像从控制台启动程序一样。 -c1 开关会导致程序在第 4 阶段崩溃。要从头开始运行程序,您不用退出 GDB只需再次运行'run'命令。如果没有 -c1 开关,程序将陷入死循环,您必须使用 Ctrl+C 来结束死循环。

gdb output stopped by sigint

您也可以一步一步运行程序。在 C/C++ 中,入口是 main 函数。使用 list main命令打开显示部分 main 函数的源代码:

gdb output list main

main 函数在第 33 行,因此输入break 33 在33行添加断点:

gdb output breakpoint added

输入 run 运行程序。正如预期的那样,程序在 main 函数处停止。输入 layout src 并行查看源代码:

gdb output break at main

您现在处于 GDB 的文本用户界面 (TUI) 模式。使用键盘向上和向下箭头键滚动查看源代码。

GDB 高亮显示当前行。通过输入 next (n),您可以输入 next (n)命令逐行查看命令。如果您一直输入next (n)命令GBD 会一直高亮显示到最后一个命令。要逐行运行代码,只需按 Enter 键。

有时,您会发现文本的输出有点显示不正常:

gdb output corrupted

如果发生这种情况,请按 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_stateState_5 值时停止程序。为此,请在 main 函数处停止程序并为 n_state 设置监察点:

watch n_state == State_5

只有当所需的变量在当前上下文中可用时,使用变量名设置监察点才有效。

当您输入 continue 继续运行程序时,您会得到如下输出:

gdb output stop on watchpoint_1

如果您继续运行程序,当监察点表达式评估为 false 时 GDB 将停止:

gdb output stop on watchpoint_2

您可以自定义监察点的一般值、特定值,读取或写入权限。

更改断点和监察点

输入 info watchpoints 打印先前设置的监察点列表:

gdb output info watchpoints

删除断点和监察点

如您所见,监察点就是数字。要删除特定的监察点,请先输入delete后输入监察点的编号。例如,我的监察点编号为 2要删除此监察点输入 delete 2

注意: 如果您使用 delete 而没有指定数字,所有 监察点和断点将被删除。

这同样适用于断点。在下面的截屏中,我添加了几个断点,输入 info breakpoint 打印断点列表:

gdb output info breakpoints

要删除单个断点,请先输入delete后输入监察点的编号。另外一种方式:您可以通过指定断点的行号来删除断点。例如,clear 78命令将删除第 78 行设置的断点号 7。

禁用或启用断点和监察点

除了删除断点或监察点之外,您可以通过先输入disable,后输入编号禁用断点或监察点。在下文中,断点 3 和 4 被禁用,并在代码窗口中用减号标记:

disabled breakpoints

也可以通过输入类似 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

Set conditional breakpoint

继续运行程序。程序将在第 54 行停止之前运行状态机 3 次。要检查 n_state_3_count 的值,请输入:

print n_state_3_count

print variable

使断点成为条件断点

您也可以使现有断点成为条件断点。用 clear 54 命令删除最近添加的断点,并通过输入 break 54命令添加一个简单的断点。您可以输入以下内容使此断点成为条件断点:

condition 3 n_state_3_count == 9

3 指的是断点编号。

modify breakpoint

在其他源文件中设置断点

如果您的程序由多个源文件组成,您可以在行号前指定文件名来设置断点,例如,break main. cpp:54

捕捉断点

除了断点和监察点之外,您还可以设置捕获点。捕获点适用于执行系统调用、加载共享库或引发异常等事件。

要捕获用于写入 STDOUT 的 write 系统调用,请输入:

catch syscall write

catch syscall write output

每当程序写入控制台输出时GDB将中断执行。

在手册中,您可以找到一整章 断点、监察点和捕捉点 的内容。

评估和操作符号

print命令打印变量的值。一般语法是print <表达式> <值>。修改变量的值,请输入:

set variable <variable-name> <new-value>.

在下面的截屏中,我将变量 n_state_3_count 的值设为 123

catch syscall write output

/x 表达式以十六进制打印值;使用 & 运算符,您可以打印虚拟地址空间内的地址。

如果您不确定某个符号的数据类型,可以使用 whatis 来查明。

whatis output

如果您要列出 main 函数范围内可用的所有变量,请输入info scope main :

info scope main output

DW_OP_fbreg 值是指基于当前子程序的堆栈偏移量。

或者,如果您已经在一个函数中并且想要列出当前堆栈帧上的所有变量,您可以使用 info locals :

info locals output

查看手册以了解更多检查符号的内容。

调试正在运行的进程

gdb attach <process-id>命令允许您通过指定进程IDPID调试已经在运行的进程。幸运的是coredump 程序将其当前 PID 打印到屏幕上,因此您不必使用 pstop 手动查找PID。

启动 coredump 应用程序的一个实例:

./coredump

coredump application

操作系统显示PID为 2849。打开一个单独的控制台窗口,移动到 coredump 应用程序的根目录然后调试GDB

gdb attach 2849

attach GDB to coredump

当你调试 GDB 时GDB会立即停止运行。输入 layout srcbacktrace 来检查调用堆栈:

layout src and backtrace output

输出显示在main.cpp第92行运行 std::this_thread::sleep_for<...>(. ..) 函数时进程中断。

只要您退出 GDB该进程将继续运行。

您可以在 GDB 手册中找到有关 调试正在运行的进程 的更多信息。

在堆栈中移动

在命令窗口,输入up 两次可以在堆栈中向上移动到 main.cpp :

moving up the stack to main.cpp

通常,编译器将为每个函数或方法创建一个子程序。每个子程序都有自己的栈帧,所以在栈帧中向上移动意味着在调用栈中向上移动。

您可以在手册中找到有关 堆栈计算 的更多信息。

指定源文件

当调试一个已经在运行的进程时GDB 将在当前工作目录中寻找源文件。您也可以使用 目录命令 手动指定源目录。

评估dump文件

阅读 创建和调试Linux的dump文件 了解有关此主题的信息。

参考文章太长,没看的看下文:

  1. 假设您使用的是最新版本的 Fedora
  2. 使用 c1 开关调用 coredumpcoredump -c1

Crash meme

  1. 使用 GDB 加载最新的dump文件coredumpctl debug
  2. 打开 TUI 模式并输入 layout src

coredump output

backtrace 的输出显示崩溃发生在距离 main.cpp 五个堆栈帧之外。回车直接跳转到main.cpp中的错误代码行:

up 5 output

看源码发现程序试图释放一个内存管理函数没有返回的指针。这会导致未定义的行为并引起SIGABRT

无符号调试

如果没有可用的资源,事情会变得非常困难。当我在尝试解决逆向工程的挑战时,我第一次体验到了这一点。了解一些 汇编语言 的知识会很有用。

我们用例子看看它是如何运行的。

找到根目录,打开 Makefile,然后像下面一样编辑第 9 行:

CFLAGS =-Wall -Werror -std=c++11 #-g

要重新编译程序,先运行 make clean ,再运行 make ,最后启动 GDB。该程序不再有任何调试符号来引导源代码。

no debugging symbols

info file命令显示二进制文件的内存区域和入口点:

info file output

.text区段始终从入口点开始,其中包含实际的操作码。要在入口点添加断点,输入 break *0x401110 然后输入 run 开始运行程序:

breakpoint at the entry point

要在某个地址设置断点,使用取消引用运算符*指定地址。

选择反汇编程序风格

在深入研究汇编之前,您可以选择要使用的 汇编风格 。 GDB 默认是 AT&T但我更喜欢 Intel 语法。变更风格如下:

set disassembly-flavor intel

changing assembly flavor

现在输入 layout asm 调出汇编代码窗口,输入 layout reg 调出寄存器窗口。您现在应该看到如下输出:

layout asm and layout reg output

保存配置文件

尽管您已经输入了许多命令,但实际上还没有开始调试。如果您正在大量调试应用程序或尝试解决逆向工程的难题,则将 GDB 特定设置保存在文件中会很有用。

该项目的 GitHub 存储库中的 config file gdbinit 包含最近使用的命令:

set disassembly-flavor intel
set write on
break *0x401110
run -c2
layout asm
layout reg

set write on 命令使您能够在程序运行期间修改二进制文件。

退出 GDB 并使用配置文件重新启动 GDB gdb -x gdbinit coredump

阅读指令

应用 c2 开关后,程序将崩溃。程序在入口函数处停止,因此您必须编写 continue 才能继续运行:

continuing execution after crash

idiv 指令进行整数除法运算: RAX 寄存器中为被除数,指定参数为除数。商被加载到 RAX 寄存器中,余数被加载到 RDX 中。

从寄存器角度,您可以看到 RAX 包含 5,因此您必须找出存储堆栈中位置为 RBP-0x4 的值。

读取内存

要读取原始内存内容,您必须指定比读取符号更多的参数。在汇编输出中向上滚动一点,可以看到堆栈的划分:

stack division output

您最感兴趣的应该是 rbp-0x4 的值,因为它是 idiv 的存储参数。您可以从截图中看到rbp-0x8位置的下一个变量,所以rbp-0x4位置的变量是4字节宽。

在 GDB 中,您可以使用 x 命令检查任何内存内容:

x/ < 可选参数 n f u > < 内存地址 addr >

可选参数:

  • n: 单元大小的重复计数默认值1
  • f格式说明符printf
  • u单元大小
    • b字节
    • h2个字节
    • w: 4个字节(默认)
    • g: 8个字节

要打印 rbp-0x4 的值,请输入 x/u $rbp-4 :

print value

如果您能记住这种模式,则可以直接检查内存。检查手册中的 查看内存 部分。

操作程序集

子程序 zeroDivide() 发生运算异常。当你用向上箭头键向上滚动一点时,您会找到下面信息:

0x401211 <_Z10zeroDividev>              push   rbp
0x401212 <_Z10zeroDividev+1>            mov    rbp,rsp

这被称为 函数前言

  1. 调用函数的基指针rbp存放在栈上
  2. 栈指针rsp的值被加载到基指针rbp

完全跳过这个子程序。您可以使用 backtrace 检查调用堆栈。在 main 函数之前只有一个堆栈帧,所以您可以用一次 up 回到 main :

Callstack assembly

在您的 main 函数中,你会找到下面信息:

0x401431 <main+497>     cmp    BYTE PTR [rbp-0x12],0x0
0x401435 <main+501>     je     0x40145f <main+543>
0x401437 <main+503>     call   0x401211<_Z10zeroDividev>

子程序 zeroDivide() 仅在 jump equal (je)true 时输入。您可以轻松地将其替换为 jump-not-equal (jne) 指令该指令的操作码为“0x75”假设您使用的是 x86/64 架构;其他架构上的操作码不同)。输入 run 重新启动程序。当程序在入口函数处停止时,设置操作码:

set *(unsigned char*)0x401435 = 0x75

最后,输入 continue 。该程序将跳过子程序 zeroDivide() 并且不会再崩溃。

总结

您会在许多集成开发环境 (IDE) 中发现 GDB 在后台运行,包括 Qt Creator 和 VSCodium 的扩展 本地调试

GDB in VSCodium

了解如何充分利用 GDB 的功能很有用。一般情况下,并非所有 GDB 的功能都可以在 IDE 中使用,因此您可以从命令行使用 GDB 的经验中受益。


via: https://opensource.com/article/21/1/gnu-project-debugger

作者:Stephan Avenwedde 选题:lkxed 译者:Maisie-x 校对:校对者ID

本文由 LCTT 原创编译,Linux中国 荣誉推出