3. 调试与性能调优概述

调试器是嵌入式工程师的听诊器。而 GDB,就是那个最趁手的听诊器。

这一章我们聚焦 GDB 最核心的操作。说实话,我见过不少工程师,IDE 用得飞起,但一进命令行就懵了。其实 GDB 没那么玄乎,掌握几个关键操作,你就能把程序扒个底朝天。

3.1 断点管理:硬件断点 vs 软件断点

断点是调试的基石。但很多人不知道,断点还分硬件和软件两种。它们原理不同,适用场景也不同。

3.1.1 软件断点

软件断点是最常用的。GDB 会在你指定的地址,把原来的指令替换成 0xCC(即 int 3 指令)。程序执行到这里,CPU 就会触发一个异常,GDB 就接管了控制权。

优点:数量不限,随便设。缺点:会修改内存中的指令。所以不能用于 ROM 或 Flash 中的代码。

设置软件断点:
(gdb) break main          # 在函数入口设断点
(gdb) break *0x08048400   # 在指定地址设断点
(gdb) break file.c:42     # 在源文件某行设断点
(gdb) info breakpoints    # 查看所有断点

3.1.2 硬件断点

硬件断点依赖 CPU 的调试寄存器。ARM Cortex-M 系列通常有 4-8 个硬件断点寄存器。它不修改内存,所以可以用于 ROM 中的代码。

我曾在调试一个 Bootloader 时遇到过这种情况——代码在 Flash 里,软件断点根本写不进去。当时我折腾了半天,最后才想起来用硬件断点。嗯,从那以后我养成了习惯:调试只读区域代码,第一反应就是硬件断点。

设置硬件断点:
(gdb) hbreak main          # 硬件断点
(gdb) hbreak *0x08000000   # 在 ROM 地址设硬件断点
(gdb) info breakpoints     # 查看时,hw breakpoint 会标注类型

3.1.3 断点类型对比

特性 软件断点 硬件断点
原理 替换指令为 0xCC 使用调试寄存器
数量限制 无限制 通常 4-8 个
适用区域 RAM RAM / ROM / Flash
副作用 修改内存 无修改
调试器依赖 仅需 GDB 需要硬件调试器支持

3.2 单步执行与反汇编

单步执行,说白了就是让程序一步一步走。但这里有个坑:你是在源码级别单步,还是在指令级别单步?

3.2.1 源码级单步

(gdb) next    # 下一行,不进入函数
(gdb) step    # 下一行,进入函数
(gdb) finish  # 执行完当前函数,返回

我个人习惯用 next 跳过库函数调用,用 step 进入自己写的函数。这样效率高。

3.2.2 指令级单步与反汇编

有时候源码级单步不够用。比如你怀疑编译器优化出了问题,或者你在调试中断服务程序。这时候就得看汇编了。

反汇编与指令级单步:
(gdb) disassemble main          # 反汇编 main 函数
(gdb) disassemble $pc, $pc+32   # 反汇编当前地址附近 32 字节
(gdb) stepi                     # 单步执行一条汇编指令
(gdb) nexti                     # 单步执行,跳过函数调用

我曾经调试过一个诡异的死机问题。源码看着完全正常,但程序就是跑飞了。我反汇编一看,发现编译器把两个相邻的变量优化到了同一个寄存器里。嗯,这就是典型的优化坑。从那以后,我调试疑难杂症时,一定会看一眼反汇编。

3.3 内存与寄存器查看

断点停下来了,然后呢?你得看看程序的状态。内存和寄存器,就是程序的全部状态。

3.3.1 查看寄存器

(gdb) info registers        # 查看所有寄存器
(gdb) info registers r0 r1  # 查看指定寄存器
(gdb) print $r0             # 打印 r0 的值
(gdb) print/x $r0           # 十六进制打印

3.3.2 查看内存

(gdb) x/10x 0x20000000      # 查看 10 个十六进制字
(gdb) x/20b 0x20000000      # 查看 20 个字节
(gdb) x/5i  $pc             # 查看当前地址的 5 条指令
(gdb) x/s 0x20000000        # 查看字符串

x 命令的格式是 x/[数量][格式][单位] 地址。格式有 x(十六进制)、d(十进制)、s(字符串)、i(指令)。单位有 b(字节)、h(半字)、w(字)。

实用技巧:

用 display 命令可以设置自动显示的变量或寄存器。每次单步后,GDB 会自动打印它们。

(gdb) display/x $r0
(gdb) display/4x 0x20000000
(gdb) info display   # 查看所有自动显示项

3.4 Core Dump 分析

程序崩溃了,但现场没保留?那是你没用 Core Dump。

Core Dump 是程序崩溃时,操作系统把内存映像保存下来的文件。你可以用 GDB 加载它,就像调试一个活着的程序一样——查看调用栈、变量值、寄存器状态。

3.4.1 生成 Core Dump

在 Linux 系统中,需要先设置 Core Dump 文件大小限制:

$ ulimit -c unlimited        # 取消 Core Dump 大小限制
$ echo "/tmp/core.%p" > /proc/sys/kernel/core_pattern  # 设置 Core Dump 路径

在嵌入式系统中,通常需要内核支持 CONFIG_ELF_CORE 选项,并且要有足够的存储空间。

3.4.2 分析 Core Dump

$ arm-none-eabi-gdb my_program.elf /tmp/core.1234
(gdb) bt                # 查看调用栈
(gdb) info registers    # 查看崩溃时的寄存器状态
(gdb) frame 2           # 切换到第 2 层调用栈
(gdb) print local_var   # 查看局部变量
注意:

Core Dump 分析的关键是:可执行文件必须包含调试信息(编译时加 -g 选项),并且 Core Dump 文件必须与可执行文件匹配。版本对不上,分析结果就是错的。

我记得有一次,客户现场的设备频繁重启。我远程拿到 Core Dump 文件,加载到 GDB 一看——调用栈停在了一个野指针赋值的地方。再查那个指针的来源,发现是一个全局变量被意外覆盖了。整个过程没花半小时。如果没有 Core Dump,我可能得飞到现场,接上 JTAG,复现几个小时才能找到问题。

3.5 本章知识结构

下面这张图总结了 GDB 核心操作的知识体系。你可以把它当作调试时的快速参考。

GDB 核心操作知识结构图 GDB 核心操作 断点管理 单步执行与反汇编 内存与寄存器查看 Core Dump 分析 软件断点 硬件断点 next/step stepi/nexti x 命令 info reg 生成配置 bt/frame 硬件断点用于 ROM/Flash 软件断点数量无限制 display 命令自动跟踪 反汇编看优化问题 远程分析崩溃现场

这张图把 GDB 的核心操作分成了四个模块。断点管理是入口,单步执行是过程,内存与寄存器查看是状态检查,Core Dump 分析是事后复盘。调试时,你就在这四个模块之间来回切换。

我的调试习惯:

先设断点,跑到可疑位置。然后单步跟踪,同时用 display 盯着关键变量。如果程序崩溃了,第一时间看 Core Dump。这套流程,我用了十年,基本没失手过。