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 在调用链的不同层级之间切换查看变量,排查“为什么会走到这里”必备。

← 上一章:运行与参数传递 | 目录 | 下一章:命令速查与常见误区 →

posted @ 2026-10-10 08:29  BlackSnow  阅读(12)  评论(0)    收藏  举报