马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
先分清:debug 模式与 release 模式
程序有两种发布方式,debug(调试)模式和 release(发布)模式,它们的定位完全不同。
debug 模式
- 目的:用于开发和测试阶段,让开发者更容易地调试和跟踪程序里的 bug,确保发布前没有大问题。
- 特性:编译时不做过多优化,方便 GDB 这类工具观察程序状态(变量值、函数调用栈等);包含详细的调试信息(符号表);运行时可能带额外检查(数组越界、空指针等);因为没优化还要带调试信息,通常跑得慢。
release 模式
- 目的:部署到生产环境,给用户稳定的体验。
- 特性:编译时做各类优化(代码重排、循环展开、函数内联等)来提速;不带调试信息(符号表),可执行文件更小、加载更快;移除运行时检查以减少开销;跑得快。
Linux 默认是 release 模式
用 gcc/g++ 编出来的二进制默认是 release,里面没有调试信息,gdb 直接上会报"没有调试符号"。要用 gdb 调试,编译时必须加 -g 选项。
怎么验证?两条路:
- 先用裸
gcc Test.c 生成 a.out,gdb 进去会看到"无调试信息"的提示;再 gcc -g Test.c 重新生成,同样的 gdb 提示就没了。
- 看体积:
-g 编出来的文件明显更大,多出来的部分就是调试信息。
更严谨的办法是用 readelf 直接检查 ELF 文件(Linux 可执行文件的标准格式)里的调试段:
readelf -S a.out | grep debug # release 版通常看不到 debug 段
gcc -g Test.c -o test_dbg
readelf -S test_dbg | grep debug # debug 版能看到 .debug_info 等段
再补一条实战建议:调试时用 gcc -g -O0。-O0 关掉优化,源码顺序和机器执行顺序一一对应,断点跳来跳去的情况最少;开着 -O2 调试经常出现"这一行还没执行变量就变了"的诡异现象。
gdb 基本用法
进入调试:gdb 文件名。下面按使用频率把常用命令列一遍。
(gdb) list / l 行号 # 从指定行开始往下显示源码,每次 10 行;不带行号则按上下文续
(gdb) list / l 函数名 # 显示某个函数的源码
(gdb) info line 行号 # 某行在编译后的地址范围
断点
(gdb) break / b 行号 # 在某行打断点
(gdb) break 函数名 # 在函数开头打断点
(gdb) info breakpoints # 查看所有断点
(gdb) disable breakpoints n # 禁用序号为 n 的断点
(gdb) enable breakpoints n # 启用序号为 n 的断点
(gdb) delete / d breakpoints n # 删除序号 n 的断点
(gdb) delete breakpoints # 删除全部断点
注意断点序号:断点的编号是线性递增的,只会越来越大。删掉 3 号再打新断点会得到 5 号、6 号,绝不会复用已经删掉的 3 号。
跑起来
(gdb) run / r # 从头运行;有断点就停在断点处
(gdb) start # 停在 main 第一行,先别急着跑完
(gdb) continue / c # 继续跑到下一个断点、程序结束或异常
(gdb) until 行号 # 运行到第 n 行停下
(gdb) finish # 执行到当前函数返回后停下
(gdb) next / n # 单步:执行下一行;若该行是函数调用,直接把结果显示出来,不进函数
(gdb) step / s # 单步:执行下一行;若该行是函数调用,进到函数内部逐行跑
(gdb) quit / q # 退出 gdb
n 与 s 的区别是面试和调试里最常被问的:一个"跳过函数"、一个"走进函数"。想看某个函数的内部逻辑就用 s,只想确认它返回了啥就用 n。
看数据
(gdb) print / p 变量 # 打印变量值
(gdb) p 表达式 # 可以算表达式,也能调用函数
(gdb) set var 变量=值 # 修改变量的值(用来复现 bug 很顺手)
(gdb) display 变量名 # 跟踪变量,每次停下来都自动显示
(gdb) undisplay n # 取消对序号 n 的跟踪
(gdb) info locals # 看当前栈帧里的局部变量
(gdb) x /nfu 地址 # 按格式看内存,如 x/10c buf 看 10 个字符
看调用栈
(gdb) backtrace / bt # 各级函数调用及参数(查看崩溃时是怎么一路调过来的)
(gdb) bt full # 更完整,含局部变量
(gdb) frame 帧号 # 切到指定栈帧
(gdb) up / down # 往上/往下翻栈帧
排查段错误(Segmentation fault)时,bt 基本是第一步:看栈的最顶层是哪个函数、哪个地址,再结合 info locals 和 x 看当时的数据。
动态观察点
(gdb) watch 变量 # 变量被写时停下(硬件观察点)
(gdb) rwatch 变量 # 变量被读时停下
(gdb) awatch 变量 # 读写都停
适合"某个值莫名其妙被改了"这种场景——与其到处打断点猜,不如直接盯住它。
命令行参数与 core
(gdb) set args 参数1 参数2 # 给被调程序传参
(gdb) run 参数1 参数2 # 一起给也行
(gdb) gdb ./app core # 直接拿 core 文件进去看崩溃现场
core 调试的前提是系统允许生成 core:ulimit -c 不能是 0,kernel.core_pattern 也要指向可写位置。线上服务一般默认关掉,排查崩溃时要先打开。
一个完整调试流程示例
$ gcc -g -O0 myprog.c -o myprog
$ gdb ./myprog
(gdb) b main # 在 main 打断点
(gdb) r # 跑起来
(gdb) l # 看当前位置的源码
(gdb) n # 一行一行走
(gdb) s # 想进函数内部就用 s
(gdb) p argc # 看变量
(gdb) display argc # 挂上跟踪,之后每次停都显示
(gdb) bt # 走到一半想看调用链
(gdb) c # 继续
(gdb) q # 退出
常见踩坑
- 提示 "No symbol table info available":忘了
-g,重新编译再进 gdb。
- 断点打不上 / 行号对不上:源码文件和编译时的那份不一致(比如代码改过但二进制是旧的), gdb 定位的行会漂移。调试前先确认二进制是最新编的。
- 库函数没有符号:调试依赖库时,对应库也要带调试符号(发行版一般提供
-dbgsym 或 -debuginfo 包)。
run 不起来:程序会读 stdin/argv,卡住时先 set args 或 set stdin /dev/null。
- 多线程程序默认跟到主线程:可以用
thread apply all bt 一次性看所有线程的栈,很实用。
小结
gdb 的使用分四块:看代码(list)、控流程(b 打点、r 运行、n/s 单步、c 继续、until/finish)、查数据(p、display、x、watch)、看调用栈(bt、frame)。
前提是编译时带 -g(建议 -g -O0),并且调试的二进制与源码版本一致。掌握这套命令,段错误、越界、野指针这类问题就有了最直接的定位手段——比满屏打日志 高效得多。
(本文由 qidao123.com企服评测-应用市场 编辑提示整理:示例基于 GNU gdb 与 gcc,适用于 Ubuntu / Debian / CentOS 等 Linux 环境,命令行已在标准编译流程中验证。) |