Skip to content

LV093-gdb-反向调试

一、反向调试

1. 什么是反向调试

目前为止我们已经学会了借助 GDB 调试器对代码进行单步调试和断点调试。这 2 种调试方法有一个共同的特点,即调试过程中代码一直都是“正向”执行的(从第一行代码执行到最后一行代码)。这就产生一个问题,如果调试过程中不小心多执行了一次 next、step 或者 continue 命令,又或者想再次查看刚刚程序执行的过程,该怎么办呢?

面对这种情况,很多读者想到的是借助 run 命令重新启动程序,还原之前所做的所有调试工作。的确,这种方式可以解决上面列举的类似问题,只不过比较麻烦。事实上,如果我们使用的是 7.0 及以上版本的 GDB 调试器,还有一种更简单的解决方法,即反向调试。

所谓 反向调试,指的是临时改变程序的执行方向,反向执行指定行数的代码,此过程中 GDB 调试器可以消除这些代码所做的工作,将调试环境还原到这些代码未执行前的状态。

2. 常用命令

表 1 GDB 反向调试的常用命令
命 令 功 能
(gdb) record
(gdb) record btrace
让程序开始记录反向调试所必要的信息,其中包括保存程序每一步运行的结果等等信息。进行反向调试之前(启动程序之后),需执行此命令,否则是无法进行反向调试的。
(gdb) reverse-continue
(gdb) rc
反向运行程序,直到遇到使程序中断的事件,比如断点或者已经退回到 record 命令开启时程序执行到的位置。
(gdb) reverse-step 反向执行一行代码,并在上一行代码的开头处暂停。和 step 命令类似,当反向遇到函数时,该命令会回退到函数内部,并在函数最后一行代码的开头处(通常为 return 0; )暂停执行。
(gdb) reverse-next 反向执行一行代码,并在上一行代码的开头处暂停。和 reverse-step 命令不同,该命令不会进入函数内部,而仅将被调用函数视为一行代码。
(gdb) reverse-finish 当在函数内部进行反向调试时,该命令可以回退到调用当前函数的代码处。
(gdb) set exec-direction mode mode 参数值可以为 forward (默认值)和 reverse:
  • forward 表示 GDB 以正常的方式执行所有命令;
  • reverse 表示 GDB 将反向执行所有命令,由此我们直接只用 step、next、continue、finish 命令来反向调试程序。注意,return 命令不能在 reverse 模式中使用。

注意,表 1 中仅罗列了常用的一些命令,并且仅展示了各个命令最常用的语法格式。有关 GDB 调试器提供的更多支持反向调试的命令,以及各个命令不同的语法格式,可以到 GDB 官网 查看。

3. AVX2指令集冲突问题

后面在ubuntu20.04上调试的时候出现下面的问题了:

shell
(gdb) record 
(gdb) b 10 if n==10
Breakpoint 2 at 0x55555555517c: file 041-gdb-reserve.c, line 10.
(gdb) c
Continuing.
Process record does not support instruction 0xc5 at address 0x7ffff7f48546.
Process record: failed to record execution log.

Program stopped.
__strchrnul_avx2 () at ../sysdeps/x86_64/multiarch/strchr-avx2.S:47
47      ../sysdeps/x86_64/multiarch/strchr-avx2.S: 没有那个文件或目录.

主要是GDB无法记录由AVX2指令集扩展带来的特定指令。核心原因是AVX2指令集与GDB记录功能的冲突:

(1)触发指令 (0xc5): 看到的 0xc5 是一个 VEX 前缀。它就像是AVX(包括AVX2)指令集的“标识符”,告诉CPU接下来要执行一个高级的向量运算指令。在你遇到的这个具体场景中,是 vmovd (将数据移动到XMM寄存器) 或类似的 vmovdqu (移动未对齐的打包整数) 指令导致了冲突。

(2)根本原因: GDB 的 record 功能是按指令逐条记录程序运行状态的,但它的“指令字典”里缺少对较新AVX2指令的支持。这并非新问题,相关错误报告(如PR record/23188)已存在多年,虽然开发者们已开始在补丁中增加支持,但高版本GDB仍未完全支持。

(3)为何进入 __strchrnul_avx2:我的的程序调用了C标准库函数(如 printf),Glibc为了追求极致性能,使用了名为 IFUNC (间接函数) 的机制,在运行时动态检测你的CPU,并自动选择最适合的AVX2优化版函数。所以程序“跑偏”进了这些AVX2函数,从而触发了不支持指令。这就是为什么错误信息会把GDB带入一个不存在的源码路径 ../sysdeps/x86_64/multiarch/strchr-avx2.S,因为那只是Glibc开发用的源码位置,我的系统里当然没有。

  • 解决方案一:通过环境变量禁用AVX优化 (最推荐)

这是最安全、最便捷、无需重新编译的方法,尤其适合调试场景。通过设置环境变量,可以动态地影响 ld.so 动态链接器和Glibc的行为。

对于新版GLibC (2.26及以后),GLIBC 2.26 引入了 GLIBC_TUNABLES 机制,可以更精细地控制这些“可调参数”。

shell
# 在当前 shell 会话中生效
export GLIBC_TUNABLES=glibc.cpu.hwcaps=-AVX_Usable,-AVX2_Usable
# 然后通过 GDB 启动程序,顺序执行
gdb ./a.out
(gdb) start
(gdb) record

兼容性更好的方法 (LD_HWCAP_MASK):这个环境变量对所有Glibc版本都有效,通过屏蔽CPU的某些硬件能力来生效。

shell
# 在当前 shell 会话中生效
export LD_HWCAP_MASK=0x6
# 然后通过 GDB 启动程序,顺序执行
gdb ./a.out
(gdb) start
(gdb) record

0x6 是一个掩码值,其二进制表示为 110,用于屏蔽 AVXAVX2 的CPU能力标志位。

  • 解决方案二:静态链接编译 (特定场景适用)

这种方法可以彻底避免调用动态链接库(如Glibc)里的AVX2优化函数。

shell
# 使用 -static 选项进行编译
gcc -g -static -o a.out your_program.c

但需要注意,这会导致生成的二进制文件体积显著增大。

  • 解决方案三:在GDB内部设置环境变量 (更精确的控制)

如果不希望影响整个Shell环境,可以在GDB内部为被调试程序设置环境变量。

shell
# 进入 GDB 后,先设置环境变量
(gdb) set environment GLIBC_TUNABLES=glibc.cpu.hwcaps=-AVX_Usable,-AVX2_Usable
# 或者使用 LD_HWCAP_MASK
# (gdb) set environment LD_HWCAP_MASK=0x6

# 然后启动程序
(gdb) start
(gdb) record

二、使用实例

1. 测试程序

shell
#include <stdio.h>
int main(int argc, const char *argv[]) {
    int n, sum;
    n = 1;
    sum = 0;
    while (n <= 100) {
        sum = sum + n;
        n = n + 1;
    }
    printf("1 + 2 + ... + 100 = %d\n", sum);
    return 0;
}

我们在 ubuntu 中测试,所以直接用下面的命令编译:

shell
cd ~/workspace/c-learning/02-c-basic/21-debug
gcc 041-gdb-reserve.c -g -static

2. 调试示例

shell
 sumu@virtual-machine:~/workspace/c-learning/02-c-basic/21-debug [main  +1 ~0 -0 !]
$ gdb ./a.out -q
Reading symbols from ./a.out...
(gdb) l    # <=== 1. 查看源码
1       #include <stdio.h>
2       int main(int argc, const char *argv[]) {
3           int n, sum;
4           n = 1;
5           sum = 0;
6           while (n <= 100) {
7               sum = sum + n;
8               n = n + 1;
9           }
10          printf("1 + 2 + ... + 100 = %d\n", sum);
(gdb) b 5    # <=== 2. 第5行打断点
Breakpoint 1 at 0x401ccf: file 041-gdb-reserve.c, line 5.
(gdb) r      # <=== 3. 运行程序
Starting program: /home/sumu/workspace/c-learning/02-c-basic/21-debug/a.out 

Breakpoint 1, main (argc=1, argv=0x7fffffffce28) at 041-gdb-reserve.c:5
5           sum = 0; # <=== 在第5行暂停
(gdb) record         # <=== 4. 开启记录模式
(gdb) b 8 if n==10   # <=== 5. 当n=10的时候在第8行打断点
Breakpoint 2 at 0x401cde: file 041-gdb-reserve.c, line 8.
(gdb) c              # <=== 6. 继续运行
Continuing.

Breakpoint 2, main (argc=1, argv=0x7fffffffce28) at 041-gdb-reserve.c:8
8               n = n + 1;
(gdb) p n           # <=== 7. 运行到n=10,sum=55
$1 = 10
(gdb) p sum
$2 = 55
(gdb) reverse-next # <=== 8. 回退一步,暂停在第7行代码开头
7               sum = sum + n;
(gdb) p n          # <=== 9. 此时n=10,sum=45
$3 = 10
(gdb) p sum
$4 = 45
(gdb) reverse-continue # <=== 10. 反向执行代码,直至上一个断断点,也就是开始记录的起始位置
Continuing.

No more reverse-execution history.
main (argc=1, argv=0x7fffffffce28) at 041-gdb-reserve.c:5
5           sum = 0;
(gdb) p n             # <=== 11. 回到了n=1,sum=0的时候
$5 = 1
(gdb) p sum
$6 = 0
(gdb)