编辑
2025-09-05
记录知识
0

目录

编译环境和运行环境
示例代码
处理行号问题
简单调试
打印数组的值
打印堆栈中的局部变量
主动调用函数
利用c的jump指令
总结

只要有了良好的调试信息,那么使用gdb调试就会变得非常简单,本文基于此再介绍在操作系统更常见和常使用的调试技巧

编译环境和运行环境

在讨论常用的调试技巧之前,我们必须清楚什么是编译环境,什么是运行环境,基本上所有的操作系统发行时,编译环境一定不是运行环境,操作系统发行方来保证编译环境和运行环境的基础库尽可能一致。

但是对于gdb的调试者而言,这里就会存在一个常见的问题,因为部署在某嵌入式操作系统上的软件的编译环境和运行环境路径不一致,此时即使二进制存在DWARF调试信息,也没办法准确的查看到行号。 下面举个例子说明

示例代码

为了演示这个问题,需要准备example_print.c文件,内容如下

#include <stdio.h> int func1(int a) { int array[100]; int i; for (i = 0; i < 100; i++) array[i] = i; return array[99] * a; } int func2(int a) { int b = 2; return b * func1(a); } int func3(int a) { int b = 3; return b * func2(a); } int main(void) { printf("%d\n", func3(10)); return 0; }

我们将其编译带上DWARF调试信息,如下

gcc example_print.c -g -o print

此时我们将其放置到运行环境中,gdb调试它,如下

scp print kylin@91:~

使用gdb加载时,可以看到DWARF的line info失效了。

# gdb ./print Reading symbols from ./print... (gdb) l 14 example_print.c: 没有那个文件或目录.

处理行号问题

上述这种情况下,我们需要将对应的源码获取,如下

scp example_print.c kylin@91:~

然后使用directory命令加载即可

(gdb) directory ~/. (gdb) l 14 int func2(int a) 15 { 16 int b = 2; 17 return b * func1(a); 18 }

到这里,我们解决了在操作系统上非常常见的无源码行号的问题。

简单调试

当我们准备好对应某个源码行号之后,可以做一些基础的调试,下面分析非常常见的调试方法

打印数组的值

在上述代码中,数组array会被i赋值,如果我们想要分析其中某个值,例如array[55]可以如下

(gdb) u 11 (gdb) p array[55]@1 $4 = {55}

可以看到,这样能够轻松的查找内存的想要的内容

打印堆栈中的局部变量

在上面代码中,每个函数都附带了局部变量,我们可以直接在最底层堆栈中打印其上层堆栈的局部变量,如下

(gdb) frame #0 func1 (a=10) at example_print.c:11 11 return array[99] * a; (gdb) bt #0 func1 (a=10) at example_print.c:11 #1 0x00000055555508f0 in func2 (a=10) at example_print.c:17 #2 0x0000005555550920 in func3 (a=10) at example_print.c:23 #3 0x0000005555550944 in main () at example_print.c:28 (gdb) p func2::b $5 = 2 (gdb) p func3::b $6 = 3 (gdb) p func1::array $7 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99} (gdb) p func1::i $8 = 100

可以看到,代码当前在的func1的栈上,但是gdb能够帮我们计算好其所有堆栈的局部变量,我们直接打印即可。

主动调用函数

在调试大型代码时,有些函数可以自己手动调用,这样就不用重跑一次gdb了,如下

(gdb) set var a=100 (gdb) frame #0 func1 (a=100) at example_print.c:11 11 return array[99] * a; (gdb) call func1 $14 = {int (int)} 0x5555550844 <func1> (gdb) c Continuing. 59400 [Inferior 1 (process 3531) exited normally]

可以看到上面修改了a的值后,虽然我正在func1中,但是我仍可以主动再调用一次func1,从而按照我的目的将func1的返回值扩大10倍。

利用c的jump指令

我们知道c语言支持setjmp来实现代码的跳转,最常见的是goto语句,同样的gdb也提供了jump命令,当我们调试时,要回到之前的语句上,可以直接使用jump指令,下面演示。

首先断点到func2上

(gdb) b func2 Breakpoint 2 at 0x55555508e0: file example_print.c, line 16.

此时代码在b=2上,b的值是随机的栈上的值,如下

(gdb) p b $21 = 85

假设调试代码时,自己敲了n,则代码会到return那行语句上,如下

(gdb) n 17 return b * func1(a);

此时b的值是2,如下

(gdb) p b $22 = 2

此时通过jump命令还能回到16行,如下

(gdb) jump 16 Continuing at 0x55555508e0. Breakpoint 2, func2 (a=10) at example_print.c:16 16 int b = 2;

因为jump和c的setjmp一致,此时b还是2,如下

(gdb) p b $20 = 2

但是代码当前被我修改pc寄存器回到了16行了。

通过这个方法,很方便的在一个函数中记录自己忘记记录的值,从而便于自己调试

总结

本文总结了gdb上的常见技巧,首先解决了所有系统中存在的源码找不到的问题,补全DWARF信息,当调试信息完整之后,就可以轻松的开始调试了。 随后也同时介绍了gdb的几个小技巧,方便自己反复调试,而不是每次忘记一些关键信息的记录,从而导致自己gdb从头来过。