GDB 调试器如何通过调用帧信息来确定函数调用关系
从调试器中获取函数调用关系。在我的 上一篇文章 中，我展示了如何使用 debuginfo 在当前指令指针（IP）和包含它的函数或行之间进行映射。该信息对于显示 CPU 当前正在执行的代码很有帮助。不过，如果能显示更多的有关当前函数调用栈及其正在执行语句的上下文对我们定位问题来说也是十分有助的。例如，将空指针作为参数传递到函数中而导致非法内存访问的问题，只需查看当前执行函数行，即可发现该错误是由尝试通过空指针进行访问而触发的。但是，你真正想知道的是导致空指针访问的函数调用的完整上下文，以便确定该空指针最初是如何传递到该函数中的。此上下文信息由回溯提供，可以让你确定哪些函数可能对空指针参数负责。有一点是肯定的：确定当前活动的函数调用栈不是一项简单的操作。函数激活记录现代编程语言具有局部变量，并允许函数可以调用自身的递归。此外，并发程序具有多个线程，这些线程可能同时运行相同的函数。在这些情况下，局部变量不能存储在全局位置。对于函数的每次调用，局部变量的位置必须是唯一的。它的工作原理如下：每次调用函数时，编译器都会生成函数激活记录，以将局部变量存储在唯一位置。为了提高效率，处理器堆栈用于存储函数激活记录。当函数被调用时，会在处理器堆栈的顶部为该函数创建一条新的函数激活记录。如果该函数调用另一个函数，则新的函数激活记录将放置在现有函数激活记录之上。每次函数返回时，其函数激活记录都会从堆栈中删除。函数激活记录的创建是由函数中称为“ 序言 prologue ”的代码创建的。函数激活记录的删除由函数“ 尾声 epilogue ”处理。函数体可以利用堆栈上为其预留的内存来存储临时值和局部变量。函数激活记录的大小可以是可变的。对于某些函数，不需要空间来存储局部变量。理想情况下，函数激活记录只需要存储调用 该 函数的函数的返回地址。对于其他函数，除了返回地址之外，可能还需要大量空间来存储函数的本地数据结构。帧大小的可变导致编译器使用帧指针来跟踪函数激活帧的开始。函数序言代码具有在为当前函数创建新帧指针之前存储旧帧指针的额外任务，并且函数尾声必须恢复旧帧指针值。函数激活记录的布局方式、调用函数的返回地址和旧帧指针是相对于当前帧指针的恒定偏移量。通过旧的帧指针，可以定位堆栈上下一个函数的激活帧。重复此过程，直到检查完所有函数激活记录为止。优化复杂性在代码中使用显式帧指针有几个缺点。在某些处理器上，可用的寄存器相对较少。具有显式帧指针会导致使用更多内存操作。生成的代码速度较慢，因为帧指针必须位于寄存器中。具有显式帧指针可能会限制编译器可以生成的代码，因为编译器可能不会将函数序言和尾声代码与函数体混合。编译器的目标是尽可能生成快速代码，因此编译器通常会从生成的代码中省略帧指针。正如 Phoronix 的基准测试 所示，保留帧指针会显着降低性能。不过省略帧指针也有缺点，查找前一个调用函数的激活帧和返回地址不再是相对于帧指针的简单偏移。调用帧信息为了帮助生成函数回溯，编译器包含 DWARF 调用帧信息（CFI）来重建帧指针并查找返回地址。此补充信息存储在执行的 .eh_frame 部分中。与传统的函数和行位置信息的 debuginfo 不同，即使生成的可执行文件没有调试信息，或者调试信息已从文件中删除，.eh_frame 部分也位于可执行文件中。 调用帧信息对于 C++ 中的 throw-catch 等语言结构的操作至关重要。CFI 的每个功能都有一个帧描述条目（FDE）。作为其步骤之一，回溯生成过程为当前正在检查的激活帧找到适当的 FDE。将 FDE 视为一张表，每一行代表一个或多个指令，并具有以下列：规范帧地址（CFA），帧指针指向的位置返回地址有关其他寄存器的信息FDE 的编码旨在最大限度地减少所需的空间量。FDE 描述了行之间的变化，而不是完全指定每一行。为了进一步压缩数据，多个 FDE 共有的起始信息被分解出来并放置在通用信息条目（CIE）中。 这使得 FDE 更加紧凑，但也需要更多的工作来计算实际的 CFA 并找到返回地址位置。该工具必须从未初始化状态启动。它逐步遍历 CIE 中的条目以获取函数条目的初始状态，然后从 FDE 的第一个条目开始继续处理 FDE，并处理操作，直到到达覆盖当前正在分析的指令指针的行。调用帧信息使用实例从一个简单的示例开始，其中包含将华氏温度转换为摄氏度的函数。 内联函数在 CFI 中没有条目，因此 f2c 函数的 __attribute__((noinline)) 确保编译器将 f2c 保留为真实函数。#include <stdio.h> int __attribute__ ((noinline)) f2c(int f) { int c; printf("converting\n"); c = (f-32.0) * 5.0 /9.0; return c; } int main (int argc, char *argv[]) { int f; scanf("%d", &f); printf ("%d Fahrenheit = %d Celsius\n", f, f2c(f)); return 0; } 编译代码：$ gcc -O2 -g -o f2c f2c.c .eh_frame 部分展示如下:$ eu-readelf -S f2c |grep eh_frame [17] .eh_frame_hdr PROGBITS 0000000000402058 00002058 00000034 0 A 0 0 4 [18] .eh_frame PROGBITS 0000000000402090 00002090 000000a0 0 A 0 0 8 我们可以通过以下方式获取 CFI 信息：$ readelf --debug-dump=frames f2c > f2c.cfi 生成 f2c 可执行文件的反汇编代码，这样你可以查找 f2c 和 main 函数：$ objdump -d f2c > f2c.dis 在 f2c.dis 中找到以下信息来看看 f2c 和 main 函数的执行位置：0000000000401060 : 0000000000401190 <f2c>: 在许多情况下，二进制文件中的所有函数在执行函数的第一条指令之前都使用相同的 CIE 来定义初始条件。 在此示例中， f2c 和 main 都使用以下 CIE：00000000 0000000000000014 00000000 CIE Version: 1 Augmentation: "zR" Code alignment factor: 1 Data alignment factor: -8 Return address column: 16 Augmentation data: 1b DW_CFA_def_cfa: r7 (rsp) ofs 8 DW_CFA_offset: r16 (rip) at cfa-8 DW_CFA_nop DW_CFA_nop 本示例中，不必担心增强或增强数据条目。由于 x86\_64 处理器具有 1 到 15 字节大小的可变长度指令，因此 “代码对齐因子” 设置为 1。在只有 32 位（4 字节指令）的处理器上，“代码对齐因子” 设置为 4，并且允许对一行状态信息适用的字节数进行更紧凑的编码。类似地，还有 “数据对齐因子” 来使 CFA 所在位置的调整更加紧凑。在 x86\_64 上，堆栈槽的大小为 8 个字节。虚拟表中保存返回地址的列是 16。这在 CIE 尾部的指令中使用。 有四个 DW_CFA 指令。第一条指令 DW_CFA_def_cfa 描述了如果代码具有帧指针，如何计算帧指针将指向的规范帧地址（CFA）。 在这种情况下，CFA 是根据 r7 (rsp) 和 CFA=rsp+8 计算的。第二条指令 DW_CFA_offset 定义从哪里获取返回地址 CFA-8 。在这种情况下，返回地址当前由堆栈指针 (rsp+8)-8 指向。CFA 从堆栈返回地址的正上方开始。CIE 末尾的 DW_CFA_nop 进行填充以保持 DWARF 信息的对齐。 FDE 还可以在末尾添加填充以进行对齐。在 f2c.cfi 中找到 main 的 FDE，它涵盖了从 0x40160 到（但不包括）0x401097 的 main 函数：00000084 0000000000000014 00000088 FDE cie=00000000 pc=0000000000401060..0000000000401097 DW_CFA_advance_loc: 4 to 0000000000401064 DW_CFA_def_cfa_offset: 32 DW_CFA_advance_loc: 50 to 0000000000401096 DW_CFA_def_cfa_offset: 8 DW_CFA_nop 在执行函数中的第一条指令之前，CIE 描述调用帧状态。然而，当处理器执行函数中的指令时，细节将会改变。 首先，指令 DW_CFA_advance_loc 和 DW_CFA_def_cfa_offset 与 main 中 401060 处的第一条指令匹配。 这会将堆栈指针向下调整 0x18（24 个字节）。 CFA 没有改变位置，但堆栈指针改变了，因此 CFA 在 401064 处的正确计算是 rsp+32。 这就是这段代码中序言指令的范围。 以下是 main 中的前几条指令：0000000000401060 : 401060: 48 83 ec 18 sub $0x18,%rsp 401064: bf 1b 20 40 00 mov $0x40201b,%edi DW_CFA_advance_loc 使当前行应用于函数中接下来的 50 个字节的代码，直到 401096。CFA 位于 rsp+32，直到 401092 处的堆栈调整指令完成执行。DW_CFA_def_cfa_offset 将 CFA 的计算更新为与函数入口相同。这是预期之中的，因为 401096 处的下一条指令是返回指令 ret，并将返回值从堆栈中弹出。 401090: 31 c0 xor %eax,%eax 401092: 48 83 c4 18 add $0x18,%rsp 401096: c3 ret f2c 函数的 FDE 使用与 main 函数相同的 CIE，并覆盖 0x41190 到 0x4011c3 的范围：00000068 0000000000000018 0000006c FDE cie=00000000 pc=0000000000401190..00000000004011c3 DW_CFA_advance_loc: 1 to 0000000000401191 DW_CFA_def_cfa_offset: 16 DW_CFA_offset: r3 (rbx) at cfa-16 DW_CFA_advance_loc: 29 to 00000000004011ae DW_CFA_def_cfa_offset: 8 DW_CFA_nop DW_CFA_nop DW_CFA_nop 可执行文件中 f2c 函数的 objdump 输出：0000000000401190 <f2c>: 401190: 53 push %rbx 401191: 89 fb mov %edi,%ebx 401193: bf 10 20 40 00 mov $0x402010,%edi 401198: e8 93 fe ff ff call 401030 <puts@plt> 40119d: 66 0f ef c0 pxor %xmm0,%xmm0 4011a1: f2 0f 2a c3 cvtsi2sd %ebx,%xmm0 4011a5: f2 0f 5c 05 93 0e 00 subsd 0xe93(%rip),%xmm0 # 402040 <__dso_handle+0x38> 4011ac: 00 4011ad: 5b pop %rbx 4011ae: f2 0f 59 05 92 0e 00 mulsd 0xe92(%rip),%xmm0 # 402048 <__dso_handle+0x40> 4011b5: 00 4011b6: f2 0f 5e 05 92 0e 00 divsd 0xe92(%rip),%xmm0 # 402050 <__dso_handle+0x48> 4011bd: 00 4011be: f2 0f 2c c0 cvttsd2si %xmm0,%eax 4011c2: c3 ret 在 f2c 的 FDE 中，函数开头有一个带有 DW_CFA_advance_loc 的单字节指令。在高级操作之后，还有两个附加操作。DW_CFA_def_cfa_offset 将 CFA 更改为 %rsp+16，DW_CFA_offset 表示 %rbx 中的初始值现在位于 CFA-16（堆栈顶部）。查看这个 fc2 反汇编代码，可以看到 push 用于将 %rbx 保存到堆栈中。 在代码生成中省略帧指针的优点之一是可以使用 push 和 pop 等紧凑指令在堆栈中存储和检索值。 在这种情况下，保存 %rbx 是因为 %rbx 用于向 printf 函数传递参数（实际上转换为 puts 调用），但需要保存传递到函数中的 f 初始值以供后面的计算使用。4011ae 的 DW_CFA_advance_loc 29字节显示了 pop %rbx 之后的下一个状态变化，它恢复了 %rbx 的原始值。 DW_CFA_def_cfa_offset 指出 pop 将 CFA 更改为 %rsp+8。GDB 使用调用帧信息有了 CFI 信息，GNU 调试器（GDB） 和其他工具就可以生成准确的回溯。如果没有 CFI 信息，GDB 将很难找到返回地址。如果在 f2c.c 的第 7 行设置断点，可以看到 GDB 使用此信息。GDB在 f2c 函数中的 pop %rbx 完成且返回值不在栈顶之前放置了断点。GDB 能够展开堆栈，并且作为额外收获还能够获取当前保存在堆栈上的参数 f：$ gdb f2c [...] (gdb) break f2c.c:7 Breakpoint 1 at 0x40119d: file f2c.c, line 7. (gdb) run Starting program: /home/wcohen/present/202207youarehere/f2c [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib64/libthread_db.so.1". 98 converting Breakpoint 1, f2c (f=98) at f2c.c:8 8 return c; (gdb) where #0 f2c (f=98) at f2c.c:8 #1 0x000000000040107e in main (argc=<optimized out>, argv=<optimized out>) at f2c.c:15 调用帧信息DWARF 调用帧信息为编译器提供了一种灵活的方式来包含用于准确展开堆栈的信息。这使得可以确定当前活动的函数调用。我在本文中提供了简要介绍，但有关 DWARF 如何实现此机制的更多详细信息，请参阅 DWARF 规范。（题图：MJ/4004d7c7-8407-40bd-8aa8-92404601dba0）via: https://opensource.com/article/23/3/gdb-debugger-call-frame-active-function-calls作者：Will Cohen 选题：lkxed 译者：jrglinux 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
Rustlings 是由 Rust 团队维护的开源项目，旨在帮助你通过调试代码的方式来学习 Rust。在我上一篇 关于 Rustup 的文章 中，我向你们展示了如何安装 Rust 工具链。但是，如果不能上手操作一下 Rust 的话下载工具链又有什么用？学习任何语言都包括阅读现有的代码和写很多的示例程序，这是精通一门语言的好方法。然而，我们还可以走第三条路：调试代码。通过调试来学习牵扯到尝试去编译一个已经写好的（满是漏洞的）示例程序，理解编译器生成的错误信息，修复示例代码，然后再重新编译。重复这个过程直到代码能够成功被编译并运行。Rustlings 是一个由 Rust 团队维护的开源项目，旨在帮助你通过调试代码来学习 Rust。它也会一路为你提供提示。如果你是一名 Rust 初学者，并且刚开始阅读或已经读完了 Rust 书籍，那么 Rustlings 就是理想的下一步。Rustllings 帮助你将运用书中所学，并转向开发更大的项目。安装 Rustlings我使用（并推荐）Fedora 电脑来体验 Rustlings，但是任何 Linux 发行版都可以。要安装 Rustlings，你必须下载并运行它的安装脚本。通常建议你以不具备任何特别权限的普通用户（非 root 用户）来运行脚本。记住，你需要 Rust 工具链来使用 Rustlings。如果你还没有这些工具链，请参考我 关于 Rustup 的文章。当你准备好时，下载这个安装脚本：$ curl -L https://raw.githubusercontent.com/rust-lang/rustlings/main/install.sh > rustlings_install.sh $ file rustlings_install.sh rustlings_install.sh: Bourne-Again shell script, ASCII text executable 阅读脚本以了解它会做什么：$ less rustlings_install.sh 然后运行安装：$ bash rustlings_install.sh [...] Installing /home/tux/.cargo/bin/rustlings Installed package `rustlings v4.8.0 (/home/tux/rustlings)` (executable `rustlings`) All done! 运行 rustlings 以开始。Rustlings 练习你现在可以使用命令 rustlings。与标志 --help 一起执行来查看可选的选项。$ rustlings --help 这个安装脚本也克隆了 Rustlings 的 Git 仓库，并安装了运行示例程序所需的依赖。你可以在 ruslings 下的 exercises 目录查阅这些示例程序。$ cd rustlings $ pwd /home/tux/rustlings $ ls AUTHORS.md Cargo.toml CONTRIBUTING.md info.toml install.sh README.md target Cargo.lock CHANGELOG.md exercises install.ps1 LICENSE src tests $ ls -m exercises/ advanced_errors, clippy, collections, conversions, enums, error_handling, functions, generics, if, intro, macros, mod.rs, modules, move_semantics, option, primitive_types, quiz1.rs, quiz2.rs, quiz3.rs, quiz4.rs, README.md, standard_library_types, strings, structs, tests, threads, traits, variables 从命令行列出所有练习命令 ruslings 提供给你一个 list 命令用以展示每个示例程序，它的完整路径，以及状态 （默认为 “待定”）。$ rustlings list Name Path Status intro1 exercises/intro/intro1.rs Pending intro2 exercises/intro/intro2.rs Pending variables1 exercises/variables/variables1.rs Pending variables2 exercises/variables/variables2.rs Pending variables3 exercises/variables/variables3.rs Pending [...] 在显示结尾处，你会有一个进度报告用来追踪进度。Progress: You completed 0 / 84 exercises (0.00 %). 查看示例程序命令 rustlings list 向你展示了现有的程序，所以你可以在任何时候查看这些程序的代码，你只需要将完整路径复制到你的终端作为命令 cat 或者 less 的参数：$ cat exercises/intro/intro1.rs 验证你的程序现在你可以开始调试程序了。你可以使用命令 verify 来做这件事。注意 Rustlings 选择了列表里的第一个程序（intro1.rs）并尝试去编译它，最后编译成功：$ rustlings verify Progress: [-----------------------------------] 0/84 ✅ Successfully ran exercises/intro/intro1.rs! You can keep working on this exercise, or jump into the next one by removing the `I AM NOT DONE` comment: 6 | // Execute the command `rustlings hint intro1` for a hint. 7 | 8 | // I AM NOT DONE 9 | 正如你从结果中所见，尽管示例代码成功编译了，你依然需要做一些工作。每个示例程序的源文件中都带有以下注释：$ grep "NOT DONE" exercises/intro/intro1.rs // I AM NOT DONE 虽然第一个程序的编译没有问题，除非你去掉注释 I AM NOT DONE，Rustlings 不会移到下一个程序。来到下一个练习一旦你从 intro1.rs 中去掉这些注释，你就可以通过再一次运行命令 rustlings verify 来到下一个练习。这一次，你会发现 Rustlings 尝试去编译这个系列中的下一个程序（intro2.rs），但是遇到了一个错误。你应该调试并修复这个问题，并前进。这是你理解为什么 Rust 说程序有漏洞的至关重要的一步。$ rustlings verify Progress: [>------------------------] 1/84 ⚠️ Compiling of exercises/intro/intro2.rs failed! Please try again. Here's the output: error: 1 positional argument in format string, but no arguments were given --> exercises/intro/intro2.rs:8:21 | 8 | println!("Hello {}!"); | ^^ error: aborting due to previous error 来点提示Rustlings 有一个非常好用的 hint 参数，这个参数会告诉你示例程序中哪里出错了，以及如何去修复它。你可以认为这是在编译错误信息基础之上，一个额外的帮助选项。$ rustlings hint intro2 Add an argument after the format string. 基于以上提示，修复这个程序就很简单了。你只需要在语句 println 中加一个额外的参数。这个 diff 对比应该能帮你理解发生的变化：< println!("Hello {}!", "world"); --- > println!("Hello {}!"); 一旦你做出了修改，并从源代码中去掉了注释 NOT DONE，你可以再一次运行 rustlings verify 来编译并运行代码。$ rustlings verify Progress: [>-------------------------------------] 1/84 ✅ Successfully ran exercises/intro/intro2.rs! 追踪进度你无法在一天之内做完所有的练习，忘记练到哪也很常见。你可以执行命令 list 来查看你的练习状态。$ rustlings list Name Path Status intro1 exercises/intro/intro1.rs Done intro2 exercises/intro/intro2.rs Done variables1 exercises/variables/variables1.rs Pending variables2 exercises/variables/variables2.rs Pending variables3 exercises/variables/variables3.rs Pending [...] 运行特定的练习如果你不想从头开始并且想要跳过一些练习，Rustlings 允许你使用命令 rustlings run 来专注特定的练习。如此可以运行指定的程序而不需要验证之前的课程。例如：$ rustlings run intro2 Hello world! ✅ Successfully ran exercises/intro/intro2.rs $ rustlings run variables1 敲入练习名字可能会变得乏味，但 Rustlings 为你准备了便利的命令 next 用来移向系列中的下一个练习。$ rustlings run next 替代命令 watch如果你不想在每次修改后还要敲一次 verify，你可以在终端窗口中运行命令 watch，然后再继续修改源代码以解决问题。命令 watch 会检测到这些修改，然后重新编译以查看这些问题是否被解决。$ rustlings watch 通过调试学习Rust 编译器以提供非常有意义的错误信息而被熟知，这些错误信息会帮助你理解在你代码中的问题。这通常意味着更快的调试。Rustlings 是练习 Rust，学会阅读错误信息，并理解 Rust 语言的优秀途径。来看看 GitHub 上 Rustlings 5.0.0 的最新功能吧。下载 Rust 速查表via: https://opensource.com/article/22/7/learn-rust-rustlings作者：Gaurav Kamathe 选题：lkxed 译者：yzuowei 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
即使是复杂的函数，也有几种方法可以单步调试，所以下次在排除代码故障时，可以尝试一下这些 GDB 技术。调试器 是一个可以运行你的代码并检查问题的软件。GNU Debugger（GBD）是最流行的调试器之一，在这篇文章中，我研究了 GDB 的 step 命令和其他几种常见情况的相关命令。step 是一个被广泛使用的命令，但它有一些人们不太了解的地方，可能会使得他们十分困惑。此外，还有一些方法可以在不使用 step 命令的情况下进入一个函数，比如使用不太知名的 advance 命令。1、无调试符号考虑以下这个简单的示例程序：#include <stdio.h> int num() { return 2; } void bar(int i) { printf("i = %d\n", i); } int main() { bar(num()); return 0; } 如果你在没有 调试符号 debugging sysbols 的情况下进行编译（LCTT 译注：即在使用 gcc 编译程序时没有写 -g 选项），然后在 bar 上设置一个断点，然后尝试在这个函数内使用 step 来单步执行语句。GDB 会给出一个 没有行号信息 no line number information 的错误信息。gcc exmp.c -o exmp gdb ./exmp (gdb) b bar Breakpoint 1 at 0x401135 (gdb) r Starting program: /home/ahajkova/exmp Breakpoint 1, 0x0000000000401135 in bar () (gdb) step Single stepping until exit from function bar, which has no line number information. i = 2 0x0000000000401168 in main () 2、stepi 命令但是你仍然可以在没有行号信息的函数内部单步执行语句，但要使用 stepi 命令来代替 step。stepi 一次只执行一条指令。当使用 GDB 的 stepi 命令时，先做 display/i $pc 通常很有用，这会在每一步之后显示 程序计数器 program counter 的值和相应的 机器指令 machine instruction ：(gdb) b bar Breakpoint 1 at 0x401135 (gdb) r Starting program: /home/ahajkova/exmp Breakpoint 1, 0x0000000000401135 in bar () (gdb) display/i $pc 1: x/i $pc => 0x401135 <bar+4>: sub $0x10,%rsp 在上述的 display 命令中，i 代表机器指令，$pc 表示程序计数器寄存器（即 PC 寄存器）。使用 info registers 命令，来打印寄存器的内容，也是十分有用的。(gdb) info registers rax 0x2 2 rbx 0x7fffffffdbc8 140737488346056 rcx 0x403e18 4210200 (gdb) print $rax $1 = 2 (gdb) stepi 0x0000000000401139 in bar () 1: x/i $pc => 0x401139 <bar+8>: mov %edi,-0x4(%rbp) 3、复杂的函数调用在带调试符号的 -g 选项，重新编译示例程序后，你可以使用行号在 main 中 bar 调用上设置断点，然后再单步执行 bar 函数的语句：gcc -g exmp.c -o exmp gdb ./exmp (gdb) b exmp.c:14 Breakpoint 1 at 0x401157: file exmp.c, line 14. (gdb) r Starting program: /home/ahajkova/exmp Breakpoint 1, main () at exmp.c:14 14 bar(num()); 接下来，用 step，来单步执行 bar() 函数的语句：(gdb) step num () at exmp.c:4 4 return 2; 函数调用的参数需要在实际的函数调用之前进行处理，bar() 函数的参数是 num() 函数，所以 num() 会在 bar() 被调用之前执行。但是，通过 GDB 调试，你怎么才能如愿以偿地进入 bar() 函数呢？你可以使用 finish 命令，并再次使用 step 命令。(gdb) finish Run till exit from #0 num () at exmp.c:4 0x0000000000401161 in main () at exmp.c:14 14 bar(num()); Value returned is $1 = 2 (gdb) step bar (i=2) at exmp.c:9 9 printf("i = %d\n", i); 4、tbreak 命令tbreak 命令会设置一个临时断点。如果你不想设置永久断点，那么这个命令是很有用的。举个例子?，你想进入一个复杂的函数调用，例如 f(g(h()), i(j()), ...)，在这种情况下，你需要一个很长的 step/finish/step 序列，才能到达 f 函数。如果你设置一个临时断点，然后再使用 continue 命令，这样就不需要以上的序列了。为了证明这一点，你需要像以前一样将断点设置在 main 的 bar 调用上。然后在 bar 上设置临时断点。当到达该临时断点后，临时断点会被自动删除。(gdb) r Starting program: /home/ahajkova/exmp Breakpoint 1, main () at exmp.c:14 14 bar(num()); (gdb) tbreak bar Temporary breakpoint 2 at 0x40113c: file exmp.c, line 9. 在调用 bar 的时候遇到断点，并在 bar 上设置临时断点后，你只需要使用 continue 继续运行直到 bar 结束。(gdb) continue Continuing. Temporary breakpoint 2, bar (i=2) at exmp.c:9 9 printf("i = %d\n", i); 5、disable 命令类似地，你也可以在 bar 上设置一个正常的断点，然后执行 continue，然后在不再需要第二个断点时，使用 disable 命令禁用这个断点，这样也能达到与 tbreak 相同的效果。(gdb) b exmp.c:14 Breakpoint 1 at 0x401157: file exmp.c, line 14. (gdb) r Starting program: /home/ahajkova/exmp Breakpoint 1, main () at exmp.c:14 14 bar(num()); (gdb) b bar Breakpoint 2 at 0x40113c: file exmp.c, line 9. (gdb) c Continuing. Breakpoint 2, bar (i=2) at exmp.c:9 9 printf("i = %d\n", i); (gdb) disable 2 正如你所看到的，info breakpoints 命令在 Enb 列下显示为 n，这意味着这个断点已被禁用。但你也能在再次需要这个断点时，再启用它。(gdb) info breakpoints Num Type Disp Enb Address What 1 breakpoint keep y 0x0000000000401157 in main at exmp.c:14 breakpoint already hit 1 time 2 breakpoint keep n 0x000000000040113c in bar at exmp.c:9 breakpoint already hit 1 time (gdb) enable 2 (gdb) info breakpoints Num Type Disp Enb Address What 1 breakpoint keep y 0x000000000040116a in main at exmp.c:19 breakpoint already hit 1 time 2 breakpoint keep y 0x0000000000401158 in bar at exmp.c:14 breakpoint already hit 1 time 6、advance 命令运行程序到指定的位置另一个进入函数内部的方法是 advance 命令。你可以简单地用 advance bar，来代替 tbreak bar ; continue。这一命令会将程序继续运行到指定的位置。advance 命令的一个很棒的地方在于：如果程序并没有到达你试图进入的位置，那么 GDB 将在当前函数运行完成后停止。因此，程序的执行会受到限制：Breakpoint 1 at 0x401157: file exmp.c, line 14. (gdb) r Starting program: /home/ahajkova/exmp Breakpoint 1, main () at exmp.c:14 14 bar(num()); (gdb) advance bar bar (i=2) at exmp.c:9 9 printf("i = %d\n", i); 7、skip 命令进入 bar 函数的另一种方式是使用 skip num 命令：(gdb) b exmp.c:14 Breakpoint 1 at 0x401157: file exmp.c, line 14. (gdb) skip num Function num will be skipped when stepping. (gdb) r Starting program: /home/ahajkova/exmp Breakpoint 1, main () at exmp.c:14 14 bar(num()); (gdb) step bar (i=2) at exmp.c:9 9 printf("i = %d\n", i); 请使用 info skip 命令，来了解 GDB 跳过了哪些函数。num() 函数被标记为 y，表示跳过了 num() 函数：(gdb) info skip Num Enb Glob File RE Function 1 y n <none> n num 如果不再需要 skip，可以禁用（并稍后重新启用）或完全删除它。你可以添加另一个 skip，并禁用第一个 skip，然后全部删除。要禁用某个 skip，必须指定其编号（例如，skip disable 1），如果没有指定，则会禁用所有的 skip。启用或删除 skip 的工作原理相同：(gdb) skip bar (gdb) skip disable 1 (gdb) info skip Num Enb Glob File RE Function 1 n n <none> n num 2 y n <none> n bar (gdb) skip delete (gdb) info skip Not skipping any files or functions. GDB 的 step 命令使用 GDB 的 step 命令是调试程序的一个有用工具。即使是复杂的函数，也有几种方法可以单步调试这些函数，所以下次你在排除代码问题的时候，可以尝试一下这些 GDB 技术。via: https://opensource.com/article/22/12/gdb-step-command作者：Alexandra 选题：lkxed 译者：chai001125 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
你们好！我一直在编写一本关于调试的杂志（这是 目录的初稿）。作为其中的一部分，我认为阅读一些关于调试的学术论文可能会很有趣，上周 Greg Wilson 给我发了一些关于调试学术研究的论文。其中一篇论文（《[建立一个调试教学的框架[付费墙]](https://dl.acm.org/doi/abs/10.1145/3286960.3286970)》）对我们有效调试所需的不同种类的知识/技能进行了分类，我非常喜欢。它来自另一篇关于故障排除的更一般性的论文：《学会排错：一个新的基于理论的设计架构》。我认为这个分类对于思考如何更好地进行调试是一个非常有用的结构，所以我把论文中的五个类别重新规划为你可以采取的行动，以提高调试的效率。以下是这些行动：1、学习代码库要调试一些代码，你需要了解你正在使用的代码库。这似乎有点显而易见（当然，不了解代码的工作原理，你就无法调试代码！）这种学习随着时间的推移会很自然地发生，而且实际上调试也是 学习 一个新的代码库如何工作的最好方法之一—— 看到一些代码是如何崩溃的，有助于你了解它是如何工作的。该论文将此称为“系统知识”。2、学习系统论文中提到，你需要了解编程语言，但我认为不止于此 —— 为了修复 bug，往往你需要学习很多更广泛的环境，而不仅仅是语言。举个例子，如果你是后端 Web 开发者，你可能需要的一些“系统”知识包括：HTTP 缓存如何工作CORS数据库事务是如何工作的我发现我经常需要更有意识地去学习像这样的系统性的东西 —— 我需要真正花时间去查找和阅读它们。该论文将此称为“领域知识”。3、学习你的工具现在有很多工具，例如：调试器（GDB 等）浏览器开发工具 剖析器 profiler strace / ltracetcpdump / wireshark核心转储甚至像错误信息这样的基本东西（如何正确阅读它们）我在这个博客上写了很多关于调试工具的文章，并且肯定学习这些工具给我带来了巨大的变化。该论文将此称为“处理性知识”。4、学习策略这是最模糊的一类，在如何高效调试的过程中，我们都有很多策略和启发式方法。比如说：写一个单元测试写一个小的独立程序来重现这个错误找到一个能工作的版本的代码，看看有什么变化打印出无数的东西增加额外的日志记录休息一下向朋友解释这个错误，然后在中途发现问题所在查看 GitHub 上的问题，看看是否有匹配的问题在写这本杂志的时候，我一直在思考这个类别，但我想让这篇文章简短，所以我不会在这里多说。该论文将此称为“战略知识”。5、获得经验最后一个类别是“经验”。这篇论文对此有一个非常有趣的评论：他们的研究结果并没有显示出新手和专家所采用的策略有什么明显的区别。专家只是形成了更多正确的假设，并且在寻找故障方面更有效率。作者怀疑这个结果是由于新手和专家之间的编程经验不同造成的。这真的引起了我的共鸣 —— 我遇到过很多第一次遇到时非常令人沮丧和困难的 bug，而在第五次、第十次或第二十次时就非常简单了。对我来说，这也是最直接的知识类别之一 —— 你需要做的就是调查一百万个 bug，反正这就是我们作为程序员的全部生活 : ) 。这需要很长的时间，但我觉得它发生得很自然。本文将此称为“经验知识”。就这样吧！我打算把这篇文章写得很短，我只是非常喜欢这个分类，想把它分享出来。via: https://jvns.ca/blog/2022/08/30/a-way-to-categorize-debugging-skills/作者：Julia Evans 选题：lkxed 译者：aftermath0703 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
调试 Web 服务器的一种方法是使用 wget 命令行程序。有时在管理一个网站时，事情会被搞得一团糟。你可能会删除一些陈旧的内容，用重定向到其他页面来代替。后来，在做了其他改动后，你发现一些网页变得完全无法访问了。你可能会在浏览器中看到一个错误：“该页面没有正确重定向”，并建议你检查你的 cookie。调试这种情况的一个方法是使用 wget 命令行程序，使用 -S 选项来显示所有的服务器响应。当使用 wget 进行调试时，我也喜欢使用 -O 选项将输出保存到一些临时文件中，以备以后需要查看其内容。$ wget -O /tmp/test.html -S http://10.0.0.11/announce/ --2021-08-24 17:09:49-- http://10.0.0.11/announce/ Connecting to 10.0.0.11:80... connected. HTTP request sent, awaiting response... HTTP/1.1 302 Found Date: Tue, 24 Aug 2021 22:09:49 GMT Server: Apache/2.4.48 (Fedora) X-Powered-By: PHP/7.4.21 Location: http://10.0.0.11/assets/ Content-Length: 0 Keep-Alive: timeout=5, max=100 Connection: Keep-Alive Content-Type: text/html; charset=UTF-8 Location: http://10.0.0.11/assets/ [following] --2021-08-24 17:09:49-- http://10.0.0.11/assets/ Reusing existing connection to 10.0.0.11:80. HTTP request sent, awaiting response... HTTP/1.1 302 Found Date: Tue, 24 Aug 2021 22:09:49 GMT Server: Apache/2.4.48 (Fedora) X-Powered-By: PHP/7.4.21 Location: http://10.0.0.11/announce/ Content-Length: 0 Keep-Alive: timeout=5, max=99 Connection: Keep-Alive Content-Type: text/html; charset=UTF-8 Location: http://10.0.0.11/announce/ [following] --2021-08-24 17:09:49-- http://10.0.0.11/announce/ Reusing existing connection to 10.0.0.11:80. . . . 20 redirections exceeded. 我在这个输出中省略了很多重复的内容。通过阅读服务器的响应，你可以看到 http://10.0.0.11/announce/ 立即重定向到 http://10.0.0.11/assets/，然后又重定向到 http://10.0.0.11/announce/。以此类推。这是一个无休止的循环，wget 将在 20 次重定向后退出。但有了这些调试信息，你可以修复重定向，避免循环。via: https://opensource.com/article/21/9/wget-debug-web-server作者：Jim Hall 选题：lujun9972 译者：geekpi 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
使用 GNU 调试器来解决你的代码问题。GNU 调试器常以它的命令 gdb 称呼它，它是一个交互式的控制台，可以帮助你浏览源代码、分析执行的内容，其本质上是对错误的应用程序中出现的问题进行逆向工程。故障排除的麻烦在于它很复杂。GNU 调试器 并不是一个特别复杂的应用程序，但如果你不知道从哪里开始，甚至不知道何时和为何你可能需要求助于 GDB 来进行故障排除，那么它可能会让人不知所措。如果你一直使用 print、echo 或 printf 语句来调试你的代码，当你开始思考是不是还有更强大的东西时，那么本教程就是为你准备的。有错误的代码要开始使用 GDB，你需要一些代码。这里有一个用 C++ 写的示例应用程序（如果你一般不使用 C++ 编写程序也没关系，在所有语言中原理都是一样的），其来源于 猜谜游戏系列 中的一个例子。#include <iostream> #include <stdlib.h> //srand #include <stdio.h> //printf using namespace std; int main () { srand (time(NULL)); int alpha = rand() % 8; cout << "Hello world." << endl; int beta = 2; printf("alpha is set to is %s\n", alpha); printf("kiwi is set to is %s\n", beta); return 0; } // main 这个代码示例中有一个 bug，但它确实可以编译（至少在 GCC 5 的时候）。如果你熟悉 C++，你可能已经看到了，但这是一个简单的问题，可以帮助新的 GDB 用户了解调试过程。编译并运行它就可以看到错误：$ g++ -o buggy example.cpp $ ./buggy Hello world. Segmentation fault 排除段故障从这个输出中，你可以推测变量 alpha 的设置是正确的，因为否则的话，你就不会看到它后面的那行代码执行。当然，这并不总是正确的，但这是一个很好的工作理论，如果你使用 printf 作为日志和调试器，基本上也会得出同样的结论。从这里，你可以假设 bug 在于成功打印的那一行之后的某行。然而，不清楚错误是在下一行还是在几行之后。GNU 调试器是一个交互式的故障排除工具，所以你可以使用 gdb 命令来运行错误的代码。为了得到更好的结果，你应该从包含有调试符号的源代码中重新编译你的错误应用程序。首先，看看 GDB 在不重新编译的情况下能提供哪些信息：$ gdb ./buggy Reading symbols from ./buggy...done. (gdb) start Temporary breakpoint 1 at 0x400a44 Starting program: /home/seth/demo/buggy Temporary breakpoint 1, 0x0000000000400a44 in main () (gdb) 当你以一个二进制可执行文件作为参数启动 GDB 时，GDB 会加载该应用程序，然后等待你的指令。因为这是你第一次在这个可执行文件上运行 GDB，所以尝试重复这个错误是有意义的，希望 GDB 能够提供进一步的见解。很直观，GDB 用来启动它所加载的应用程序的命令就是 start。默认情况下，GDB 内置了一个断点，所以当它遇到你的应用程序的 main 函数时，它会暂停执行。要让 GDB 继续执行，使用命令 continue：(gdb) continue Continuing. Hello world. Program received signal SIGSEGV, Segmentation fault. 0x00007ffff71c0c0b in vfprintf () from /lib64/libc.so.6 (gdb) 毫不意外：应用程序在打印 “Hello world” 后不久就崩溃了，但 GDB 可以提供崩溃发生时正在发生的函数调用。这有可能就足够你找到导致崩溃的 bug，但为了更好地了解 GDB 的功能和一般的调试过程，想象一下，如果问题还没有变得清晰，你想更深入地挖掘这段代码发生了什么。用调试符号编译代码要充分利用 GDB，你需要将调试符号编译到你的可执行文件中。你可以用 GCC 中的 -g 选项来生成这个符号：$ g++ -g -o debuggy example.cpp $ ./debuggy Hello world. Segmentation fault 将调试符号编译到可执行文件中的结果是得到一个大得多的文件，所以通常不会分发它们，以增加便利性。然而，如果你正在调试开源代码，那么用调试符号重新编译测试是有意义的：$ ls -l *buggy* *cpp -rw-r--r-- 310 Feb 19 08:30 debug.cpp -rwxr-xr-x 11624 Feb 19 10:27 buggy* -rwxr-xr-x 22952 Feb 19 10:53 debuggy* 用 GDB 调试加载新的可执行文件（本例中为 debuggy）以启动 GDB：$ gdb ./debuggy Reading symbols from ./debuggy...done. (gdb) start Temporary breakpoint 1 at 0x400a44 Starting program: /home/seth/demo/debuggy Temporary breakpoint 1, 0x0000000000400a44 in main () (gdb) 如前所述，使用 start 命令进行：(gdb) start Temporary breakpoint 1 at 0x400a48: file debug.cpp, line 9. Starting program: /home/sek/demo/debuggy Temporary breakpoint 1, main () at debug.cpp:9 9 srand (time(NULL)); (gdb) 这一次，自动的 main 断点可以指明 GDB 暂停的行号和该行包含的代码。你可以用 continue 恢复正常操作，但你已经知道应用程序在完成之前就会崩溃，因此，你可以使用 next 关键字逐行步进检查你的代码：(gdb) next 10 int alpha = rand() % 8; (gdb) next 11 cout << "Hello world." << endl; (gdb) next Hello world. 12 int beta = 2; (gdb) next 14 printf("alpha is set to is %s\n", alpha); (gdb) next Program received signal SIGSEGV, Segmentation fault. 0x00007ffff71c0c0b in vfprintf () from /lib64/libc.so.6 (gdb) 从这个过程可以确认，崩溃不是发生在设置 beta 变量的时候，而是执行 printf 行的时候。这个 bug 在本文中已经暴露了好几次（破坏者：向 printf 提供了错误的数据类型），但暂时假设解决方案仍然不明确，需要进一步调查。设置断点一旦你的代码被加载到 GDB 中，你就可以向 GDB 询问到目前为止代码所产生的数据。要尝试数据自省，通过再次发出 start 命令来重新启动你的应用程序，然后进行到第 11 行。一个快速到达 11 行的简单方法是设置一个寻找特定行号的断点：(gdb) start The program being debugged has been started already. Start it from the beginning? (y or n) y Temporary breakpoint 2 at 0x400a48: file debug.cpp, line 9. Starting program: /home/sek/demo/debuggy Temporary breakpoint 2, main () at debug.cpp:9 9 srand (time(NULL)); (gdb) break 11 Breakpoint 3 at 0x400a74: file debug.cpp, line 11. 建立断点后，用 continue 继续执行：(gdb) continue Continuing. Breakpoint 3, main () at debug.cpp:11 11 cout << "Hello world." << endl; (gdb) 现在暂停在第 11 行，就在 alpha 变量被设置之后，以及 beta 被设置之前。用 GDB 进行变量自省要查看一个变量的值，使用 print 命令。在这个示例代码中，alpha 的值是随机的，所以你的实际结果可能与我的不同：(gdb) print alpha $1 = 3 (gdb) 当然，你无法看到一个尚未建立的变量的值：(gdb) print beta $2 = 0 使用流程控制要继续进行，你可以步进代码行来到达将 beta 设置为一个值的位置：(gdb) next Hello world. 12 int beta = 2; (gdb) next 14 printf("alpha is set to is %s\n", alpha); (gdb) print beta $3 = 2 另外，你也可以设置一个观察点，它就像断点一样，是一种控制 GDB 执行代码流程的方法。在这种情况下，你知道 beta 变量应该设置为 2，所以你可以设置一个观察点，当 beta 的值发生变化时提醒你：(gdb) watch beta > 0 Hardware watchpoint 5: beta > 0 (gdb) continue Continuing. Breakpoint 3, main () at debug.cpp:11 11 cout << "Hello world." << endl; (gdb) continue Continuing. Hello world. Hardware watchpoint 5: beta > 0 Old value = false New value = true main () at debug.cpp:14 14 printf("alpha is set to is %s\n", alpha); (gdb) 你可以用 next 手动步进完成代码的执行，或者你可以用断点、观察点和捕捉点来控制代码的执行。用 GDB 分析数据你可以以不同格式查看数据。例如，以八进制值查看 beta 的值：(gdb) print /o beta $4 = 02 要查看其在内存中的地址：(gdb) print /o &beta $5 = 0x2 你也可以看到一个变量的数据类型：(gdb) whatis beta type = int 用 GDB 解决错误这种自省不仅能让你更好地了解什么代码正在执行，还能让你了解它是如何执行的。在这个例子中，对变量运行的 whatis 命令给了你一个线索，即你的 alpha 和 beta 变量是整数，这可能会唤起你对 printf 语法的记忆，使你意识到在你的 printf 语句中，你必须使用 %d 来代替 %s。做了这个改变，就可以让应用程序按预期运行，没有更明显的错误存在。当代码编译后发现有 bug 存在时，特别令人沮丧，但最棘手的 bug 就是这样，如果它们很容易被发现，那它们就不是 bug 了。使用 GDB 是猎取并消除它们的一种方法。下载我们的速查表生活的真相就是这样，即使是最基本的编程，代码也会有 bug。并不是所有的错误都会导致应用程序无法运行（甚至无法编译），也不是所有的错误都是由错误的代码引起的。有时，bug 是基于一个特别有创意的用户所做的意外的选择组合而间歇性发生的。有时，程序员从他们自己的代码中使用的库中继承了 bug。无论原因是什么，bug 基本上无处不在，程序员的工作就是发现并消除它们。GNU 调试器是一个寻找 bug 的有用工具。你可以用它做的事情比我在本文中演示的要多得多。你可以通过 GNU Info 阅读器来了解它的许多功能：$ info gdb 无论你是刚开始学习 GDB 还是专业人员的，提醒一下你有哪些命令是可用的，以及这些命令的语法是什么，都是很有帮助的。下载 GDB 速查表via: https://opensource.com/article/21/3/debug-code-gdb作者：Seth Kenlon 选题：lujun9972 译者：wxy 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
了解如何处理转储文件将帮你找到应用中难以重现的 bug。崩溃转储、内存转储、核心转储、系统转储……这些全都会产生同样的产物：一个包含了当应用崩溃时，在那个特定时刻应用的内存状态的文件。这是一篇指导文章，你可以通过克隆示例的应用仓库来跟随学习：git clone https://github.com/hANSIc99/core_dump_example.git 信号如何关联到转储信号是操作系统和用户应用之间的进程间通讯。Linux 使用 POSIX 标准中定义的信号。在你的系统上，你可以在 /usr/include/bits/signum-generic.h 找到标准信号的定义。如果你想知道更多关于在你的应用程序中使用信号的信息，这有一个信息丰富的 signal 手册页。简单地说，Linux 基于预期的或意外的信号来触发进一步的活动。当你退出一个正在运行的应用程序时，应用程序通常会收到 SIGTERM 信号。因为这种类型的退出信号是预期的，所以这个操作不会创建一个内存转储。以下信号将导致创建一个转储文件（来源：GNU C库）:SIGFPE：错误的算术操作SIGILL：非法指令SIGSEGV：对存储的无效访问SIGBUS：总线错误SIGABRT：程序检测到的错误，并通过调用 abort() 来报告SIGIOT：这个信号在 Fedora 上已经过时，过去在 PDP-11 上用 abort() 时触发，现在映射到 SIGABRT创建转储文件导航到 core_dump_example 目录，运行 make，并使用 -c1 开关执行该示例二进制：./coredump -c1 该应用将以状态 4 退出，带有如下错误：“Abgebrochen (Speicherabzug geschrieben) ”（LCTT 译注：这是德语，应该是因为本文作者系统是德语环境）大致翻译为“分段故障（核心转储）”。是否创建核心转储是由运行该进程的用户的资源限制决定的。你可以用 ulimit 命令修改资源限制。检查当前创建核心转储的设置：ulimit -c 如果它输出 unlimited，那么它使用的是（建议的）默认值。否则，用以下方法纠正限制：ulimit -c unlimited 要禁用创建核心转储，可以设置其大小为 0：ulimit -c 0 这个数字指定了核心转储文件的大小，单位是块。什么是核心转储？内核处理核心转储的方式定义在：/proc/sys/kernel/core_pattern 我运行的是 Fedora 31，在我的系统上，该文件包含的内容是：/usr/lib/systemd/systemd-coredump %P %u %g %s %t %c %h 这表明核心转储被转发到 systemd-coredump 工具。在不同的 Linux 发行版中，core_pattern 的内容会有很大的不同。当使用 systemd-coredump 时，转储文件被压缩保存在 /var/lib/systemd/coredump 下。你不需要直接接触这些文件，你可以使用 coredumpctl。比如说：coredumpctl list 会显示系统中保存的所有可用的转储文件。使用 coredumpctl dump，你可以从最后保存的转储文件中检索信息：[stephan@localhost core_dump_example]$ ./coredump Application started… (…….) Message: Process 4598 (coredump) of user 1000 dumped core. Stack trace of thread 4598: #0 0x00007f4bbaf22625 __GI_raise (libc.so.6) #1 0x00007f4bbaf0b8d9 __GI_abort (libc.so.6) #2 0x00007f4bbaf664af __libc_message (libc.so.6) #3 0x00007f4bbaf6da9c malloc_printerr (libc.so.6) #4 0x00007f4bbaf6f49c _int_free (libc.so.6) #5 0x000000000040120e n/a (/home/stephan/Dokumente/core_dump_example/coredump) #6 0x00000000004013b1 n/a (/home/stephan/Dokumente/core_dump_example/coredump) #7 0x00007f4bbaf0d1a3 __libc_start_main (libc.so.6) #8 0x000000000040113e n/a (/home/stephan/Dokumente/core_dump_example/coredump) Refusing to dump core to tty (use shell redirection or specify — output). 这表明该进程被 SIGABRT 停止。这个视图中的堆栈跟踪不是很详细，因为它不包括函数名。然而，使用 coredumpctl debug，你可以简单地用调试器（默认为 GDB）打开转储文件。输入 bt（ 回溯 backtrace 的缩写）可以得到更详细的视图：Core was generated by `./coredump -c1'. Program terminated with signal SIGABRT, Aborted. #0 __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50 50 return ret; (gdb) bt #0 __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50 #1 0x00007fc37a9aa8d9 in __GI_abort () at abort.c:79 #2 0x00007fc37aa054af in __libc_message (action=action@entry=do_abort, fmt=fmt@entry=0x7fc37ab14f4b "%s\n") at ../sysdeps/posix/libc_fatal.c:181 #3 0x00007fc37aa0ca9c in malloc_printerr (str=str@entry=0x7fc37ab130e0 "free(): invalid pointer") at malloc.c:5339 #4 0x00007fc37aa0e49c in _int_free (av=<optimized out>, p=<optimized out>, have_lock=0) at malloc.c:4173 #5 0x000000000040120e in freeSomething(void*) () #6 0x0000000000401401 in main () 与后续帧相比，main() 和 freeSomething() 的内存地址相当低。由于共享对象被映射到虚拟地址空间末尾的区域，可以认为 SIGABRT 是由共享库中的调用引起的。共享对象的内存地址在多次调用之间并不是恒定不变的，所以当你看到多次调用之间的地址不同时，完全可以认为是共享对象。堆栈跟踪显示，后续的调用源于 malloc.c，这说明内存的（取消）分配可能出了问题。在源代码中，（即使没有任何 C++ 知识）你也可以看到，它试图释放一个指针，而这个指针并没有被内存管理函数返回。这导致了未定义的行为，并导致了 SIGABRT。void freeSomething(void *ptr){ free(ptr); } int nTmp = 5; int *ptrNull = &nTmp; freeSomething(ptrNull); systemd 的这个 coredump 工具可以在 /etc/systemd/coredump.conf 中配置。可以在 /etc/systemd/systemd-tmpfiles-clean.timer 中配置轮换清理转储文件。你可以在其手册页中找到更多关于 coredumpctl 的信息。用调试符号编译打开 Makefile 并注释掉第 9 行的最后一部分。现在应该是这样的：CFLAGS =-Wall -Werror -std=c++11 -g -g 开关使编译器能够创建调试信息。启动应用程序，这次使用 -c2 开关。./coredump -c2 你会得到一个浮点异常。在 GDB 中打开该转储文件：coredumpctl debug 这一次，你会直接被指向源代码中导致错误的那一行：Reading symbols from /home/stephan/Dokumente/core_dump_example/coredump… [New LWP 6218] Core was generated by `./coredump -c2'. Program terminated with signal SIGFPE, Arithmetic exception. #0 0x0000000000401233 in zeroDivide () at main.cpp:29 29 nRes = 5 / nDivider; (gdb) 键入 list 以获得更好的源代码概览：(gdb) list 24 int zeroDivide(){ 25 int nDivider = 5; 26 int nRes = 0; 27 while(nDivider > 0){ 28 nDivider--; 29 nRes = 5 / nDivider; 30 } 31 return nRes; 32 } 使用命令 info locals 从应用程序失败的时间点检索局部变量的值：(gdb) info locals nDivider = 0 nRes = 5 结合源码，可以看出，你遇到的是零除错误：nRes = 5 / 0 结论了解如何处理转储文件将帮助你找到并修复应用程序中难以重现的随机错误。而如果不是你的应用程序，将核心转储转发给开发人员将帮助她或他找到并修复问题。via: https://opensource.com/article/20/8/linux-dump作者：Stephan Avenwedde 选题：lujun9972 译者：wxy 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
用这个微软的开源工具，获取进程信息。微软越来越心仪 Linux 和开源，这并不是什么秘密。在过去几年中，该公司稳步地增加了对开源的贡献，包括将其部分软件和工具移植到 Linux。2018 年底，微软宣布将其 Sysinternals 的部分工具以开源的方式移植到 Linux，Linux 版的 ProcDump是其中的第一个。如果你在 Windows 上从事过调试或故障排除工作，你可能听说过 Sysinternals，它是一个“瑞士军刀”工具集，可以帮助系统管理员、开发人员和 IT 安全专家监控和排除 Windows 环境的故障。Sysinternals 最受欢迎的工具之一是 ProcDump。顾名思义，它用于将正在运行的进程的内存转储到磁盘上的一个核心文件中。然后可以用调试器对这个核心文件进行分析，了解转储时进程的状态。因为之前用过 Sysinternals，所以我很想试试 ProcDump 的 Linux 移植版。开始使用 Linux 上的 ProcDump要试用 Linux 上的 ProcDump，你需要下载该工具并编译它。（我使用的是 Red Hat Enterprise Linux，尽管这些步骤在其他 Linux 发行版上应该是一样的）：$ cat /etc/redhat-release Red Hat Enterprise Linux release 8.2 (Ootpa) $ $ uname -r 4.18.0-193.el8.x86_64 $ 首先，克隆 Linux 版 ProcDump 的版本库。$ git clone https://github.com/microsoft/ProcDump-for-Linux.git Cloning into 'ProcDump-for-Linux'... remote: Enumerating objects: 40, done. remote: Counting objects: 100% (40/40), done. remote: Compressing objects: 100% (33/33), done. remote: Total 414 (delta 14), reused 14 (delta 6), pack-reused 374 Receiving objects: 100% (414/414), 335.28 KiB | 265.00 KiB/s, done. Resolving deltas: 100% (232/232), done. $ $ cd ProcDump-for-Linux/ $ $ ls azure-pipelines.yml CONTRIBUTING.md docs INSTALL.md Makefile procdump.gif src CODE_OF_CONDUCT.md dist include LICENSE procdump.1 README.md tests $ 接下来，使用 make 构建程序。它能准确地输出编译源文件所需的 GCC 命令行参数。$ make rm -rf obj rm -rf bin rm -rf /root/ProcDump-for-Linux/pkgbuild gcc -c -g -o obj/Logging.o src/Logging.c -Wall -I ./include -pthread -std=gnu99 gcc -c -g -o obj/Events.o src/Events.c -Wall -I ./include -pthread -std=gnu99 gcc -c -g -o obj/ProcDumpConfiguration.o src/ProcDumpConfiguration.c -Wall -I ./include -pthread -std=gnu99 gcc -c -g -o obj/Handle.o src/Handle.c -Wall -I ./include -pthread -std=gnu99 gcc -c -g -o obj/Process.o src/Process.c -Wall -I ./include -pthread -std=gnu99 gcc -c -g -o obj/Procdump.o src/Procdump.c -Wall -I ./include -pthread -std=gnu99 gcc -c -g -o obj/TriggerThreadProcs.o src/TriggerThreadProcs.c -Wall -I ./include -pthread -std=gnu99 gcc -c -g -o obj/CoreDumpWriter.o src/CoreDumpWriter.c -Wall -I ./include -pthread -std=gnu99 gcc -o bin/procdump obj/Logging.o obj/Events.o obj/ProcDumpConfiguration.o obj/Handle.o obj/Process.o obj/Procdump.o obj/TriggerThreadProcs.o obj/CoreDumpWriter.o -Wall -I ./include -pthread -std=gnu99 gcc -c -g -o obj/ProcDumpTestApplication.o tests/integration/ProcDumpTestApplication.c -Wall -I ./include -pthread -std=gnu99 gcc -o bin/ProcDumpTestApplication obj/ProcDumpTestApplication.o -Wall -I ./include -pthread -std=gnu99 $ 编译过程中会创建两个新的目录。第一个是 obj/ 目录，存放编译期间创建的对象文件。第二个（也是更重要的）目录是 bin/，它是存储编译出的 procdump 程序的地方。它还会编译另一个名为 ProcDumpTestApplication 的测试二进制文件：$ ls obj/ CoreDumpWriter.o Handle.o ProcDumpConfiguration.o ProcDumpTestApplication.o TriggerThreadProcs.o Events.o Logging.o Procdump.o Process.o $ $ $ ls bin/ procdump ProcDumpTestApplication $ $ file bin/procdump bin/procdump: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=6e8827db64835ea0d1f0941ac3ecff9ee8c06e6b, with debug_info, not stripped $ $ file bin/ProcDumpTestApplication bin/ProcDumpTestApplication: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=c8fd86f53c07df142e52518815b2573d1c690e4e, with debug_info, not stripped $ 在此情况下，每次运行 procdump 实用程序时，你都必须移动到 bin/ 文件夹中。要使它在系统中的任何地方都可以使用，运行 make install。这将这个二进制文件复制到通常的 bin/ 目录中，它是你的 shell $PATH 的一部分：$ which procdump /usr/bin/which: no procdump in (/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin) $ $ make install mkdir -p //usr/bin cp bin/procdump //usr/bin mkdir -p //usr/share/man/man1 cp procdump.1 //usr/share/man/man1 $ $ which procdump /usr/bin/procdump $ 安装时，ProcDump 提供了一个手册页，你可以用 man procdump 访问：$ man procdump $ 运行 ProcDump要转储一个进程的内存，你需要向 ProcDump 提供它的进程 ID（PID）。你可以使用机器上任何正在运行的程序或守护进程。在这个例子中，我将使用一个永远循环的小 C 程序。编译程序并运行它（要退出程序，按 Ctrl+C，如果程序在后台运行，则使用 kill 命令并输入 PID）：$ cat progxyz.c #include <stdio.h> int main() { for (;;) { printf("."); sleep(1); } return 0; } $ $ gcc progxyz.c -o progxyz $ $ ./progxyz & [1] 350498 $ 运行该程序，你可以使用 pgrep 或 ps 找到它的 PID。记下 PID：$ pgrep progxyz 350498 $ $ ps -ef | grep progxyz root 350498 345445 0 03:29 pts/1 00:00:00 ./progxyz root 350508 347350 0 03:29 pts/0 00:00:00 grep --color=auto progxyz $ 当测试进程正在运行时，调用 procdump 并提供 PID。下面的输出表明了该进程的名称和 PID，并报告它生成了一个核心转储文件，并显示其文件名：$ procdump -p 350498 ProcDump v1.1.1 - Sysinternals process dump utility Copyright (C) 2020 Microsoft Corporation. All rights reserved. Licensed under the MIT license. Mark Russinovich, Mario Hewardt, John Salem, Javid Habibi Monitors a process and writes a dump file when the process exceeds the specified criteria. Process: progxyz (350498) CPU Threshold: n/a Commit Threshold: n/a Polling interval (ms): 1000 Threshold (s): 10 Number of Dumps: 1 Press Ctrl-C to end monitoring without terminating the process. [03:30:00 - INFO]: Timed: [03:30:01 - INFO]: Core dump 0 generated: progxyz_time_2020-06-24_03:30:00.350498 $ 列出当前目录的内容，你应该可以看到新的核心文件。文件名与 procdump 命令显示的文件名一致，日期、时间、PID 都会附加在文件名上：$ ls -l progxyz_time_2020-06-24_03\:30\:00.350498 -rw-r--r--. 1 root root 356848 Jun 24 03:30 progxyz_time_2020-06-24_03:30:00.350498 $ $ file progxyz_time_2020-06-24_03\:30\:00.350498 progxyz_time_2020-06-24_03:30:00.350498: ELF 64-bit LSB core file, x86-64, version 1 (SYSV), SVR4-style, from './progxyz', real uid: 0, effective uid: 0, real gid: 0, effective gid: 0, execfn: './progxyz', platform: 'x86_64' $ 用 GNU 项目调试器分析核心文件。要查看是否可以读取该转储文件，调用 GNU 项目调试器（gdb）。记得提供测试二进制文件的路径，这样你就可以看到堆栈上所有的函数名。在这里，bt（回溯）表明，当转储被采集时，sleep() 函数正在执行：$ gdb -q ./progxyz ./progxyz_time_2020-06-24_03\:30\:00.350498 Reading symbols from ./progxyz...(no debugging symbols found)...done. [New LWP 350498] Core was generated by `./progxyz'. #0 0x00007fb6947e9208 in nanosleep () from /lib64/libc.so.6 Missing separate debuginfos, use: yum debuginfo-install glibc-2.28-101.el8.x86_64 (gdb) bt #0 0x00007fb6947e9208 in nanosleep () from /lib64/libc.so.6 #1 0x00007fb6947e913e in sleep () from /lib64/libc.so.6 #2 0x00000000004005f3 in main () (gdb) gcore 怎么样？Linux 用户会很快指出，Linux 已经有一个叫 gcore 的命令，大多数 Linux 发行版都有这个命令，它的作用和 ProcDump 完全一样。你说的对。如果你从来没有使用过它，可以尝试用 gcore 来转储一个进程的核心。再次运行测试程序，然后运行 gcore，并提供 PID 作为参数：$ ./progxyz & [1] 350664 $ $ $ pgrep progxyz 350664 $ $ $ gcore 350664 0x00007fefd3be2208 in nanosleep () from /lib64/libc.so.6 Saved corefile core.350664 [Inferior 1 (process 350664) detached] $ gcore 打印一条消息，说它已将核心文件保存到一个特定的文件中。检查当前目录，找到这个核心文件，然后再次使用 gdb 加载它：$ $ ls -l core.350664 -rw-r--r--. 1 root root 356848 Jun 24 03:34 core.350664 $ $ $ file core.350664 core.350664: ELF 64-bit LSB core file, x86-64, version 1 (SYSV), SVR4-style, from './progxyz', real uid: 0, effective uid: 0, real gid: 0, effective gid: 0, execfn: './progxyz', platform: 'x86_64' $ $ gdb -q ./progxyz ./core.350664 Reading symbols from ./progxyz...(no debugging symbols found)...done. [New LWP 350664] Core was generated by `./progxyz'. #0 0x00007fefd3be2208 in nanosleep () from /lib64/libc.so.6 Missing separate debuginfos, use: yum debuginfo-install glibc-2.28-101.el8.x86_64 (gdb) bt #0 0x00007fefd3be2208 in nanosleep () from /lib64/libc.so.6 #1 0x00007fefd3be213e in sleep () from /lib64/libc.so.6 #2 0x00000000004005f3 in main () (gdb) q $ 为了使 gcore 可以工作，你需要确保以下设置到位。首先，确保为核心文件设置了 ulimit，如果设置为 0，核心文件将不会被生成。第二，确保 /proc/sys/kernel/core_pattern 有正确的设置来指定核心模式：$ ulimit -c unlimited $ 你应该使用 ProcDump 还是 gcore？有几种情况下，你可能更喜欢使用 ProcDump 而不是 gcore，ProcDump 有一些内置的功能，在一些情况下可能很有用。等待测试二进制文件的执行无论是使用 ProcDump 还是 gcore，测试进程必须被执行并处于运行状态，这样才能提供一个 PID 来生成核心文件。但 ProcDump 有一个功能，就是等待特定的二进制文件运行，一旦发现运行的测试二进制文件与给定的名称相匹配，它就会为该测试二进制文件生成一个核心文件。它可以使用 -w 参数和程序名称而不是 PID 来启用。这个功能在测试程序快速退出的情况下很有用。下面是它的工作原理。在这个例子中，没有名为 progxyz 的进程在运行：$ pgrep progxyz $ 用 -w 参数调用 procdump，让它保持等待。在另一个终端，调用测试二进制 progxyz：$ procdump -w progxyz ProcDump v1.1.1 - Sysinternals process dump utility Copyright (C) 2020 Microsoft Corporation. All rights reserved. Licensed under the MIT license. Mark Russinovich, Mario Hewardt, John Salem, Javid Habibi Monitors a process and writes a dump file when the process exceeds the specified criteria. Process: progxyz (pending) CPU Threshold: n/a Commit Threshold: n/a Polling interval (ms): 1000 Threshold (s): 10 Number of Dumps: 1 Press Ctrl-C to end monitoring without terminating the process. [03:39:23 - INFO]: Waiting for process 'progxyz' to launch... 然后，从另一个终端调用测试二进制 progxyz：$ ./progxyz & [1] 350951 $ ProcDump 立即检测到该二进制正在运行，并转储这个二进制的核心文件：[03:39:23 - INFO]: Waiting for process 'progxyz' to launch... [03:43:22 - INFO]: Found process with PID 350951 [03:43:22 - INFO]: Timed: [03:43:23 - INFO]: Core dump 0 generated: progxyz_time_2020-06-24_03:43:22.350951 $ $ ls -l progxyz_time_2020-06-24_03\:43\:22.350951 -rw-r--r--. 1 root root 356848 Jun 24 03:43 progxyz_time_2020-06-24_03:43:22.350951 $ $ file progxyz_time_2020-06-24_03\:43\:22.350951 progxyz_time_2020-06-24_03:43:22.350951: ELF 64-bit LSB core file, x86-64, version 1 (SYSV), SVR4-style, from './progxyz', real uid: 0, effective uid: 0, real gid: 0, effective gid: 0, execfn: './progxyz', platform: 'x86_64' $ 多个核心转储另一个重要的 ProcDump 功能是，你可以通过使用命令行参数 -n <count> 指定要生成多少个核心文件。核心转储之间的默认时间间隔是 10 秒，但你可以使用 -s 参数修改。这个例子使用 ProcDump 对测试二进制文件进行了三次核心转储：$ ./progxyz & [1] 351014 $ $ procdump -n 3 -p 351014 ProcDump v1.1.1 - Sysinternals process dump utility Copyright (C) 2020 Microsoft Corporation. All rights reserved. Licensed under the MIT license. Mark Russinovich, Mario Hewardt, John Salem, Javid Habibi Monitors a process and writes a dump file when the process exceeds the specified criteria. Process: progxyz (351014) CPU Threshold: n/a Commit Threshold: n/a Polling interval (ms): 1000 Threshold (s): 10 Number of Dumps: 3 Press Ctrl-C to end monitoring without terminating the process. [03:45:20 - INFO]: Timed: [03:45:21 - INFO]: Core dump 0 generated: progxyz_time_2020-06-24_03:45:20.351014 [03:45:31 - INFO]: Timed: [03:45:32 - INFO]: Core dump 1 generated: progxyz_time_2020-06-24_03:45:31.351014 [03:45:42 - INFO]: Timed: [03:45:44 - INFO]: Core dump 2 generated: progxyz_time_2020-06-24_03:45:42.351014 $ $ ls -l progxyz_time_2020-06-24_03\:45\:* -rw-r--r--. 1 root root 356848 Jun 24 03:45 progxyz_time_2020-06-24_03:45:20.351014 -rw-r--r--. 1 root root 356848 Jun 24 03:45 progxyz_time_2020-06-24_03:45:31.351014 -rw-r--r--. 1 root root 356848 Jun 24 03:45 progxyz_time_2020-06-24_03:45:42.351014 $ 基于 CPU 和内存使用情况的核心转储ProcDump 还可以让你在测试二进制或进程达到一定的 CPU 或内存阈值时触发核心转储。ProcDump 的手册页显示了调用 ProcDump 时使用的命令行参数：-C：当 CPU 超过或等于指定值时，触发核心转储生成（0 到 100 * nCPU）。-c：当 CPU 小于指定值时，触发核心转储生成（0 到 100 * nCPU）。-M：当内存提交超过或等于指定值（MB）时，触发核心转储生成。-m：当内存提交小于指定值（MB）时，触发核心转储生成。-T：当线程数超过或等于指定值时触发。-F：当文件描述符数量超过或等于指定值时触发。-I：轮询频率，单位为毫秒（默认为 1000）。例如，当给定 PID 的 CPU 使用率超过 70% 时，可以要求 ProcDump 转储核心：procdump -C 70 -n 3 -p 351014 结论ProcDump 是一长串被移植到 Linux 的 Windows 程序中的一个有趣的补充。它不仅为 Linux 用户提供了额外的工具选择，而且可以让 Windows 用户在 Linux 上工作时更有熟悉的感觉。via: https://opensource.com/article/20/7/procdump-linux作者：Gaurav Kamathe 选题：lujun9972 译者：wxy 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
Delve 是能让调试变成轻而易举的事的万能工具包。你上次尝试去学习一种新的编程语言时什么时候？你有没有持之以恒，你是那些在新事物发布的第一时间就勇敢地去尝试的一员吗？不管怎样，学习一种新的语言也许非常有用，也会有很多乐趣。你尝试着写简单的 “Hello, world!”，然后写一些示例代码并执行，继续做一些小的修改，之后继续前进。我敢保证我们都有过这个经历，不论我们使用哪种技术。假如你尝试用一段时间一种语言，并且你希望能够精通它，那么有一些事物能在你的进取之路上帮助你。其中之一就是调试器。有些人喜欢在代码中用简单的 “print” 语句进行调试，这种方式很适合代码量少的简单程序；然而，如果你处理的是有多个开发者和几千行代码的大型项目，你应该使用调试器。最近我开始学习 Go 编程语言了，在本文中，我们将探讨一种名为 Delve 的调试器。Delve 是专门用来调试 Go 程序的工具，我们会借助一些 Go 示例代码来了解下它的一些功能。不要担心这里展示的 Go 示例代码；即使你之前没有写过 Go 代码也能看懂。Go 的目标之一是简单，因此代码是始终如一的，理解和解释起来都很容易。Delve 介绍Delve 是托管在 GitHub 上的一个开源项目。它自己的文档中写道：Delve 是 Go 编程语言的调试器。该项目的目标是为 Go 提供一个简单、全功能的调试工具。Delve 应该是易于调用和易于使用的。当你使用调试器时，事情可能不会按你的思路运行。如果你这样想，那么你不适合用 Delve。让我们来近距离看一下。我的测试系统是运行着 Fedora Linux 的笔记本电脑，Go 编译器版本如下：$ cat /etc/fedora-release Fedora release 30 (Thirty) $ $ go version go version go1.12.17 linux/amd64 $ Golang 安装如果你没有安装 Go，你可以运行下面的命令，很轻松地就可以从配置的仓库中获取。$ dnf install golang.x86_64 或者，你可以在安装页面找到适合你的操作系统的其他安装版本。在开始之前，请先确认已经设置好了 Go 工具依赖的下列各个路径。如果这些路径没有设置，有些示例可能不能正常运行。你可以在 SHELL 的 RC 文件中轻松设置这些环境变量，我的机器上是在 $HOME/bashrc 文件中设置的。$ go env | grep GOPATH GOPATH="/home/user/go" $ $ go env | grep GOBIN GOBIN="/home/user/go/gobin" $ Delve 安装你可以像下面那样，通过运行一个简单的 go get 命令来安装 Delve。go get 是 Golang 从外部源下载和安装需要的包的方式。如果你安装过程中遇到了问题，可以查看 Delve 安装教程。$ go get -u github.com/go-delve/delve/cmd/dlv $ 运行上面的命令，就会把 Delve 下载到你的 $GOPATH 的位置，如果你没有把 $GOPATH 设置成其他值，那么默认情况下 $GOPATH 和 $HOME/go 是同一个路径。你可以进入 go/ 目录，你可以在 bin/ 目录下看到 dlv。$ ls -l $HOME/go total 8 drwxrwxr-x. 2 user user 4096 May 25 19:11 bin drwxrwxr-x. 4 user user 4096 May 25 19:21 src $ $ ls -l ~/go/bin/ total 19596 -rwxrwxr-x. 1 user user 20062654 May 25 19:17 dlv $ 因为你把 Delve 安装到了 $GOPATH，所以你可以像运行普通的 shell 命令一样运行它，即每次运行时你不必先进入它所在的目录。你可以通过 version 选项来验证 dlv 是否正确安装。示例中安装的版本是 1.4.1。$ which dlv ~/go/bin/dlv $ $ dlv version Delve Debugger Version: 1.4.1 Build: $Id: bda606147ff48b58bde39e20b9e11378eaa4db46 $ $ 现在，我们一起在 Go 程序中使用 Delve 来理解下它的功能以及如何使用它们。我们先来写一个 hello.go，简单地打印一条 Hello, world! 信息。记着，我把这些示例程序放到了 $GOBIN 目录下。$ pwd /home/user/go/gobin $ $ cat hello.go package main import "fmt" func main() { fmt.Println("Hello, world!") } $ 运行 build 命令来编译一个 Go 程序，它的输入是 .go 后缀的文件。如果程序没有语法错误，Go 编译器把它编译成一个二进制可执行文件。这个文件可以被直接运行，运行后我们会在屏幕上看到 Hello, world! 信息。$ go build hello.go $ $ ls -l hello -rwxrwxr-x. 1 user user 1997284 May 26 12:13 hello $ $ ./hello Hello, world! $ 在 Delve 中加载程序把一个程序加载进 Delve 调试器有两种方式。在源码编译成二进制文件之前使用 debug 参数第一种方式是在需要时对源码使用 debug 命令。Delve 会为你编译出一个名为 __debug_bin 的二进制文件，并把它加载进调试器。在这个例子中，你可以进入 hello.go 所在的目录，然后运行 dlv debug 命令。如果目录中有多个源文件且每个文件都有自己的主函数，Delve 则可能抛出错误，它期望的是单个程序或从单个项目构建成单个二进制文件。如果出现了这种错误，那么你就应该用下面展示的第二种方式。$ ls -l total 4 -rw-rw-r--. 1 user user 74 Jun 4 11:48 hello.go $ $ dlv debug Type 'help' for list of commands. (dlv) 现在打开另一个终端，列出目录下的文件。你可以看到一个多出来的 __debug_bin 二进制文件，这个文件是由源码编译生成的，并会加载进调试器。你现在可以回到 dlv 提示框继续使用 Delve。$ ls -l total 2036 -rwxrwxr-x. 1 user user 2077085 Jun 4 11:48 __debug_bin -rw-rw-r--. 1 user user 74 Jun 4 11:48 hello.go $ 使用 exec 参数如果你已经有提前编译好的 Go 程序或者已经用 go build 命令编译完成了，不想再用 Delve 编译出 __debug_bin 二进制文件，那么第二种把程序加载进 Delve 的方法在这些情况下会很有用。在上述情况下，你可以使用 exec 命令来把整个目录加载进 Delve 调试器。$ ls -l total 4 -rw-rw-r--. 1 user user 74 Jun 4 11:48 hello.go $ $ go build hello.go $ $ ls -l total 1956 -rwxrwxr-x. 1 user user 1997284 Jun 4 11:54 hello -rw-rw-r--. 1 user user 74 Jun 4 11:48 hello.go $ $ dlv exec ./hello Type 'help' for list of commands. (dlv) 查看 delve 帮助信息在 dlv 提示符中，你可以运行 help 来查看 Delve 提供的多种帮助选项。命令列表相当长，这里我们只列举一些重要的功能。下面是 Delve 的功能概览。(dlv) help The following commands are available: Running the program: Manipulating breakpoints: Viewing program variables and memory: Listing and switching between threads and goroutines: Viewing the call stack and selecting frames: Other commands: Type help followed by a command for full documentation. (dlv) 设置断点现在我们已经把 hello.go 程序加载进了 Delve 调试器，我们在主函数处设置断点，稍后来确认它。在 Go 中，主程序从 main.main 处开始执行，因此你需要给这个名字提供个 break 命令。之后，我们可以用 breakpoints 命令来检查断点是否正确设置了。不要忘了你还可以用命令简写，因此你可以用 b main.main 来代替 break main.main，两者效果相同，bp 和 breakpoints 同理。你可以通过运行 help 命令查看帮助信息来找到你想要的命令简写。(dlv) break main.main Breakpoint 1 set at 0x4a228f for main.main() ./hello.go:5 (dlv) breakpoints Breakpoint runtime-fatal-throw at 0x42c410 for runtime.fatalthrow() /usr/lib/golang/src/runtime/panic.go:663 (0) Breakpoint unrecovered-panic at 0x42c480 for runtime.fatalpanic() /usr/lib/golang/src/runtime/panic.go:690 (0) print runtime.curg._panic.arg Breakpoint 1 at 0x4a228f for main.main() ./hello.go:5 (0) (dlv) 程序继续执行现在，我们用 continue 来继续运行程序。它会运行到断点处中止，在我们的例子中，会运行到主函数的 main.main 处中止。从这里开始，我们可以用 next 命令来逐行执行程序。请注意，当我们运行到 fmt.Println("Hello, world!") 处时，即使我们还在调试器里，我们也能看到打印到屏幕的 Hello, world!。(dlv) continue > main.main() ./hello.go:5 (hits goroutine(1):1 total:1) (PC: 0x4a228f) 1: package main 2: 3: import "fmt" 4: => 5: func main() { 6: fmt.Println("Hello, world!") 7: } (dlv) next > main.main() ./hello.go:6 (PC: 0x4a229d) 1: package main 2: 3: import "fmt" 4: 5: func main() { => 6: fmt.Println("Hello, world!") 7: } (dlv) next Hello, world! > main.main() ./hello.go:7 (PC: 0x4a22ff) 2: 3: import "fmt" 4: 5: func main() { 6: fmt.Println("Hello, world!") => 7: } (dlv) 退出 Delve你随时可以运行 quit 命令来退出调试器，退出之后你会回到 shell 提示符。相当简单，对吗？(dlv) quit $ Delve 的其他功能我们用其他的 Go 程序来探索下 Delve 的其他功能。这次，我们从 golang 教程 中找了一个程序。如果你要学习 Go 语言，那么 Golang 教程应该是你的第一站。下面的程序，functions.go 中简单展示了 Go 程序中是怎样定义和调用函数的。这里，我们有一个简单的把两数相加并返回和值的 add() 函数。你可以像下面那样构建程序并运行它。$ cat functions.go package main import "fmt" func add(x int, y int) int { return x + y } func main() { fmt.Println(add(42, 13)) } $ 你可以像下面那样构建和运行程序。$ go build functions.go && ./functions 55 $ 进入函数跟前面展示的一样，我们用前面提到的一个选项来把二进制文件加载进 Delve 调试器，再一次在 main.main 处设置断点，继续运行程序直到断点处。然后执行 next 直到 fmt.Println(add(42, 13)) 处；这里我们调用了 add() 函数。我们可以像下面展示的那样，用 Delve 的 step 命令从 main 函数进入 add() 函数。$ dlv debug Type 'help' for list of commands. (dlv) break main.main Breakpoint 1 set at 0x4a22b3 for main.main() ./functions.go:9 (dlv) c > main.main() ./functions.go:9 (hits goroutine(1):1 total:1) (PC: 0x4a22b3) 4: 5: func add(x int, y int) int { 6: return x + y 7: } 8: => 9: func main() { 10: fmt.Println(add(42, 13)) 11: } (dlv) next > main.main() ./functions.go:10 (PC: 0x4a22c1) 5: func add(x int, y int) int { 6: return x + y 7: } 8: 9: func main() { => 10: fmt.Println(add(42, 13)) 11: } (dlv) step > main.add() ./functions.go:5 (PC: 0x4a2280) 1: package main 2: 3: import "fmt" 4: => 5: func add(x int, y int) int { 6: return x + y 7: } 8: 9: func main() { 10: fmt.Println(add(42, 13)) (dlv) 使用文件名:行号来设置断点上面的例子中，我们经过 main 函数进入了 add() 函数，但是你也可以在你想加断点的地方直接使用“文件名:行号”的组合。下面是在 add() 函数开始处加断点的另一种方式。(dlv) break functions.go:5 Breakpoint 1 set at 0x4a2280 for main.add() ./functions.go:5 (dlv) continue > main.add() ./functions.go:5 (hits goroutine(1):1 total:1) (PC: 0x4a2280) 1: package main 2: 3: import "fmt" 4: => 5: func add(x int, y int) int { 6: return x + y 7: } 8: 9: func main() { 10: fmt.Println(add(42, 13)) (dlv) 查看当前的栈信息现在我们运行到了 add() 函数，我们可以在 Delve 中用 stack 命令查看当前栈的内容。这里在 0 位置展示了栈顶的函数 add() ，紧接着在 1 位置展示了调用 add() 函数的 main.main。在 main.main 下面的函数属于 Go 运行时，是用来处理加载和执行该程序的。(dlv) stack 0 0x00000000004a2280 in main.add at ./functions.go:5 1 0x00000000004a22d7 in main.main at ./functions.go:10 2 0x000000000042dd1f in runtime.main at /usr/lib/golang/src/runtime/proc.go:200 3 0x0000000000458171 in runtime.goexit at /usr/lib/golang/src/runtime/asm_amd64.s:1337 (dlv) 在帧之间跳转在 Delve 中我们可以用 frame 命令实现帧之间的跳转。在下面的例子中，我们用 frame 实现了从 add() 帧跳到 main.main 帧，以此类推。(dlv) frame 0 > main.add() ./functions.go:5 (hits goroutine(1):1 total:1) (PC: 0x4a2280) Frame 0: ./functions.go:5 (PC: 4a2280) 1: package main 2: 3: import "fmt" 4: => 5: func add(x int, y int) int { 6: return x + y 7: } 8: 9: func main() { 10: fmt.Println(add(42, 13)) (dlv) frame 1 > main.add() ./functions.go:5 (hits goroutine(1):1 total:1) (PC: 0x4a2280) Frame 1: ./functions.go:10 (PC: 4a22d7) 5: func add(x int, y int) int { 6: return x + y 7: } 8: 9: func main() { => 10: fmt.Println(add(42, 13)) 11: } (dlv) 打印函数参数一个函数通常会接收多个参数。在 add() 函数中，它的入参是两个整型。Delve 有个便捷的 args 命令，它能打印出命令行传给函数的参数。(dlv) args x = 42 y = 13 ~r2 = 824633786832 (dlv) 查看反汇编码由于我们是调试编译出的二进制文件，因此如果我们能查看编译器生成的汇编语言指令将会非常有用。Delve 提供了一个 disassemble 命令来查看这些指令。在下面的例子中，我们用它来查看 add() 函数的汇编指令。(dlv) step > main.add() ./functions.go:5 (PC: 0x4a2280) 1: package main 2: 3: import "fmt" 4: => 5: func add(x int, y int) int { 6: return x + y 7: } 8: 9: func main() { 10: fmt.Println(add(42, 13)) (dlv) disassemble TEXT main.add(SB) /home/user/go/gobin/functions.go => functions.go:5 0x4a2280 48c744241800000000 mov qword ptr [rsp+0x18], 0x0 functions.go:6 0x4a2289 488b442408 mov rax, qword ptr [rsp+0x8] functions.go:6 0x4a228e 4803442410 add rax, qword ptr [rsp+0x10] functions.go:6 0x4a2293 4889442418 mov qword ptr [rsp+0x18], rax functions.go:6 0x4a2298 c3 ret (dlv) 单步退出函数另一个功能是 stepout，这个功能可以让我们跳回到函数被调用的地方。在我们的例子中，如果我们想回到 main.main 函数，我们只需要简单地运行 stepout 命令，它就会把我们带回去。在我们调试大型代码库时，这个功能会是一个非常便捷的工具。(dlv) stepout > main.main() ./functions.go:10 (PC: 0x4a22d7) Values returned: ~r2: 55 5: func add(x int, y int) int { 6: return x + y 7: } 8: 9: func main() { => 10: fmt.Println(add(42, 13)) 11: } (dlv) 打印变量信息我们一起通过 Go 教程 的另一个示例程序来看下 Delve 是怎么处理 Go 中的变量的。下面的示例程序定义和初始化了一些不同类型的变量。你可以构建和运行程序。$ cat variables.go package main import "fmt" var i, j int = 1, 2 func main() { var c, python, java = true, false, "no!" fmt.Println(i, j, c, python, java) } $ $ go build variables.go &&; ./variables 1 2 true false no! $ 像前面说过的那样，用 delve debug 在调试器中加载程序。你可以在 Delve 中用 print 命令通过变量名来展示他们当前的值。(dlv) print c true (dlv) print java "no!" (dlv) 或者，你还可以用 locals 命令来打印函数内所有的局部变量。(dlv) locals python = false c = true java = "no!" (dlv) 如果你不知道变量的类型，你可以用 whatis 命令来通过变量名来打印它的类型。(dlv) whatis python bool (dlv) whatis c bool (dlv) whatis java string (dlv) 总结现在我们只是了解了 Delve 所有功能的皮毛。你可以自己去查看帮助内容，尝试下其它的命令。你还可以把 Delve 绑定到运行中的 Go 程序上（守护进程！），如果你安装了 Go 源码库，你甚至可以用 Delve 导出 Golang 库内部的信息。勇敢去探索吧！via: https://opensource.com/article/20/6/debug-go-delve作者：Gaurav Kamathe 选题：lujun9972 译者：lxbwolf 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
了解如何使用 gdb 的一些鲜为人知的功能来检查和修复代码。GNU 调试器（gdb）是一种宝贵的工具，可用于在开发程序时检查正在运行的进程并解决问题。你可以在特定位置（按函数名称、行号等）设置断点、启用和禁用这些断点、显示和更改变量值，并执行所有调试器希望执行的所有标准操作。但是它还有许多其它你可能没有尝试过的功能。这里有五个你可以尝试一下。条件断点设置断点是学习使用 GNU 调试器的第一步。程序在达到断点时停止，你可以运行 gdb 的命令对其进行检查或更改变量，然后再允许该程序继续运行。例如，你可能知道一个经常调用的函数有时会崩溃，但仅当它获得某个参数值时才会崩溃。你可以在该函数的开始处设置一个断点并运行程序。每次碰到该断点时都会显示函数参数，并且如果未提供触发崩溃的参数值，则可以继续操作，直到再次调用该函数为止。当这个惹了麻烦的参数触发崩溃时，你可以单步执行代码以查看问题所在。(gdb) break sometimes_crashes Breakpoint 1 at 0x40110e: file prog.c, line 5. (gdb) run [...] Breakpoint 1, sometimes_crashes (f=0x7fffffffd1bc) at prog.c:5 5 fprintf(stderr, (gdb) continue Breakpoint 1, sometimes_crashes (f=0x7fffffffd1bc) at prog.c:5 5 fprintf(stderr, (gdb) continue为了使此方法更具可重复性，你可以在你感兴趣的特定调用之前计算该函数被调用的次数，并在该断点处设置一个计数器（例如，continue 30 以使其在接下来的 29 次到达该断点时忽略它）。但是断点真正强大的地方在于它们在运行时评估表达式的能力，这使你可以自动化这种测试。break [LOCATION] if CONDITION (gdb) break sometimes_crashes if !f Breakpoint 1 at 0x401132: file prog.c, line 5. (gdb) run [...] Breakpoint 1, sometimes_crashes (f=0x0) at prog.c:5 5 fprintf(stderr, (gdb)条件断点使你不必让 gdb 每次调用该函数时都去问你要做什么，而是让条件断点仅在特定表达式的值为 true 时才使 gdb 停止在该位置。如果执行到达条件断点的位置，但表达式的计算结果为 false，调试器会自动使程序继续运行，而无需询问用户该怎么做。断点命令GNU 调试器中断点的一个甚至更复杂的功能是能够编写对到达断点的响应的脚本。断点命令使你可以编写一系列 GNU 调试器命令，以在到达该断点时运行。我们可以使用它来规避在 sometimes_crashes 函数中我们已知的错误，并在它提供空指针时使其无害地从该函数返回。我们可以使用 silent 作为第一行，以更好地控制输出。否则，每次命中断点时，即使在运行断点命令之前，也会显示堆栈帧。(gdb) break sometimes_crashes Breakpoint 1 at 0x401132: file prog.c, line 5. (gdb) commands 1 Type commands for breakpoint(s) 1, one per line. End with a line saying just "end". >silent >if !f >frame >printf "Skipping call\n" >return 0 >continue >end >printf "Continuing\n" >continue >end (gdb) run Starting program: /home/twaugh/Documents/GDB/prog warning: Loadable section ".note.gnu.property" outside of ELF segments Continuing Continuing Continuing #0 sometimes_crashes (f=0x0) at prog.c:5 5 fprintf(stderr, Skipping call [Inferior 1 (process 9373) exited normally] (gdb)转储二进制内存GNU 调试器内置支持使用 x 命令以各种格式检查内存，包括八进制、十六进制等。但是我喜欢并排看到两种格式：左侧为十六进制字节，右侧为相同字节表示的 ASCII 字符。当我想逐字节查看文件的内容时，经常使用 hexdump -C（hexdump 来自 util-linux 软件包）。这是 gdb 的 x 命令显示的十六进制字节：(gdb) x/33xb mydata 0x404040 <mydata> : 0x02 0x01 0x00 0x02 0x00 0x00 0x00 0x01 0x404048 <mydata+8> : 0x01 0x47 0x00 0x12 0x61 0x74 0x74 0x72 0x404050 <mydata+16>: 0x69 0x62 0x75 0x74 0x65 0x73 0x2d 0x63 0x404058 <mydata+24>: 0x68 0x61 0x72 0x73 0x65 0x75 0x00 0x05 0x404060 <mydata+32>: 0x00如果你想让 gdb 像 hexdump 一样显示内存怎么办？这是可以的，实际上，你可以将这种方法用于你喜欢的任何格式。通过使用 dump 命令以将字节存储在文件中，结合 shell 命令以在文件上运行 hexdump 以及define 命令，我们可以创建自己的新的 hexdump 命令来使用 hexdump 显示内存内容。(gdb) define hexdump Type commands for definition of "hexdump". End with a line saying just "end". >dump binary memory /tmp/dump.bin $arg0 $arg0+$arg1 >shell hexdump -C /tmp/dump.bin >end这些命令甚至可以放在 ~/.gdbinit 文件中，以永久定义 hexdump 命令。以下是它运行的例子：(gdb) hexdump mydata sizeof(mydata) 00000000 02 01 00 02 00 00 00 01 01 47 00 12 61 74 74 72 |.........G..attr| 00000010 69 62 75 74 65 73 2d 63 68 61 72 73 65 75 00 05 |ibutes-charseu..| 00000020 00 |.| 00000021行内反汇编有时你想更多地了解导致崩溃的原因，而源代码还不够。你想查看在 CPU 指令级别发生了什么。disassemble 命令可让你查看实现函数的 CPU 指令。但是有时输出可能很难跟踪。通常，我想查看与该函数源代码的特定部分相对应的指令。为此，请使用 /s 修饰符在反汇编中包括源代码行。(gdb) disassemble/s main Dump of assembler code for function main: prog.c: 11 { 0x0000000000401158 <+0>: push %rbp 0x0000000000401159 <+1>: mov %rsp,%rbp 0x000000000040115c <+4>: sub $0x10,%rsp 12 int n = 0; 0x0000000000401160 <+8>: movl $0x0,-0x4(%rbp) 13 sometimes_crashes(&n); 0x0000000000401167 <+15>: lea -0x4(%rbp),%rax 0x000000000040116b <+19>: mov %rax,%rdi 0x000000000040116e <+22>: callq 0x401126 <sometimes_crashes> [...snipped...]这里，用 info 寄存器查看所有 CPU 寄存器的当前值，以及用如 stepi 这样命令一次执行一条指令，可以使你对程序有了更详细的了解。反向调试有时，你希望自己可以逆转时间。想象一下，你已经达到了变量的监视点。监视点像是一个断点，但不是在程序中的某个位置设置，而是在表达式上设置（使用 watch 命令）。每当表达式的值更改时，执行就会停止，并且调试器将获得控制权。想象一下你已经达到了这个监视点，并且由该变量使用的内存已更改了值。事实证明，这可能是由更早发生的事情引起的。例如，内存已释放，现在正在重新使用。但是它是何时何地被释放的呢？GNU 调试器甚至可以解决此问题，因为你可以反向运行程序！它通过在每个步骤中仔细记录程序的状态来实现此目的，以便可以恢复以前记录的状态，从而产生时间倒流的错觉。要启用此状态记录，请使用 target record-full 命令。然后，你可以使用一些听起来不太可行的命令，例如：reverse-step，倒退到上一个源代码行*reverse-next，它倒退到上一个源代码行，向后跳过函数调用reverse-finish，倒退到当前函数即将被调用的时刻reverse-continue，它返回到程序中的先前状态，该状态将（现在）触发断点（或其他导致断点停止的状态）这是运行中的反向调试的示例：(gdb) b main Breakpoint 1 at 0x401160: file prog.c, line 12. (gdb) r Starting program: /home/twaugh/Documents/GDB/prog [...] Breakpoint 1, main () at prog.c:12 12 int n = 0; (gdb) target record-full (gdb) c Continuing. Program received signal SIGSEGV, Segmentation fault. 0x0000000000401154 in sometimes_crashes (f=0x0) at prog.c:7 7 return *f; (gdb) reverse-finish Run back to call of #0 0x0000000000401154 in sometimes_crashes (f=0x0) at prog.c:7 0x0000000000401190 in main () at prog.c:16 16 sometimes_crashes(0);这些只是 GNU 调试器可以做的一些有用的事情。还有更多有待发现。你最喜欢 gdb 的哪个隐藏的、鲜为人知或令人吃惊的功能？请在评论中分享。via: https://opensource.com/article/19/9/tips-gnu-debugger作者：Tim Waugh 选题：lujun9972 译者：wxy 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出