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 理清静态内存分析流程

下面这张图总结了静态内存分析的核心步骤和决策点。我建议你把它存下来,每次做移植前都过一遍:

静态内存分析流程 步骤1:编译生成 .elf 步骤2:size 查看三段 .text 分析 .data 分析 .bss 分析 objdump -t 排序 找出最大函数 检查初始值占用 考虑 const 优化 nm --size-sort 找出缓冲区大户 是否超出目标芯片限制? 否 → 继续移植 是 → 进入优化流程 提示:每次修改代码后重新运行 size,对比变化。养成习惯,不要等到最后才看内存。

2.6 实战建议:建立你的内存基线

我每做一个新项目,第一件事就是跑一次 size,把结果记下来。哪怕程序还只是个空壳。这叫“内存基线”。

然后每加一个模块,再跑一次,对比变化。这样你就能精确知道:

  • 加了这个字体库,.text 涨了多少
  • 开了双缓冲,.bss 涨了多少
  • 用了这个图片解码器,.data 涨了多少

我曾经在一个项目里,就是因为坚持做这个“每次提交都看 size”的习惯,提前发现了一个第三方库在 Release 模式下居然比 Debug 模式大了30%——原因是它的断言宏在 Release 下没有消除干净。嗯,这种坑,你不看 size 根本发现不了。

我的个人习惯: 在 Makefile 或 CMakeLists.txt 里加一行编译后自动执行 size 的命令。这样每次编译完,终端最后一行就是内存占用。不用刻意记,看多了自然就有感觉了。