3、内存分析工具入门:使用size命令、nm、objdump分析固件内存布局
做嵌入式开发,说白了就是跟有限的内存资源打交道。你写了几万行代码,编译出来多大?哪些函数占了空间?全局变量都放在哪了?
我刚开始做MQTT客户端移植那会儿,就吃过这个亏。代码写完了,一烧录,芯片直接罢工。后来才发现,是.bss段太大,把RAM撑爆了。从那以后,我养成了一个习惯——每次编译完,先跑一遍内存分析工具。
今天咱们就聊聊三个最常用的工具:size、nm 和 objdump。它们能帮你把固件的内存布局看得清清楚楚。
3.1 size命令:一眼看穿固件大小
size 命令是最简单的。你编译完一个 .elf 文件,直接敲 size 就能看到三段信息:text、data、bss。
$ arm-none-eabi-size mqtt_client.elf
text data bss dec hex filename
45632 1284 10240 57156 df44 mqtt_client.elf
这三列分别代表什么?
- text:代码段。存放你的函数指令、常量字符串。这部分通常烧写在Flash里。
- data:已初始化的全局变量和静态变量。它们初始值在Flash,运行时拷贝到RAM。
- bss:未初始化的全局变量和静态变量。运行时在RAM里,默认清零。
嗯,这里要注意一点:dec 列是 text+data+bss 的总和,但它不代表实际占用的Flash或RAM大小。为什么?因为 data 段既占Flash(存初始值),又占RAM(运行时)。实际Flash占用是 text+data,RAM占用是 data+bss。
实际内存占用公式:
Flash = text + data
RAM = data + bss
我个人习惯在 Makefile 里加一行编译后自动执行 size,这样每次编译完扫一眼,心里就有数了。
# Makefile 片段
$(TARGET).elf: $(OBJS)
$(LD) $(LDFLAGS) -o $@ $^
$(SIZE) $@
3.2 nm命令:谁在吃内存?逐个点名
size 只能看个总数。想知道具体哪个函数、哪个变量占了多少空间?那就得上 nm。
$ arm-none-eabi-nm -S --size-sort mqtt_client.elf | head -20
00008000 00001234 T mqtt_publish
00009234 00000890 T mqtt_subscribe
00009ac4 00000456 T mqtt_connect
20000000 00001000 B mqtt_rx_buffer
20001000 00001000 B mqtt_tx_buffer
20002000 00000200 D mqtt_config
这里 -S 显示符号大小,--size-sort 按大小排序。输出格式是:地址、大小、符号类型、符号名。
符号类型怎么看?
- T/t:代码段中的函数(T是全局,t是静态)
- D/d:已初始化的数据段
- B/b:未初始化的bss段
- U:未定义符号(等着链接器去填)
我在项目中遇到过一件事:MQTT客户端的接收缓冲区设成了 8KB,但实际业务根本用不了那么多。用 nm 一查,mqtt_rx_buffer 赫然排在第一位。后来改成 2KB,RAM 瞬间省出 6KB。你想想看,这种优化是不是比抠代码效率高多了?
小技巧: 用 nm -C 可以 demangle C++ 符号名,让函数名更可读。如果是 C 代码,不需要加这个选项。
3.3 objdump:反汇编看细节
objdump 是个瑞士军刀。它能反汇编、看段信息、看符号表。咱们做内存分析,主要用它的两个功能:
3.3.1 查看段详细信息
$ arm-none-eabi-objdump -h mqtt_client.elf
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0000B264 08000000 08000000 00010000 2**2
1 .data 00000504 20000000 0800B264 00020000 2**2
2 .bss 00002800 20000504 20000504 00020504 2**2
3 .rodata 00000120 0800B264 0800B264 0001B264 2**2
这里 VMA(虚拟地址)是运行时地址,LMA(加载地址)是烧录地址。对于 .text 和 .rodata,两者相同,都在Flash。对于 .data,VMA在RAM,LMA在Flash——启动代码会把它从Flash拷贝到RAM。
我曾经调试过一个诡异的问题:MQTT客户端在某种情况下发不出数据。反汇编一看,发现某个关键函数被优化掉了,因为链接器觉得它没被调用。用 objdump -d 反汇编确认后,加了个 __attribute__((used)) 才搞定。
3.3.2 反汇编特定函数
$ arm-none-eabi-objdump -d mqtt_client.elf | grep -A 50 "<mqtt_publish>:"
08000000 <mqtt_publish>:
8000000: b508 push {r3, lr}
8000002: 4b0c ldr r3, [pc, #48]
8000004: 681b ldr r3, [r3, #0]
8000006: 2b00 cmp r3, #0
8000008: d001 beq.n 800000e <mqtt_publish+0xe>
...
反汇编能帮你确认编译器生成的代码是否符合预期。比如你写了个 memcpy,编译器有没有内联?函数调用开销多大?这些都能从汇编代码里看出来。
注意: 反汇编时加上 -S 选项,可以混排源代码和汇编,对照着看更直观。但前提是编译时加了 -g 调试信息。
3.4 实战:三步定位内存问题
好了,工具都认识了。咱们来个实战流程,看看怎么用这三个工具定位内存问题。
- 先用 size 看全局:编译完跑 size,确认 text、data、bss 是否在芯片规格内。如果超了,继续下一步。
- 再用 nm 找大户:按大小排序,找出占用最大的函数和变量。重点关注 bss 段的大数组、data 段的大结构体。
- 最后用 objdump 确认:对可疑的函数反汇编,看有没有冗余代码。或者查看段信息,确认链接脚本是否合理。
我记得有一次帮客户优化 MQTT 固件,他们抱怨 RAM 不够用。我按这个流程走了一遍,发现 .bss 里有个 4KB 的日志缓冲区,但产品量产时日志功能是关闭的。改成条件编译后,RAM 占用直接降了 30%。
核心思路: 先宏观,后微观。不要一上来就抠代码,先用工具找到瓶颈在哪。
3.5 常见陷阱与避坑指南
用这些工具时,有几个坑我踩过,分享给你:
- 陷阱一:只看 size 不看链接脚本。size 显示的是默认布局,如果你的链接脚本自定义了段位置,实际占用可能不同。比如把 .rodata 放到 RAM 里,那 RAM 占用就要加上它。
- 陷阱二:nm 显示的符号大小不包含子函数。比如一个函数调用了
memcpy,nm 只显示这个函数本身的指令大小,不包含memcpy的代码。要看整体,得用bloaty或map文件。 - 陷阱三:优化等级影响分析结果。用
-O0编译出来的 size 可能比-Os大好几倍。我建议用最终发布的优化等级来分析。
我曾经在 -O0 下分析内存,发现一个函数占了 2KB,觉得太大了。结果改成 -Os 后,它只剩 400 字节。嗯,白忙活一场。所以记住:用发布配置做分析。
3.6 小结
这三个工具,size 看总数,nm 看明细,objdump 看细节。组合使用,基本能把固件的内存布局摸个底朝天。
下一章咱们会深入链接脚本,看看怎么通过修改链接脚本来精细控制内存布局。到时候这些工具还会派上用场。
公众号:蓝海资料掘金营,微信deep3321