标签 调试 下的文章

从调试器中获取函数调用关系。

在我的 上一篇文章 中,我展示了如何使用 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 可执行文件的反汇编代码,这样你可以查找 f2cmain 函数:

$ objdump -d f2c > f2c.dis

f2c.dis 中找到以下信息来看看 f2cmain 函数的执行位置:

0000000000401060 <main>:
0000000000401190 <f2c>:

在许多情况下,二进制文件中的所有函数在执行函数的第一条指令之前都使用相同的 CIE 来定义初始条件。 在此示例中, f2cmain 都使用以下 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 到(但不包括)0x401097main 函数:

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_locDW_CFA_def_cfa_offsetmain401060 处的第一条指令匹配。 这会将堆栈指针向下调整 0x18(24 个字节)。 CFA 没有改变位置,但堆栈指针改变了,因此 CFA 在 401064 处的正确计算是 rsp+32。 这就是这段代码中序言指令的范围。 以下是 main 中的前几条指令:

0000000000401060 <main>:
  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,并覆盖 0x411900x4011c3 的范围:

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+16DW_CFA_offset 表示 %rbx 中的初始值现在位于 CFA-16(堆栈顶部)。

查看这个 fc2 反汇编代码,可以看到 push 用于将 %rbx 保存到堆栈中。 在代码生成中省略帧指针的优点之一是可以使用 pushpop 等紧凑指令在堆栈中存储和检索值。 在这种情况下,保存 %rbx 是因为 %rbx 用于向 printf 函数传递参数(实际上转换为 puts 调用),但需要保存传递到函数中的 f 初始值以供后面的计算使用。4011aeDW_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。

Ferris the crab under the sea, unofficial logo for Rust programming language

在我上一篇 关于 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 命令来代替 stepstepi 一次只执行一条指令。当使用 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 选项,重新编译示例程序后,你可以使用行号在 mainbar 调用上设置断点,然后再单步执行 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 命令,这样就不需要以上的序列了。为了证明这一点,你需要像以前一样将断点设置在 mainbar 调用上。然后在 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 / ltrace
  • tcpdump / 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 命令行程序。

 title=

有时在管理一个网站时,事情会被搞得一团糟。你可能会删除一些陈旧的内容,用重定向到其他页面来代替。后来,在做了其他改动后,你发现一些网页变得完全无法访问了。你可能会在浏览器中看到一个错误:“该页面没有正确重定向”,并建议你检查你的 cookie。

 title=

调试这种情况的一个方法是使用 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 调试器来解决你的代码问题。

 title=

GNU 调试器常以它的命令 gdb 称呼它,它是一个交互式的控制台,可以帮助你浏览源代码、分析执行的内容,其本质上是对错误的应用程序中出现的问题进行逆向工程。

故障排除的麻烦在于它很复杂。GNU 调试器 并不是一个特别复杂的应用程序,但如果你不知道从哪里开始,甚至不知道何时和为何你可能需要求助于 GDB 来进行故障排除,那么它可能会让人不知所措。如果你一直使用 printechoprintf 语句来调试你的代码,当你开始思考是不是还有更强大的东西时,那么本教程就是为你准备的。

有错误的代码

要开始使用 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 命令给了你一个线索,即你的 alphabeta 变量是整数,这可能会唤起你对 printf 语法的记忆,使你意识到在你的 printf 语句中,你必须使用 %d 来代替 %s。做了这个改变,就可以让应用程序按预期运行,没有更明显的错误存在。

当代码编译后发现有 bug 存在时,特别令人沮丧,但最棘手的 bug 就是这样,如果它们很容易被发现,那它们就不是 bug 了。使用 GDB 是猎取并消除它们的一种方法。

下载我们的速查表

生活的真相就是这样,即使是最基本的编程,代码也会有 bug。并不是所有的错误都会导致应用程序无法运行(甚至无法编译),也不是所有的错误都是由错误的代码引起的。有时,bug 是基于一个特别有创意的用户所做的意外的选择组合而间歇性发生的。有时,程序员从他们自己的代码中使用的库中继承了 bug。无论原因是什么,bug 基本上无处不在,程序员的工作就是发现并消除它们。

GNU 调试器是一个寻找 bug 的有用工具。你可以用它做的事情比我在本文中演示的要多得多。你可以通过 GNU Info 阅读器来了解它的许多功能:

$ info gdb

无论你是刚开始学习 GDB 还是专业人员的,提醒一下你有哪些命令是可用的,以及这些命令的语法是什么,都是很有帮助的。


via: https://opensource.com/article/21/3/debug-code-gdb

作者:Seth Kenlon 选题:lujun9972 译者:wxy 校对:wxy

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