本文由 AI 辅助生成并经自动审查;尚未完成真实硬件验证。涉及具体芯片、时序和电气行为时,请以文末第一方资料和实际测试为准。
第 17 课:存储器布局:把映像、初始化与运行时边界放在同一张图上 #
程序看起来还有“空闲空间”,仍可能因为某个区域越过边界、两个运行区相交,或者带初值数据的装载位置与运行位置没有正确衔接而失败。只看源码对象、某个段名或调试器里的一次地址显示,都不足以回答“这份程序实际怎样占用有限存储器”。
本课把已经学过的链接脚本、ELF(Executable and Linkable Format,可执行与可链接格式)、map(链接映射)文件、启动初始化、调用栈和受控运行时资源放进同一份布局中追溯。本课不假定具体开发板或 STM32 型号,不给出真实芯片地址、容量、栈大小或启动符号。第 9 节的 Host Fake(主机伪实现)只验证一张受控布局表的范围规则,不模拟链接器、存储器控制器或真实 MCU(Microcontroller Unit,微控制器)。
代码状态:主机逻辑示例;真实目标构建与上板均待验证。hardware_verified=false。
1. 本节目标 #
本课唯一的核心目标是:
学习者能把链接后的代码与数据区、启动时初始化动作及预留的运行时区域视为同一份有限存储器布局,并能根据受控布局表判断区域归属、边界和重叠风险。
完成本课后,你应能:
- 把源码对象、链接后的段、装载映像、启动动作和运行时访问串成一条证据链;
- 分别指出带初值数据的装载位置与运行位置,不把二者混成一个地址;
- 使用半开区间
[start, end)判断一个范围是否属于指定区域,以及同一区域内两个范围是否重叠; - 从静态布局表中识别有效布局、范围越界和区域重叠;
- 说明 map/ELF、链接脚本、启动实现与器件资料各能证明什么;
- 明确 Host Fake 的结果不能证明真实 STM32 工程的布局。
本课只有一个新核心概念:
从构建产物到运行时存储区域的映射
.text、.rodata、.data、.bss、VMA、LMA、装载映像、零初始化、区域边界和重叠检查,都是服务这个模型的辅助术语,不是新的独立机制。
2. 它解决什么问题 #
假设一份程序包含代码、只读常量、带初值的静态对象、零初始化静态对象,以及为调用栈或受控资源池预留的空间。常见但不完整的判断有:
- “固件文件还不大,所以 RAM(Random Access Memory,随机存取存储器)一定够”;
- “
.data在 RAM,所以只占 RAM”; - “
.bss不保存初值,所以不占空间”; - “map 文件里每一段单独都没越界,所以整体不会相撞”;
- “主机实验通过,所以 STM32 链接脚本是对的”。
这些判断把不同阶段拆开了。真正需要回答的是:
- 哪些内容进入装载映像?
- 哪些内容在运行时需要一个可访问范围?
- 启动代码要复制或清零哪些范围?
- 调用栈、受控资源池等预留区域与静态数据是否共享同一有限区域?
- 每个范围属于哪个区域,终点是否越界,同一区域的范围是否相交?
GNU ld 手册说明,MEMORY 命令描述链接器可使用的存储范围;当指定区域过满时,链接器报告错误。它还说明输出段的装载地址可以通过 AT 与运行位置分开,并由运行时初始化代码把带初值内容准备到运行地址。[2] 这给出的是 GNU ld 的机制,不自动证明任何具体 STM32 工程采用了相同脚本、段名或启动实现。
所以本课要解决的不是“背一张固定内存图”,而是建立一套可迁移的追溯方法:把各阶段的证据放在同一张有限布局上,再检查归属、边界和重叠。
3. 必要前置知识 #
本课只复核学习状态中已经完成的内容:
- 链接脚本把目标文件的输入内容组织成输出段,并约束其位置。
- ELF 与 map 文件记录某一次具体构建的段、符号、地址和大小,是构建期证据。
- 进入
main前,启动实现可能需要复制带初值数据并清零某个范围;具体符号与动作必须查看目标工程。 - 自动、静态和动态存储期说明 C 对象的生命周期,不直接规定某颗 MCU 的物理地址。
- 调用栈和显式管理的运行时资源也需要有限空间;不用动态内存不等于不会耗尽 RAM。
本课不要求也不引入中断、RTOS(Real-Time Operating System,实时操作系统)、DMA(Direct Memory Access,直接存储器访问)、缓存、MPU(Memory Protection Unit,内存保护单元)、Bootloader(引导加载程序)、具体寄存器或外设配置。
4. 核心原理 #
4.1 先看一份可观察结果 #
第 9 节程序使用两块虚拟区域:IMAGE 表示装载映像的有限范围,RAM 表示运行时范围。数值只是 Host Fake 坐标,不是任何 MCU 的地址。程序的确定输出为:
valid: OK
.text owner=IMAGE [0x00001000, 0x00001060)
.rodata owner=IMAGE [0x00001060, 0x00001080)
.data.load owner=IMAGE [0x00001080, 0x000010A0)
.data.run owner=RAM [0x00002000, 0x00002020)
.bss.run owner=RAM [0x00002020, 0x00002050)
runtime.reserved owner=RAM [0x00002050, 0x00002090)
.data mapping: load=[0x00001080, 0x000010A0) -> run=[0x00002000, 0x00002020)
out_of_range: OUT_OF_RANGE
overlap: OVERLAP
zero_size: BAD_INPUT
all checks passed先只观察三件事:
- 正常布局中,每个范围都完整落在所属区域内;相邻范围可以首尾相接,但不相交。
.data.load和.data.run大小相同,却位于不同区域,分别代表初值来源和运行位置。- 故意把一个范围移到区域外、让
.bss.run与.data.run相交,或给出零长度放置项,验证器都会拒绝布局。
这就是本课的最小可运行形态。下面再把它映射到构建与启动过程。
4.2 同一份有限布局中的四类内容 #
以下名称是常见约定,不是 C 标准强制名称;实际工程必须查看链接脚本与构建产物。
| 内容 | 装载映像中通常有什么 | 运行时通常需要什么 | 建立运行状态的动作 | 必须核对的证据 |
|---|---|---|---|---|
.text 与只读内容 |
代码和只读字节 | 可执行或可读取的位置 | 由装载方式与目标系统提供 | 器件资料、链接脚本、ELF/map |
.data |
对象的初始字节 | 可写的运行范围 | 启动实现按工程约定复制初值 | 装载地址、运行地址、边界符号、启动文件 |
.bss |
通常不为每个零值保存同等大小的初值字节 | 零初始化对象的运行范围 | 启动实现按工程约定清零 | 段范围、启动文件、ELF/map |
| 运行时预留 | 通常不是一组需要复制的初值 | 调用栈、受控资源池等有限范围 | 链接/启动配置预留,运行代码消费 | 链接脚本、map/ELF、运行观察 |
“通常”很重要。.text、.rodata、.data、.bss 是常见工具链约定;段名、属性、是否合并、是否存在,以及启动代码怎样处理它们,都以实际工程为准。[2]
4.3 .data 为什么有两个位置
#
带初值的可写对象同时提出两个问题:
装载映像中的初值来源 RAM 中的运行对象
IMAGE: .data.load RAM: .data.run
[load_start, load_end) [run_start, run_end)
| ^
+---- 启动实现按工程约定复制 ------+- LMA(Load Memory Address,装载内存地址):内容在装载映像中的位置概念。
- VMA(Virtual Memory Address,虚拟内存地址):链接器为内容指定的运行地址概念。这里的“虚拟”是链接器术语,本课不引入操作系统虚拟内存。
GNU ld 2.9.1 手册用“relocation address”和“load address”解释 AT:未单独指定时二者通常相同;使用 AT 后可以不同,并需要运行时初始化代码完成复制。[2] 具体项目使用的链接器版本、语法和启动实现仍需逐项核对。
.data 因而可能同时消耗:
- 装载映像中的初值空间;
- RAM 中对象运行时的空间。
只统计其中一边,就不能描述完整映射。
4.4 .bss 为什么“映像里省字节”仍会占 RAM
#
零初始化静态对象在运行时仍需要真实范围。常见工程让启动代码根据边界清零这段范围,而不是在装载映像中为每个零保存同等数量的字节。这里能确定的只有责任关系:链接结果给出范围,启动实现建立零状态,应用随后使用对象。
“.bss 一定不占固件文件字节”“所有启动文件都按同样符号清零”都过于绝对。目标文件格式、输出段属性、映像格式和启动代码必须按该次工程核对。[2]
4.5 边界判断:使用半开区间 #
本课把每个范围写为 [start, end):包含 start,不包含 end。若 end = start + size,则:
- 范围属于区域:
item_start >= region_start且item_end <= region_end; - 两范围不重叠:
left_end <= right_start或right_end <= left_start; - 两范围重叠:
left_start < right_end且right_start < left_end; [0x2000, 0x2020)与[0x2020, 0x2050)首尾相接,不重叠。
数学上的空区间可以用零长度表示,但本实验把每个 Region 和 Placement 都定义为实际占用的非空范围,因此零长度属于 BAD_INPUT。这是一条 Host Fake 输入契约,不是 GNU ld 或 C 语言规则。
计算 start + size 前必须先检查无符号整数是否溢出。否则回绕后的较小终点可能让错误范围伪装成“没有越界”。Host Fake 先检查 size <= UINT32_MAX - start,再做加法。
4.6 谁定义、谁持有、谁建立、谁使用 #
| 对象或规则 | 在哪里定义 | 谁持有 | 谁把它带入下一阶段 | 调用或数据如何流动 |
|---|---|---|---|---|
| C 函数、常量、静态对象 | C 源文件中的声明与定义 | 源码模块 | 编译器把内容写入目标文件 | 源码 -> 目标文件输入段 |
| 输出段与存储器区域规则 | 具体工程的链接脚本 | 构建配置 | 链接器读取脚本并处理目标文件 | 输入段 -> 输出段 -> 指定区域 |
| 某次构建的段和符号实例 | 链接产生的 ELF/map | 构建产物 | 构建流程交付映像与符号边界 | 链接结果 -> 装载与启动证据 |
| 初始化边界与动作 | 链接脚本符号、启动 C/汇编实现 | 目标工程 | 链接器给出数值,复位路径进入启动代码 | 初值复制/范围清零 -> main |
| 运行时对象与预留范围 | 已建立的 RAM 状态及运行配置 | 正在运行的系统 | 启动过程交给应用 | 应用读取、修改对象并消耗预留空间 |
这里的“带入”是构建与启动阶段之间的数据交付,不是后续架构课程中的依赖注入模式。链接脚本不在复位后执行;启动代码也不负责发明段地址。
4.7 声明、实现、实例化、组装和使用 #
为了不混淆动作,本课把同一条链拆成五步:
- 声明:C 头文件声明对象或函数;链接脚本声明区域、输出段和边界符号的名字与规则。
- 实现:C 文件提供函数或对象定义;启动文件实现复制、清零等动作;链接器实现脚本所定义的布局语义。
- 实例化:具体函数、常量和静态对象经编译形成实际输入内容;某次链接形成具有确定地址与大小的段和符号。
- 组装:构建系统把目标文件、库、链接脚本和启动实现交给链接器;复位路径随后按工程约定把装载状态转换为运行状态。
- 使用:应用代码进入
main后访问已经建立生命周期的对象,并消耗调用栈或其他预留资源。
这五步只是用于追溯存储关系。本课不把它扩展成驱动架构,也不使用函数指针、组合根或依赖注入。
5. 关键术语与直观模型 #
| 术语 | 本课中的准确含义 |
|---|---|
| section(段) | 链接器组织输入内容与输出布局的单位;名称和规则由工具链与工程决定。 |
| region(存储器区域) | 布局规则中一段有限地址范围;区域名字不自动等同于某颗芯片的物理块。 |
.text |
常见的代码输出段名;实际属性与位置待工程核对。 |
.rodata |
常见的只读内容段名;是否独立存在待工程核对。 |
.data |
常见的带初值、运行时可写数据段名;可能具有不同装载位置和运行位置。 |
.bss |
常见的零初始化对象运行范围名称;映像表示和清零方式待工程核对。 |
| VMA | 链接器为内容指定的运行地址概念;不是本课中的操作系统虚拟内存。 |
| LMA | 内容初值所在装载映像位置的概念;是否与 VMA 不同由实际布局决定。 |
| 装载映像 | 用来交付程序代码与初值内容的构建结果;不等于已经完成 RAM 初始化的运行状态。 |
| Map 文件 | 链接器输出的布局报告,是某一次构建的地址和大小证据。 |
| Host Fake | 用静态数据和确定规则验证逻辑,不模拟真实 MCU 或真实链接。 |
可以把完整模型压缩成一张图:
C 源码对象
|
v
目标文件输入内容
|
| 链接脚本持有布局规则,链接器执行布局
v
装载映像 某次 ELF/map 证据
.text / 只读内容 ----------------------+
.data 初值 (LMA) |
| |
| 启动实现按实际工程复制/清零 |
v v
RAM 运行状态 <---------------------- 段、符号、地址、大小
.data 运行范围 (VMA)
.bss 零初始化范围
调用栈/受控运行时预留
|
v
应用访问对象,同时必须守住区域边界6. 从输入到结果的完整流程 #
6.1 构建到运行的真实工程流程 #
- 开发者在 C 文件中定义函数、常量和具有静态存储期的对象。
- 编译器按目标格式产生输入内容;具体输入段名称受工具链与选项影响。
- 链接器读取目标文件和链接脚本,把输入内容放入输出段与有限区域。
- 链接产生 ELF、map 和用于装载的映像;这些文件记录该次构建的布局证据。
- 目标复位后进入具体启动实现。
- 启动实现依据该工程的边界符号,准备带初值数据和零初始化范围。
- 应用进入
main,访问运行对象,同时使用调用栈或其他预留区域。 - 调试者把实际器件资料、链接脚本、ELF/map、启动实现和运行观察互相核对。
第 3、4 步是构建期;第 5 至 7 步是运行期。把“链接器为 .bss 清零”或“启动代码决定 .data 放在哪”说在一起,都会混淆责任。
6.2 Host Fake 的调用流 #
main 中的静态 Region/Placement 表
-> layout_validate()
-> 显式拒绝零长度输入
-> checked_end() 检查加法溢出
-> 检查每项是否属于声明的 region
-> 检查同一 region 中任意两项是否重叠
-> 输出 OK / OUT_OF_RANGE / OVERLAP
-> 单独报告 .data.load 到 .data.run 的映射Host Fake 中:
Region和Placement类型在示例源文件中声明;valid_regions、valid_layout等静态常量是具体实例;main持有并选择测试场景,把表和元素数量显式传给验证函数;layout_validate只读取布局记录,不访问记录所描述的虚拟地址;- 输出只证明给定表满足或违反了本程序的规则。
7. 嵌入式系统中的对应位置 #
7.1 可迁移的方法 #
面对 STM32F4、F7、H5 或 H7,先保持同一套方法:
- 用具体器件的官方参考手册或数据手册确认真实存储器边界。
- 检查实际链接脚本怎样命名区域、放置输出段和导出边界。
- 保存同一次构建的链接器版本、命令、ELF 和 map 文件。
- 检查实际启动文件怎样使用装载位置、运行位置和清零边界。
- 检查运行时预留是否与静态区保持明确边界。
- 只有具备构建、下载与运行证据时,才形成对具体板卡的结论。
可迁移的是证据链,不是某组地址或某份默认脚本。
7.2 当前不能给出的系列差异 #
本课来源只核对了 STM32 Cortex-M4 编程手册 PM0214 的适用范围,以及 GNU ld 的通用链接机制。[1][2] 它们不足以证明 F7、H5、H7 某个具体型号的存储器块、别名、容量、链接脚本或启动实现。因此这些系列差异全部标记为:待拿到具体型号及对应第一方资料后核对。
ST PM0214 可用于理解其覆盖范围内的 Cortex-M4 编程模型和地址空间背景,但不能替代具体 STM32F4 型号的参考手册或数据手册,也不能外推到 F7、H5 或 H7。[1]
8. 主机实验与硬件迁移边界 #
8.1 Host Fake 能证明什么 #
- 对给定
Region和Placement表,范围终点加法受到溢出保护; - 每个范围被检查为完整落在它声明的区域内;
- 同一区域内任意两个非空范围被检查为不重叠;
.data的装载位置与运行位置可以被分别记录和报告;- 故意构造的越界、重叠与零长度场景会进入确定的拒绝分支。
8.2 Host Fake 不能证明什么 #
- 不能证明 GNU
ld已处理某份真实链接脚本; - 不能证明某个 ELF/map 中存在示例段名或示例地址;
- 不能证明启动代码真的执行了复制或清零;
- 不能证明真实调用栈、堆或资源池的上限;
- 不能证明任一 STM32 的 Flash(闪存)、RAM、别名区或安全属性;
- 不能替代交叉编译、下载、调试和测量记录。
8.3 迁移到真实工程时需要收集什么 #
| 证据 | 用途 | 缺失时的处理 |
|---|---|---|
| 具体 MCU 完整型号 | 选择正确官方资料 | 不填写地址和容量 |
| 参考手册与数据手册版本 | 确认存储器边界 | 标记待核对 |
| 工具链与链接器版本 | 确认脚本语义与报告格式 | 不套用本课版本细节 |
| 实际链接脚本 | 确认区域、段与边界符号 | 不猜段名和符号名 |
| 启动文件 | 确认复制、清零和进入 main 的动作 |
不声称运行状态已建立 |
| 同次 ELF/map 与构建日志 | 确认实际地址、大小及成功构建 | 不声称已编译 |
| 下载、调试或测量记录 | 确认目标运行事实 | 保持 hardware_verified=false |
9. 最小代码示例 #
下面是完整的单文件 C11 Host Fake。它使用固定宽度整数和静态存储,不使用动态内存、函数指针或未初始化读取,也不会解引用虚拟地址。
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
enum LayoutStatus {
LAYOUT_OK = 0,
LAYOUT_BAD_INPUT = 1,
LAYOUT_ARITHMETIC_OVERFLOW = 2,
LAYOUT_OUT_OF_RANGE = 3,
LAYOUT_OVERLAP = 4
};
struct Region {
const char *name;
uint32_t start;
uint32_t size;
};
struct Placement {
const char *name;
uint32_t region_index;
uint32_t start;
uint32_t size;
};
static uint32_t g_failures;
static uint32_t checked_end(uint32_t start, uint32_t size, uint32_t *end)
{
if (end == NULL || size > UINT32_MAX - start) {
return 0U;
}
*end = start + size;
return 1U;
}
static uint32_t ranges_overlap(const struct Placement *left,
const struct Placement *right)
{
uint32_t left_end;
uint32_t right_end;
if (checked_end(left->start, left->size, &left_end) == 0U ||
checked_end(right->start, right->size, &right_end) == 0U) {
return 0U;
}
return (left->start < right_end && right->start < left_end) ? 1U : 0U;
}
static enum LayoutStatus layout_validate(const struct Region *regions,
uint32_t region_count,
const struct Placement *items,
uint32_t item_count)
{
uint32_t item_index;
uint32_t other_index;
if (regions == NULL || region_count == 0U ||
(items == NULL && item_count != 0U)) {
return LAYOUT_BAD_INPUT;
}
for (item_index = 0U; item_index < item_count; ++item_index) {
const struct Placement *item = &items[item_index];
const struct Region *owner;
uint32_t region_end;
uint32_t item_end;
if (item->region_index >= region_count) {
return LAYOUT_BAD_INPUT;
}
owner = ®ions[item->region_index];
if (owner->size == 0U || item->size == 0U) {
return LAYOUT_BAD_INPUT;
}
if (checked_end(owner->start, owner->size, ®ion_end) == 0U ||
checked_end(item->start, item->size, &item_end) == 0U) {
return LAYOUT_ARITHMETIC_OVERFLOW;
}
if (item->start < owner->start || item_end > region_end) {
return LAYOUT_OUT_OF_RANGE;
}
}
for (item_index = 0U; item_index < item_count; ++item_index) {
for (other_index = item_index + 1U;
other_index < item_count;
++other_index) {
if (items[item_index].region_index ==
items[other_index].region_index &&
ranges_overlap(&items[item_index],
&items[other_index]) != 0U) {
return LAYOUT_OVERLAP;
}
}
}
return LAYOUT_OK;
}
static const char *status_name(enum LayoutStatus status)
{
switch (status) {
case LAYOUT_OK:
return "OK";
case LAYOUT_BAD_INPUT:
return "BAD_INPUT";
case LAYOUT_ARITHMETIC_OVERFLOW:
return "ARITHMETIC_OVERFLOW";
case LAYOUT_OUT_OF_RANGE:
return "OUT_OF_RANGE";
case LAYOUT_OVERLAP:
return "OVERLAP";
default:
return "UNKNOWN";
}
}
static void print_placement(const struct Region *regions,
const struct Placement *item)
{
uint32_t end = item->start + item->size;
(void)printf("%s owner=%s [0x%08" PRIX32 ", 0x%08" PRIX32 ")\n",
item->name,
regions[item->region_index].name,
item->start,
end);
}
static void expect_status(const char *scenario,
enum LayoutStatus actual,
enum LayoutStatus expected)
{
(void)printf("%s: %s\n", scenario, status_name(actual));
if (actual != expected) {
g_failures++;
}
}
int main(void)
{
static const struct Region valid_regions[] = {
{ "IMAGE", UINT32_C(0x00001000), UINT32_C(0x00000200) },
{ "RAM", UINT32_C(0x00002000), UINT32_C(0x00000180) }
};
static const struct Placement valid_layout[] = {
{ ".text", 0U, UINT32_C(0x00001000), UINT32_C(0x60) },
{ ".rodata", 0U, UINT32_C(0x00001060), UINT32_C(0x20) },
{ ".data.load", 0U, UINT32_C(0x00001080), UINT32_C(0x20) },
{ ".data.run", 1U, UINT32_C(0x00002000), UINT32_C(0x20) },
{ ".bss.run", 1U, UINT32_C(0x00002020), UINT32_C(0x30) },
{ "runtime.reserved", 1U, UINT32_C(0x00002050), UINT32_C(0x40) }
};
static const struct Placement out_of_range_layout[] = {
{ ".data.run", 1U, UINT32_C(0x00002000), UINT32_C(0x20) },
{ "runtime.reserved", 1U, UINT32_C(0x00002170), UINT32_C(0x20) }
};
static const struct Placement overlap_layout[] = {
{ ".data.run", 1U, UINT32_C(0x00002000), UINT32_C(0x20) },
{ ".bss.run", 1U, UINT32_C(0x00002010), UINT32_C(0x30) }
};
static const struct Placement zero_size_layout[] = {
{ ".data.run", 1U, UINT32_C(0x00002000), UINT32_C(0x00) }
};
enum LayoutStatus status;
uint32_t index;
uint32_t load_end;
uint32_t run_end;
status = layout_validate(valid_regions, 2U, valid_layout, 6U);
expect_status("valid", status, LAYOUT_OK);
if (status == LAYOUT_OK) {
for (index = 0U; index < 6U; ++index) {
print_placement(valid_regions, &valid_layout[index]);
}
load_end = valid_layout[2].start + valid_layout[2].size;
run_end = valid_layout[3].start + valid_layout[3].size;
(void)printf(".data mapping: load=[0x%08" PRIX32
", 0x%08" PRIX32 ") -> run=[0x%08" PRIX32
", 0x%08" PRIX32 ")\n",
valid_layout[2].start,
load_end,
valid_layout[3].start,
run_end);
}
status = layout_validate(valid_regions, 2U, out_of_range_layout, 2U);
expect_status("out_of_range", status, LAYOUT_OUT_OF_RANGE);
status = layout_validate(valid_regions, 2U, overlap_layout, 2U);
expect_status("overlap", status, LAYOUT_OVERLAP);
status = layout_validate(valid_regions, 2U, zero_size_layout, 1U);
expect_status("zero_size", status, LAYOUT_BAD_INPUT);
if (g_failures == 0U) {
(void)printf("all checks passed\n");
return 0;
}
(void)printf("failures=%" PRIu32 "\n", g_failures);
return 1;
}9.1 代码分层、依赖方向和生命周期 #
这是教学实验层的单文件程序:
场景与静态表 main
-> 布局规则 layout_validate
-> 无符号边界运算 checked_end
-> 主机标准输出- 依赖方向从实验场景指向验证规则;验证规则不依赖 STM32 HAL(硬件抽象层)或任何硬件地址。
- 所有区域和布局实例都是
static const,具有静态存储期,程序全程存在。 layout_validate只在调用期间持有指向表的参数,不保存指针。- 虚拟范围只是整数记录,不对应可解引用的主机地址。
- 公开可见的数据字段和计数均使用固定宽度整数;程序不申请动态内存。
9.2 Windows 主机构建 #
在普通 Windows 主机上,将代码保存为 memory_layout_fake.c,任选本机已有编译器:
gcc -std=c11 -Wall -Wextra -Wpedantic -Werror memory_layout_fake.c -o memory_layout_fake.exe
./memory_layout_fake.exe或:
clang -std=c11 -Wall -Wextra -Wpedantic -Werror memory_layout_fake.c -o memory_layout_fake.exe
./memory_layout_fake.exe验收条件是编译退出码为 0,程序退出码为 0,并得到第 4.1 节所示结果。必须记录实际编译器版本、命令和退出码后,才能声称该次主机构建成功;即使主机构建成功,仍不构成 STM32 硬件验证。
10. 常见错误 #
10.1 把段名当成 C 语言保证 #
错误:“C 标准规定代码在 .text,零初始化变量在 .bss。”
正确做法:把这些名字称为常见工具链约定,再从实际目标文件、链接脚本和 ELF/map 核对。C 对象的语言属性与链接器怎样命名输出段不是同一层规则。
10.2 只看总空闲量,不看连续边界 #
两个区域各自有剩余,不代表某个指定区域能容纳一个范围。布局检查首先关心“属于哪个区域”和“范围终点在哪里”,不能把不同区域的零散余量相加后得出可放置结论。
10.3 把 .data 只算一次
#
只看 RAM 会漏掉装载映像中的初值,只看固件映像会漏掉运行时可写范围。应分别记录 LMA 和 VMA,再核对启动实现怎样连接两者。[2]
10.4 认为 .bss 不占存储器
#
即使零值不以同等字节数出现在装载映像中,运行对象仍需要范围,并且启动过程需要建立规定的初始状态。实际表示方式和清零动作要从构建产物与启动文件确认。
10.5 只检查相邻表项 #
如果布局表没有先按地址排序,只检查数组中的相邻项可能漏掉交叉范围。示例对同一区域内所有项做两两检查;对更大数据集可以先排序再检查,但那是实现优化,不改变本课规则。
10.6 先加法,后检查溢出 #
end = start + size 可能发生无符号回绕。应先验证 size <= UINT32_MAX - start,再计算终点。
10.7 把相邻误判为重叠 #
使用半开区间后,左区终点等于右区起点表示恰好相邻。若用闭区间混算,很容易多算或少算一个地址单位。
10.8 把 Host Fake 当成 ELF 解析器 #
示例没有读取 ELF,没有执行链接脚本,也没有读取真实地址。修改表格只是在修改测试输入,不能声称改变了真实固件布局。
10.9 从 PM0214 外推所有 STM32 #
PM0214 的标题和适用范围明确指向其覆盖的 Cortex-M4 MCU/MPU 编程背景。[1] 它不能单独证明 F7、H5、H7 或具体型号的存储器布局。
11. 调试观察点 #
11.1 在验证入口先看四个量 #
在 layout_validate 设置断点,先观察:
region_count与item_count是否和静态表一致;- 当前
item->region_index是否落在区域表范围内; item->start与item->size是否为预期场景;region_end和item_end是否在完成溢出检查后计算。
11.2 观察越界分支 #
RAM 的半开终点是 0x2180。错误项 [0x2170, 0x2190) 的起点仍在 RAM 内,但终点越过区域终点。这个场景用于纠正“起始地址有效就算有效”的误解。
11.3 观察重叠分支 #
错误场景中:
.data.run [0x2000, 0x2020)
.bss.run [0x2010, 0x2040)两者满足 left.start < right.end 且 right.start < left.end,所以相交。把 .bss.run 起点改成 0x2020 后,两者只相邻,应通过重叠检查。
11.4 真实工程的四向核对 #
拿到目标工程后,对同一范围同时看:
- 链接脚本中的区域与放置规则;
- map 文件中的段起点、大小和边界符号;
- ELF 工具显示的段与装载信息;
- 启动文件实际读取的符号和执行的循环。
若四者名称或数值对不上,先确认它们是否来自同一次构建。不要先修改脚本,也不要用调试器单次显示覆盖构建证据。
11.5 当前硬件观察边界 #
本课没有具体 MCU、开发板、调试器、目标 ELF/map、构建日志、下载记录或测量数据。因此没有可报告的真实寄存器、地址、容量或运行结果,hardware_verified 保持 false。
12. 实战实验 #
实验 A:复现有效布局与三个拒绝分支 #
目标:验证最小 Host Fake 的确定结果。
步骤:
- 把第 9 节代码保存为
memory_layout_fake.c。 - 记录编译器名称和
--version输出。 - 使用
-std=c11 -Wall -Wextra -Wpedantic -Werror构建。 - 记录编译命令、编译退出码、程序退出码和完整输出。
- 对照第 4.1 节,确认正常布局为
OK,三个错误分别为OUT_OF_RANGE、OVERLAP和BAD_INPUT。
验收:没有警告被忽略;程序退出码为 0;能说明该结果只适用于 Host Fake 输入表。
实验 B:区分相邻与重叠 #
目标:用半开区间判断边界。
步骤:
- 保持
.data.run为[0x2000, 0x2020)。 - 依次把
.bss.run起点设为0x201F、0x2020、0x2021,大小保持非零。 - 预测每次结果,再运行验证。
- 解释为什么
0x2020是相邻而不是重叠。
验收:0x201F 被拒绝;0x2020 和 0x2021 不因这两个范围相交而被拒绝;答案使用 [start, end) 说明。
实验 C:证明只检查起点不够 #
目标:观察“起点在区域内、终点在区域外”的越界。
步骤:
- 找出
RAM的起点、大小和半开终点。 - 保持错误项起点为
0x2170,将大小依次设为0x10、0x11、0x20。 - 先手算终点,再运行。
- 说明哪一项恰好贴住边界,哪一项开始越界。
验收:大小 0x10 的终点为 0x2180,可落在区域内;更大的范围越界。
实验 D:增加算术溢出场景 #
目标:验证边界加法不会回绕后伪装成有效范围。
步骤:
- 新增一个区域或测试项,使
start接近UINT32_MAX。 - 令
size > UINT32_MAX - start。 - 预期返回
ARITHMETIC_OVERFLOW。 - 不解引用该数值,也不把它称为真实硬件地址。
验收:程序拒绝场景;答案能说明为什么检查必须发生在加法前。
实验 E:纸面追溯一个带初值对象 #
目标:把源码、链接和启动证据连起来,不修改真实工程。
步骤:
- 在纸上写一个虚构对象名
config_word,只作为追踪标签。 - 画出“源码定义 -> 目标文件输入内容 -> 输出段 ->
.data装载范围 ->.data运行范围 -> 启动复制 ->main使用”。 - 在每条箭头旁写出需要的真实证据:目标文件/ELF、链接脚本、map、启动文件。
- 圈出目前缺失的证据,不填写虚构地址。
验收:装载位置与运行位置分开;没有把段名当作 C 标准保证。
可选实验 F:核对自己的真实 STM32 工程 #
只有在已有具体工程时执行:
- 记录完整 MCU 型号、开发板、工具链和链接器版本。
- 只读查看链接脚本、启动文件、同次 ELF/map 和构建日志。
- 从器件官方资料确认相关存储器边界。
- 建立自己的区域表,逐项填写来源路径或文档章节。
- 检查
.data的装载/运行范围、.bss范围和运行时预留。 - 不烧录、不修改 IDE/CubeMX 全局配置;没有运行证据时只形成构建期结论。
验收:每个地址和大小都有来源;不同构建的文件没有混用;结论明确区分构建期与运行期。
13. 自检题 #
- 为什么“固件映像还有空间”不能证明 RAM 布局安全?
.data的 LMA 与 VMA 分别回答什么问题?.bss没有逐字节初值时,为什么运行时仍需要空间?[0x1000, 0x1020)与[0x1020, 0x1030)是否重叠?- 为什么计算
end = start + size前要检查溢出? - 谁持有存储器区域和输出段规则?谁在运行时执行初始化动作?
- map 文件能否单独证明启动代码已经执行?
- Host Fake 的
IMAGE和RAM是否代表某颗 STM32 的真实地址? - 要把本课方法迁移到 F7,需要新增哪些证据?
- 生成这篇课程后,为什么 mastery 不能自动提高?
参考要点:
- 映像空间和 RAM 空间承担不同内容,运行时预留还可能与静态范围竞争边界。
- LMA 追踪初值的装载位置,VMA 追踪内容运行时使用的位置。
- 对象在运行时仍必须有可访问范围,并由具体启动实现建立规定初态。
- 不重叠;半开区间允许终点与下一范围起点相等。
- 无符号回绕可能产生较小终点,让越界范围通过错误检查。
- 工程链接脚本持有布局规则;具体启动 C/汇编代码执行运行时动作。
- 不能;它是构建期证据,不是运行记录。
- 不是;它们只是 Host Fake 的静态坐标。
- 具体型号资料、实际脚本、启动文件、工具链版本、同次 ELF/map 和所需运行记录。
- 掌握度只能由用户答案、实验或复习结果提高,生成资产不是学习证据。
14. 面试题 #
14.1 为什么 .data 可能同时占用非易失性映像和 RAM
#
建议答案:带初值可写对象需要保存初始字节,又需要在运行时存在于可写位置。链接布局可能为它提供不同的装载地址和运行地址,启动实现再按工程约定复制。具体位置、符号与动作必须从实际链接脚本、ELF/map 和启动文件确认。[2]
14.2 .bss 不占 RAM,对吗
#
建议答案:不对。常见情况下,它不需要在装载映像中保存与运行范围等大的零字节,但零初始化对象运行时仍占范围。映像表示与清零方式属于具体工具链和启动实现事实。
14.3 如何判断两个内存范围是否重叠 #
建议答案:先用无溢出的方式得到两个半开区间终点。若 a.start < b.end 且 b.start < a.end,则相交;终点恰好等于另一范围起点时不相交。
14.4 链接器报区域未溢出,是否就能证明运行时不会内存不足 #
建议答案:不能。链接器能检查它看得到并被脚本约束的构建期布局;实际调用栈峰值、受控资源池使用和某些运行路径还需要额外静态分析或运行证据。还要确认预留规则本身正确。
14.5 如何审查一份陌生 STM32 工程的存储器布局 #
建议答案:先确定具体器件与官方资料,再记录工具链版本;对照链接脚本、同次 ELF/map 和启动文件,分别追踪代码、只读内容、.data 装载/运行范围、.bss 和运行时预留;最后根据构建与运行证据限定结论,不从默认工程名或段名猜地址。
14.6 VMA 中的“虚拟”是否表示 Cortex-M 正在使用操作系统虚拟内存 #
建议答案:不是。这里是链接器描述内容运行地址的术语。本课没有引入操作系统虚拟内存,也不能从 VMA 一词推断存在地址转换。
15. 延伸思考 #
- 如果两个范围属于不同区域,但数值坐标恰好相同,验证器应该怎样理解“重叠”?这取决于区域是否代表独立地址空间还是同一物理空间的别名,真实工程需要什么资料才能判断?
- 为什么把所有运行时预留简单写成一个“大空洞”可能掩盖实际峰值风险?
- 若 map 文件和调试器中的符号地址不同,应该先核对哪些构建标识和装载事实?
- 如果
.data的装载范围和运行范围大小不同,应当停止在哪一步,检查哪些脚本与启动约定? - 后续 Device Framework 中的静态对象、缓冲区和接口实例,怎样沿本课证据链追溯存储占用?这里只思考存储归属,不提前设计架构。
- 在不使用动态内存的前提下,如何为多个静态缓冲区保留清晰边界,并让 map/ELF 提供可审计证据?
16. 本节总结 #
本课只有一个核心模型:从构建产物到运行时存储区域的映射。
源码对象
-> 编译后的输入内容
-> 链接脚本约束下的输出段与区域
-> ELF/map 和装载映像
-> 启动复制/清零等初始化动作
-> RAM 中的运行对象与运行时预留
-> 应用访问检查布局时,不只问“总共多大”,还要逐项问:
- 它属于哪个有限区域?
- 起点和大小来自哪份证据?
- 终点计算是否溢出或越界?
- 与同一区域的其他范围是否重叠?
- 它是装载位置、运行位置,还是二者都有?
- 谁建立运行状态,谁随后使用它?
Host Fake 让这些判断在普通 Windows 主机上可观察,但它不生成 ELF、不执行启动代码,也不证明硬件事实。真实 STM32 结论必须由具体器件资料、实际链接脚本、同次 ELF/map、启动实现和运行记录共同支撑。
17. 下一步 #
本课完成 startup-linking-memory 阶段的 memory-layout 主题。开始下一阶段前,至少完成实验 A,并能不看正文回答:
.data为什么可能有装载与运行两个位置;- 为什么
.bss和运行时预留必须放进同一份 RAM 边界分析; - 如何用半开区间检查归属和重叠;
- Host Fake、map/ELF、启动文件和硬件资料分别能证明什么。
生成本课程本身不会提高掌握度。本次任务不修改 config/learning-state.toml 或任何 mastery 值;只有用户答案、实验结果或复习记录可作为后续状态变化证据。
18. 参考资料 #
[1] STMicroelectronics, STM32 Cortex-M4 MCUs and MPUs programming manual, PM0214 Rev 10, March 2020. 用于限定其覆盖范围内的 Cortex-M4 编程模型和存储器背景;不能替代具体器件参考手册,也不外推到 F7、H5 或 H7。
https://www.st.com/resource/en/programming_manual/pm0214-stm32f3-and-stm32f4-series-cortexm4-programming-manual-stmicroelectronics.pdf
[2] GNU Project, Using LD, the GNU Linker 2.9.1, 1998,重点参阅 “Memory Layout”、LOADADDR 与输出段 AT (ldadr) 说明。用于核对 GNU ld 的区域容量、装载地址与运行地址机制;真实工程必须以实际链接器版本和脚本复核。
https://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_mono/ld.html
来源访问日期:2026-10-03。Arm DDI0403 计划入口本次访问返回 403,未用于支撑正文结论。本文没有使用来源政策之外的博客或第三方完整源码。