2. 静态内存分析:代码段(.text)大小分析、数据段(.data/.bss)分析、使用size命令查看段信息
做界面移植,第一件事不是写代码,而是搞清楚你的程序到底占了多少内存。我见过太多人上来就堆功能,结果编译完发现Flash爆了,RAM也不够用。嗯,这时候再回头优化,代价就大了。
静态内存分析,说白了就是看编译出来的二进制文件里,各个段分别占了多少空间。你不需要运行程序,只需要一个命令——size,就能把底牌看得一清二楚。
2.1 三个核心段:.text、.data、.bss
一个嵌入式程序编译后,主要分成三个区域。我习惯把它们比作一个工具箱:
- .text(代码段):存放你的函数指令、常量字符串、跳转表。相当于工具箱里的扳手和螺丝刀——只读,不能改。
- .data(已初始化数据段):存放你显式赋了初值的全局变量和静态变量。比如
int flag = 1里的那个1,就存在这里。 - .bss(未初始化数据段):存放没赋初值或初值为0的全局/静态变量。它不占Flash空间,只占RAM空间。
为什么会把 .bss 单独拎出来?因为它的初始值全是0,编译器根本不用在Flash里存这些0,只需要在启动时把对应RAM区域清零就行。这是嵌入式系统里一个非常经典的优化点。
关键认知:
- .text 占用 Flash(或ROM),运行时也可能占用指令缓存
- .data 既占用 Flash(存初始值),又占用 RAM(运行时拷贝)
- .bss 只占用 RAM,不占用 Flash
2.2 使用 size 命令查看段信息
打开终端,对你的目标可执行文件执行 size,结果大概长这样:
$ size build/ui_demo.elf
text data bss dec hex filename
45232 1284 4096 50612 c5b4 build/ui_demo.elf
这四列分别对应:
- text:代码段大小(字节)
- data:已初始化数据段大小
- bss:未初始化数据段大小
- dec / hex:总和,十进制和十六进制
我个人习惯先看 dec 这一列,它告诉你整个程序在RAM里占多大。然后看 text,判断Flash够不够用。如果Flash只有64KB,text 已经45KB了,那你得小心了——后面加个字体库可能就爆了。
小技巧: 用 size -A 可以看更详细的段信息,包括每个段的具体起始地址和长度。这在排查链接脚本问题时特别有用。
2.3 深入分析:谁在吃你的内存?
光看总数还不够。你得知道是哪个函数、哪个变量在吃内存。我推荐两个工具:
2.3.1 用 objdump 看 .text 细节
$ arm-none-eabi-objdump -t build/ui_demo.elf | sort -k5 -n -r | head -20
这条命令会把所有符号按大小倒序排列,前20个就是最大的“内存杀手”。我在项目中遇到过,一个看似无害的 sprintf 调用,拉进来一整套 stdio 库,直接多了8KB的代码段。从那以后,我对 printf 系列函数就格外警惕。
2.3.2 用 nm 看 .bss 大户
$ arm-none-eabi-nm --size-sort build/ui_demo.elf | grep ' [Bb] '
这会列出所有 .bss 符号及其大小。你可能会发现,某个全局缓冲区数组占了2KB,某个结构体实例占了1KB。我曾经在一个项目中,发现一个用于日志缓存的 char log_buf[4096] 占了 .bss 的一半——而那个日志功能其实根本没启用。嗯,这种“死代码”和“死数据”是最容易优化的。
避坑指南: 我曾经接手过一个项目,.bss 段显示只有2KB,但运行时系统总是不稳定。查了两天才发现,链接脚本里把 .bss 的起始地址和堆区重叠了。所以,size 告诉你的是编译时的静态大小,不代表运行时的实际内存布局。一定要结合链接脚本(.ld文件)和 map 文件一起看。
2.4 界面移植中的典型内存分布
做界面移植时,内存分布往往有规律可循。我整理了一个典型的小型GUI项目数据:
| 模块 | 段类型 | 典型大小 | 说明 |
|---|---|---|---|
| GUI核心库 | .text | 12~20 KB | 窗口管理、事件分发、绘制引擎 |
| 字体数据 | .rodata(属于.text) | 5~50 KB | 取决于字库大小和字符集 |
| 图片资源 | .rodata | 10~100+ KB | 位图、图标、背景图 |
| 全局配置 | .data | 0.1~1 KB | 屏幕参数、颜色主题等 |
| 帧缓冲区 | .bss | 几十~几百 KB | 这是RAM大户,常需优化 |
| 控件对象池 | .bss | 2~10 KB | 按钮、标签等控件实例 |
你看,帧缓冲区往往是 .bss 里最大的那块。如果你的屏幕是320×240、16位色,一个帧缓冲区就是 320×240×2 = 153,600 字节,约150KB。如果你的MCU只有64KB RAM,那根本放不下。这时候就得用分块刷新、压缩缓冲区、或者直接操作显存——这些我们后面章节会细讲。
2.5 用 SVG 理清静态内存分析流程
下面这张图总结了静态内存分析的核心步骤和决策点。我建议你把它存下来,每次做移植前都过一遍:
2.6 实战建议:建立你的内存基线
我每做一个新项目,第一件事就是跑一次 size,把结果记下来。哪怕程序还只是个空壳。这叫“内存基线”。
然后每加一个模块,再跑一次,对比变化。这样你就能精确知道:
- 加了这个字体库,.text 涨了多少
- 开了双缓冲,.bss 涨了多少
- 用了这个图片解码器,.data 涨了多少
我曾经在一个项目里,就是因为坚持做这个“每次提交都看 size”的习惯,提前发现了一个第三方库在 Release 模式下居然比 Debug 模式大了30%——原因是它的断言宏在 Release 下没有消除干净。嗯,这种坑,你不看 size 根本发现不了。
我的个人习惯: 在 Makefile 或 CMakeLists.txt 里加一行编译后自动执行 size 的命令。这样每次编译完,终端最后一行就是内存占用。不用刻意记,看多了自然就有感觉了。