4、应用程序映像结构:可执行映像的组成、.text/.data/.bss段详解、映像的加载地址与运行地址

好,咱们接着聊。上一章我们把 Bootloader 怎么跳转讲清楚了,但有个关键问题一直没细说——跳过去的是什么?

说白了,跳转的目标是一个可执行映像。这个映像长什么样?里面装了什么?地址怎么安排的?这些搞不清楚,跳转就是瞎跳。

我个人习惯,在讲任何跳转逻辑之前,先让学生把映像结构吃透。因为 Bootloader 的很多 bug,根源都在这里。

4.1 可执行映像的组成

一个编译好的应用程序,烧到 Flash 里,或者加载到 RAM 里,它不是一个简单的二进制大块。它是有结构的。

我一般把它拆成三部分来看:

  • 头部信息:包括中断向量表、启动代码、一些元数据。这部分是 CPU 上电后最先执行的。
  • 代码段和数据段:就是 .text、.data、.bss 这些。
  • 附加信息:比如 CRC 校验值、版本号、签名等。这些是 Bootloader 用来做校验和管理的。

你想想看,Bootloader 拿到一个映像文件,它首先要解析头部,确认这个映像是不是合法的,然后才能决定要不要跳过去。

核心观点:映像结构是 Bootloader 和应用程序之间的“契约”。双方必须按同一个格式来,否则跳转就是一场灾难。

4.2 .text / .data / .bss 段详解

这三个段,是嵌入式开发里最基础也最容易出问题的概念。我见过太多人栽在这上面。

.text 段(代码段)

这个段存放的是你的程序指令。说白了,就是编译器把你写的 C 代码翻译成的机器码。

特点:

  • 只读:运行时不会修改它。所以它通常放在 Flash 里。
  • 可共享:如果多个任务执行同一段代码,可以共用一份 .text。
  • 位置有关 vs 位置无关:这个后面会细讲,但你要知道,Bootloader 跳转时特别在意这个。

我记得有一次,同事把 .text 段链接到了 RAM 地址,结果程序一跑就崩。查了半天,发现是链接脚本写错了,把只读代码放到了 RAM 里,上电后内容全是乱的。

.data 段(已初始化数据段)

这个段存放的是你定义了初始值的全局变量和静态变量。比如:

int g_counter = 100;  // 这个变量在 .data 段
static char g_buf[64] = "hello";  // 这个也在 .data 段

问题来了:这些变量有初始值,但初始值存在哪里?

嗯,这里要注意。编译后,.data 段的初始值其实是放在 Flash 里的。但运行时,这些变量必须在 RAM 里才能读写。所以 Bootloader 或者启动代码,必须做一件事——把 .data 段从 Flash 拷贝到 RAM

避坑指南:我曾经遇到过一个问题,Bootloader 跳转后应用程序跑不起来。查到最后发现,是启动代码里 .data 段的拷贝地址算错了。源地址和目的地址重叠了一部分,导致拷贝过程中数据被覆盖。从那以后,我每次都会仔细检查链接脚本里的加载地址和运行地址。

.bss 段(未初始化数据段)

这个段存放的是没有初始值,或者初始值为 0 的全局变量和静态变量。比如:

int g_flag;  // 这个在 .bss 段
static int g_temp;  // 这个也在 .bss 段

.bss 段的特点是:它不占用 Flash 空间。编译后的映像文件里,.bss 段只记录了一个大小信息,没有实际数据。

为什么?因为运行时,这些变量默认就是 0。启动代码只需要在 RAM 里把这块区域清零就行了。

你想想看,如果你的程序有 1MB 的 .bss 变量,但 Flash 里只存了几个字节的描述信息,这多省空间。

段名 存储内容 存储位置(加载时) 存储位置(运行时) 初始化方式
.text 程序指令 Flash Flash(或 RAM) 无需初始化
.data 已初始化全局/静态变量 Flash RAM 从 Flash 拷贝到 RAM
.bss 未初始化/零初始化变量 不占用 RAM RAM 区域清零

4.3 映像的加载地址与运行地址

这两个概念,是 Bootloader 跳转的核心。搞不懂它们,跳转就是碰运气。

加载地址(Load Address):映像在非易失性存储器(比如 Flash)中存放的位置。说白了,就是烧录时放哪。

运行地址(Run Address):映像在运行时应该位于的内存地址。CPU 从这个地址取指令、读写数据。

大多数情况下,加载地址和运行地址是一样的。比如你的程序直接在 Flash 里运行,那加载地址就是 Flash 地址,运行地址也是 Flash 地址。

但有些情况不一样:

  • 程序需要拷贝到 RAM 里运行:比如为了速度,或者 Flash 太慢。这时候加载地址是 Flash,运行地址是 RAM。
  • Bootloader 把应用程序搬到了不同的 Flash 位置:比如 OTA 升级后,新程序放在了一个临时区域,需要搬回主区域。

我建议你在设计 Bootloader 时,一定要明确这两个地址。链接脚本里通常用 LOAD_ADDRESSRUN_ADDRESS 来区分。

个人经验:我习惯在映像头部加一个结构体,里面明确写上加载地址、运行地址、映像大小、CRC 值。这样 Bootloader 在跳转前,可以自己判断是否需要搬移。省得每次都要手动配置。

举个例子,一个典型的链接脚本片段:

MEMORY
{
    FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 512K
    RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}

SECTIONS
{
    .text :
    {
        *(.text)
        *(.rodata)
    } > FLASH  /* 运行地址在 Flash */

    .data : 
    {
        *(.data)
    } > RAM AT > FLASH  /* 运行地址在 RAM,加载地址在 Flash */

    .bss :
    {
        *(.bss)
    } > RAM  /* 运行地址在 RAM,无加载地址 */
}

你看,.data 段后面有个 AT > FLASH,这就是告诉链接器:这个段的初始数据放在 Flash 里,但运行时请放到 RAM 里。

启动代码要做的,就是根据这个信息,把 .data 从 Flash 拷贝到 RAM,然后把 .bss 清零。

嗯,这里有个坑。如果你的 Bootloader 跳转前没有正确初始化这些段,应用程序一运行就会访问到错误的数据。我见过最典型的情况是:.bss 没清零,全局变量初始值随机,程序行为完全不可预测。

所以,我的建议是:在 Bootloader 跳转前,确保目标应用程序的启动环境是干净的。要么让应用程序自己的启动代码做初始化,要么 Bootloader 帮它做。但一定要明确谁负责,别互相推诿。

好了,映像结构这块就讲这么多。下一章我们聊聊跳转时的栈指针和 PC 指针怎么设置,那才是跳转的临门一脚。