GDB 调试指南(第 4 章):单步调试与查看程序状态
← 上一章:运行与参数传递 | 目录 | 下一章:命令速查与常见误区 →
第 4 章 单步调试与查看程序状态
程序停在断点之后,接下来要做两件事:一步一步往下走,以及随时看清楚此刻的变量、调用栈长什么样。这一章把这两块内容讲完整——前半部分是单步执行家族,后半部分是新手必须会用但原笔记没有覆盖的“状态查看三件套”:print、backtrace、frame。
4.1 源码级单步:next / step
(gdb) n # next:单步执行一行源码,遇到函数调用时“跳过”,不进入
(gdb) s # step:单步执行一行源码,遇到函数调用时会“进入”
两者的区别只体现在当前这一行包含函数调用的时候:
next会把这个函数当成一个整体执行完,直接停在下一行,不关心函数内部发生了什么;step会真的“走进去”,停在被调用函数的第一行。
【新手提示】选择原则很简单:如果你怀疑 bug 就在这个函数调用里面,用
step进去看;如果你确定这个函数没问题(比如调用的是printf、标准库函数),用next跳过去,省得在一堆不相干的库代码里绕远路。step踏进没有调试信息的库函数时,会看到一堆没有源码的汇编或者直接提示没有源码,这时候可以用下面 4.3 节的finish立即跳出来。
4.2 指令级单步:nexti / stepi
nexti(简写 ni)和 stepi(简写 si)是 next/step 的汇编指令级版本:每次只执行一条机器指令,而不是一整行源码。一行 C/C++ 代码通常会编译成好几条汇编指令,适合需要研究底层细节(比如排查编译器优化行为、看具体某条指令的效果)时使用。
(gdb) disassemble
=> 0x0000555555555458 <+29>: mov %fs:0x28,%rax
(gdb) ni
0x0000555555555461 36 int main(int argc, char *argv[]) {
(gdb)
0x0000555555555465 36 int main(int argc, char *argv[]) {
【小贴士】直接敲回车(不输入任何命令)会重复上一条命令,这是 GDB 的通用规则,不止对
ni/si有效,对n/s/c等命令同样适用,连续单步时很常用。
nexti 与 stepi 的关系,和 next/step 完全对应:
nexti(ni):单步一条指令,遇到call指令会跳过整个被调用函数(不进入其内部单步);stepi(si):单步一条指令,遇到call指令会进入被调用函数内部。
只有在当前这条指令不是函数调用时,两者效果才相同;一旦踩到 call,行为就会分叉。调试到库函数调用(比如 printf)附近这个区别尤其明显——用 ni 会直接跳过整个库函数调用,用 si 则会一头钻进汇编实现里。
4.3 跳出当前函数:finish,跳出当前循环:until
单步一行一行走很精细,但有时候你只是想“赶紧跳出去”,不想在当前函数或者循环体里一直按 n:
(gdb) finish # 执行到当前函数返回为止,并显示返回值
Run till exit from #0 testFun (num=9988) at main.cpp:26
main () at main.cpp:46
46 testFun("xxxx");
Value returned is $1 = -2
finish 特别适合“不小心用 step 进了一个不想看的函数,想立即退出来”的场景,而且会顺带把函数的返回值打印出来,比手动单步到 return 那一行再查看方便得多。
(gdb) until # 跑到比当前行号更大的下一行为止,常用来跳出循环体
until(不带参数时)在循环体内部使用,可以一次性跑完当前这一轮循环剩下的部分,直接停在循环之后的代码,不用一圈一圈按 next 慢慢走出来。
4.4 查看变量:print(p)
print 是用得最频繁的命令,语法上它接受的是一个表达式,不仅仅是单个变量名:
(gdb) p a # 普通变量
$1 = 123
(gdb) p a + 1 # 表达式也可以
$2 = 124
(gdb) p buf # 字符数组,GDB 会按字符串方式显示
$3 = "456\000\000..."
(gdb) p str # std::string 等有调试信息的对象,直接显示内容
$4 = "abc"
(gdb) p &a # 取地址
$5 = (int *) 0x7fffffffde6c
(gdb) p *ptr # 解引用指针
$6 = 123
(gdb) p arr[2] # 数组下标
$7 = 42
(gdb) p obj.field # 访问成员变量
$8 = 10
每次 print 的结果都会被编号保存($1、$2……),之后可以直接拿 $1 这种编号当变量用,比如 p $1 + 10,在做连续计算时很方便。
4.4.1 按不同进制/格式打印
print 支持在命令后面加 /格式 来控制输出的进制或形式:
(gdb) p/x a # 十六进制
$1 = 0x7b
(gdb) p/d a # 十进制(默认就是,显式指定用于切回)
$2 = 123
(gdb) p/t a # 二进制
$3 = 1111011
(gdb) p/c a # 按字符显示
$4 = 123 '{'
【新手提示】排查“值是不是被意外改写”这类问题时,
p/x看十六进制往往比十进制更容易发现异常(比如看出某段内存被错误地填成了0xcdcdcdcd之类的特征值)。
4.5 查看调用栈:backtrace(bt)、frame、up/down
程序停下来的时候,当前函数很可能是被一层一层调用过来的(main 调 A,A 调 B,B 里出了问题)。想知道“我是怎么走到这里的”,就要看调用栈:
(gdb) bt
#0 testFun (num=9988) at main.cpp:26
#1 0x0000555555555461 in main (argc=1, argv=0x7fffffffdea8) at main.cpp:46
#0 是当前所在的最内层函数,往下编号越大,表示调用链越往外层(越接近 main)。
如果想切换到某一层去看那一层的变量,用 frame(简写 f)加上编号:
(gdb) frame 1 # 切换到 #1 这一层(main 函数那一层)
#1 0x0000555555555461 in main (argc=1, argv=0x7fffffffdea8) at main.cpp:46
46 testFun(MAX);
(gdb) p argc # 现在可以直接查看 main 这一层的局部变量了
$1 = 1
切换过去之后,print、list 等命令都是基于“你当前所在的那一层”,up 和 down 可以不输编号、按相对关系上下移动:
(gdb) up # 往外层移动一层(朝 main 方向)
(gdb) down # 往内层移动一层(朝当前执行点方向)
【新手提示】
backtrace是排查“这个函数到底是被谁调用的”“为什么会执行到这里”最直接的手段,尤其是在崩溃现场(段错误触发后 GDB 会自动停在出错的那一行),第一件事通常就是敲一下bt,看清楚整条调用链,再决定去哪一层查变量。
4.6 【常见误区】单步进入库函数卡住了怎么办
用 step 单步时,如果不小心“进入”了一个没有调试信息的库函数(比如 std::string 内部实现、libc 函数),屏幕上会变成一堆看不懂的地址和汇编提示,甚至感觉“卡住了”。
遇到这种情况,不要慢,也不需要退出重来:
- 直接敲
finish,会一路跑完这个库函数,回到调用它的那一行,并显示返回值; - 下次同样的位置想避免再进去,改用
next而不是step,从一开始就跳过它。
4.7 小结
next/step是源码级单步,区别在于遇到函数调用是跳过还是进入;nexti/stepi是对应的指令级版本。finish用于“跳出当前函数”,until用于“跳出当前循环”,都是为了避免逐行按next太慢。print接受任意表达式,支持/x、/d、/t、/c等格式控制,是查看程序状态最核心的命令。backtrace看调用链,frame/up/down在调用链的不同层级之间切换查看变量,排查“为什么会走到这里”必备。

浙公网安备 33010602011771号