4. 单步执行与观察
调试器最核心的能力,说白了就是「让代码停下来,然后一步一步看它怎么跑」。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 次时发现了问题——一个野指针在特定条件下被触发了。
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 源码的海洋里,但看多了也就习惯了。
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。
4.6 实战:调试一个 Qt 界面卡顿问题
让我给你讲一个真实的场景。有一次,用户反馈我们的 Qt 程序在点击某个按钮后,界面会卡住 3 秒钟。我用 GDB 挂上进程,在按钮的点击事件处理函数入口设了断点:
(gdb) break MainWindow::onHeavyButtonClicked
(gdb) run
程序停住后,我开始单步执行:
- 先用
next跳过一些初始化代码 - 看到一个
processLargeData()调用,用step钻进去 - 发现里面有个
for循环,循环里调用了QThread::sleep(1)—— 这就是罪魁祸首! - 用
finish跳出函数,确认调用路径 - 用
continue让程序继续跑,验证修复方案
整个过程不到 5 分钟。如果没有这些单步命令,你可能要花半天去猜问题出在哪。
4.7 知识体系图
下面这张图展示了这几个命令之间的关系和使用场景:
4.8 避坑指南
最后,分享几个我踩过的坑:
- 我曾经在调试时一直用
step,结果一头扎进了 Qt 的QApplication::exec()事件循环里,按了半小时都没出来。后来才知道,事件循环这种无限循环的函数,用step进去就是自找麻烦。 - 我曾经在调试多线程程序时,用
continue想让所有线程继续跑,结果发现只有当前线程继续了,其他线程还在断点处等着。GDB 默认只控制当前线程,需要用set scheduler-locking off才能让所有线程一起跑。 - 还有一个坑:在 Qt 的信号槽连接中,用
step钻进emit语句,你会看到一堆 MOC 生成的代码。别慌,这不是你的代码,直接finish出来就好。
这几个命令,说白了就是调试的基本功。熟练了之后,你就能像看动画片一样,看着你的代码一行一行地执行。那种掌控感,真的很爽。
n(next),食指按 s(step),中指按 c(continue)。这样手指不用移动,效率最高。