4. 单步执行与观察:step into、step over、step out、continue、run

调试器最核心的能力,说白了就是「让代码停下来,然后一步一步看它怎么跑」。GDB 里这几个命令,就是控制代码执行节奏的油门和刹车。我刚开始用 GDB 时,就靠这几个命令活下来的。

4.1 run —— 让程序跑起来

run 是调试的起点。你在 GDB 里加载完程序后,敲 run,程序就开始执行了。如果程序需要命令行参数,直接跟在后面就行:

(gdb) run --config=debug.conf --port=8080

嗯,这里要注意:如果你之前已经跑过一次,再敲 run,GDB 会问你是否要重新启动。我个人习惯直接敲 y 确认,省得它卡在那。

小技巧: 如果程序需要标准输入,可以用 run < input.txt 重定向。我在调试一个 Qt 网络服务时,经常用这种方式批量发送测试请求。

4.2 continue —— 继续执行到下一个断点

continue(简写 c)的意思是:「别停,继续跑,直到遇到下一个断点或者程序结束」。这个命令在循环里特别有用——你设了一个断点,然后敲 c,每次循环都会停一次。

我曾经调试过一个 Qt 的定时器事件处理函数,里面有个诡异的崩溃。我在函数入口设了断点,然后连续敲了十几次 c,终于在第 17 次时发现了问题——一个野指针在特定条件下被触发了。

核心区别: run 是「从头开始跑」,continue 是「从当前位置继续跑」。别搞混了。

4.3 step into —— 钻进函数内部

step(简写 s)会执行当前行,如果当前行是一个函数调用,就「钻」进去,停在函数的第一行。这是理解代码逻辑的利器。

举个例子,假设你有这样一段 Qt 代码:

void MainWindow::onButtonClicked() {
    QString result = processData(inputData);  // 停在这里,敲 s
    updateUI(result);
}

QString processData(const QString& data) {
    // 敲 s 后,会停在这里
    QString cleaned = data.trimmed();
    return cleaned.toUpper();
}

当你停在 processData(inputData) 这一行时,敲 s,GDB 就会跳进 processData 函数内部。我个人习惯在遇到不熟悉的第三方库函数时,用 s 进去看一眼——虽然有时候会一头扎进 Qt 源码的海洋里,但看多了也就习惯了。

注意: 如果函数没有调试符号(比如 Release 编译的 Qt 库),step 可能会直接跳过,或者跳进一堆汇编代码里。这时候别慌,敲 finish 就能出来。

4.4 step over —— 跳过函数调用

next(简写 n)和 step 很像,但它不会钻进函数内部。如果当前行是函数调用,next 会直接执行完整个函数,停在下一行。

你想想看,什么时候用 next?当你确定这个函数没问题,或者你只关心调用结果时。比如:

void MainWindow::saveDocument() {
    validateInput();       // 这个函数你确定没问题,敲 n 跳过
    QString content = editor->toPlainText();  // 停在这里
    file->write(content); // 再敲 n,到这里
}

我在项目中遇到过一种情况:调试一个 Qt 的绘图函数,里面调用了十几个辅助函数。如果每个都用 step 钻进去,一天都看不完。这时候 next 就是救星——只关注当前函数的逻辑,不关心内部实现。

命令 简写 行为 适用场景
step s 钻进函数内部 想了解函数实现细节
next n 跳过函数调用 只关心调用结果
continue c 继续执行到下一个断点 跳过大量无关代码
run r 从头启动程序 重新开始调试

4.5 step out —— 从函数里跳出来

finish(没有简写)会执行完当前函数,然后停在该函数的调用处。说白了就是「我不想看了,让我出去」。

为什么需要这个?举个例子:你不小心用 step 钻进了一个 500 行的函数,看了几行发现这不是你要找的问题。这时候敲 finish,GDB 会执行完剩下的所有代码,然后回到调用处。

我曾经有一次调试 Qt 的信号槽机制,用 step 钻进了 QMetaObject::activate 的内部实现。那叫一个深啊,层层嵌套。我果断敲了 finish,连续按了三次才回到自己的代码里。嗯,从那以后我学乖了——不确定的函数先用 next 跳过,真想看再用 step。

一句话总结: step 是「进去看看」,next 是「跳过不看」,finish 是「看够了出来」。

4.6 实战:调试一个 Qt 界面卡顿问题

让我给你讲一个真实的场景。有一次,用户反馈我们的 Qt 程序在点击某个按钮后,界面会卡住 3 秒钟。我用 GDB 挂上进程,在按钮的点击事件处理函数入口设了断点:

(gdb) break MainWindow::onHeavyButtonClicked
(gdb) run

程序停住后,我开始单步执行:

  1. 先用 next 跳过一些初始化代码
  2. 看到一个 processLargeData() 调用,用 step 钻进去
  3. 发现里面有个 for 循环,循环里调用了 QThread::sleep(1) —— 这就是罪魁祸首!
  4. 用 finish 跳出函数,确认调用路径
  5. 用 continue 让程序继续跑,验证修复方案

整个过程不到 5 分钟。如果没有这些单步命令,你可能要花半天去猜问题出在哪。

4.7 知识体系图

下面这张图展示了这几个命令之间的关系和使用场景:

GDB 单步执行命令体系 程序执行流程 断点命中,程序暂停 step (s) 钻进函数内部 next (n) 跳过函数调用 continue (c) 继续到下一断点 finish 从函数跳出 或

4.8 避坑指南

最后,分享几个我踩过的坑:

  • 我曾经在调试时一直用 step,结果一头扎进了 Qt 的 QApplication::exec() 事件循环里,按了半小时都没出来。后来才知道,事件循环这种无限循环的函数,用 step 进去就是自找麻烦。
  • 我曾经在调试多线程程序时,用 continue 想让所有线程继续跑,结果发现只有当前线程继续了,其他线程还在断点处等着。GDB 默认只控制当前线程,需要用 set scheduler-locking off 才能让所有线程一起跑。
  • 还有一个坑:在 Qt 的信号槽连接中,用 step 钻进 emit 语句,你会看到一堆 MOC 生成的代码。别慌,这不是你的代码,直接 finish 出来就好。

这几个命令,说白了就是调试的基本功。熟练了之后,你就能像看动画片一样,看着你的代码一行一行地执行。那种掌控感,真的很爽。

我的习惯: 调试时左手放在键盘上,右手握鼠标。左手拇指按 n(next),食指按 s(step),中指按 c(continue)。这样手指不用移动,效率最高。
可可精品资料库