带你了解keil的链接文件(sct)的作用
Keil ARMCC 链接文件(.sct)深度解析
📖 介绍
链接文件是嵌入式开发中最容易被忽视但极其重要的配置文件。很多开发者遇到链接错误就束手无策,或者在做 Bootloader、OTA 升级、CCMRAM 分配时不知道如何配置内存布局,本文帮你彻底搞懂 .sct 文件。
在 ARMCC(ARM Compiler v5)工具链中,链接器使用**分散加载文件(Scatter Loading File)**来描述程序的内存布局。这个文件的后缀通常为 .sct,它是 Keil 工程中控制"代码和数据放在哪里"的核心配置。理解了 .sct,你就掌握了嵌入式链接的命脉。
你将学到:
- 分散加载文件的核心概念与语法规则
- 加载域(LR)与执行域(ER)的本质区别
- +RO / +RW / +ZI 三种段类型的含义
- 链接器自动生成的代码(
__scatterload、__main) - Keil 自动链接符号(
Image$$)的使用方法 - Bootloader + App 双固件架构的
.sct配置 - CCMRAM 分配、RAM 执行函数等实战场景
- 常见链接错误的排查与解决
📌 系列文章导航:
- 📘 上一篇:STM32F407 启动文件深度解析
- 📗 本文:Keil ARMCC 链接文件(.sct)深度解析
📑 目录
一、链接文件概述
1.1 什么是链接文件?
链接文件是描述程序如何在物理内存中布局的配置文件。在 Keil MDK + ARM Compiler v5(ARMCC)工具链中,这个文件被称为分散加载描述文件(Scatter File),通常以 .sct 为后缀。
它告诉链接器两件事:
- 代码和数据的加载地址(烧录时放在哪里)
- 代码和数据的执行地址(运行时放在哪里)
一个典型的嵌入式程序包含多种类型的数据:中断向量表、用户代码、只读常量、全局变量、堆栈等。链接文件决定了这些内容在 Flash 和 RAM 中的精确位置。
💡 工程师经验: 很多人把链接文件当成"黑盒",完全依赖 Keil 的自动生成。这在简单项目中没问题,但一旦涉及多固件分区、特殊内存区域分配,就必须手动编写
.sct文件。理解它是从"会用 Keil"到"精通 Keil"的关键一步。
1.2 链接文件在编译流程中的位置
.sct 文件在编译流程中处于链接阶段,是链接器(armlink)的核心输入之一。完整的编译流程如下:
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ 源文件 │───▶│ 编译器 │───▶│ 目标文件 │───▶│ 链接器 │───▶│ .axf │
│ .c .s │ │ armcc │ │ .o │ │ armlink │ │ 可执行 │
│ .cpp │ │ armasm │ │ │ │ │ │ 文件 │
└──────────┘ └──────────┘ └──────────┘ └────┬─────┘ └────┬─────┘
│ │
┌───────┘ │
▼ ▼
┌────────┐ ┌──────────┐
│ .sct │ │ .bin/.hex│
│ 链接文件│ │ 烧录文件 │
└────────┘ └──────────┘
各阶段说明:
| 阶段 | 工具 | 输入 | 输出 | 说明 |
|---|---|---|---|---|
| 编译 | armcc / armasm | .c / .s |
.o |
词法分析、语法分析、生成目标文件 |
| 汇编 | armasm | .s |
.o |
汇编指令翻译为机器码 |
| 链接 | armlink | .o + .sct |
.axf |
符号解析、地址分配、段合并 |
| 格式转换 | fromelf | .axf |
.bin / .hex |
生成烧录文件 |
💡 工程师经验: 链接阶段是最容易出问题的环节。编译能通过不代表链接能通过,常见的
L6236E、L6218E等错误都发生在链接阶段。理解.sct文件是排查这些错误的基础。
1.3 为什么需要手动编写 .sct?
对于简单的单程序工程,Keil 可以通过 Options for Target → Target 选项卡自动生成 .sct 文件。但在以下场景中,必须手动编写或修改 .sct 文件:
| 场景 | 说明 |
|---|---|
| Bootloader + App | 需要精确控制 App 的起始地址,避免覆盖 Bootloader |
| OTA 升级 | 需要划分多个 Flash 分区(Boot、App1、App2、参数区) |
| CCMRAM 使用 | STM32F4 系列的 CCMRAM 需要单独的执行域 |
| RAM 执行函数 | 将关键函数放到 RAM 中执行以提升性能或支持 Flash 擦写时运行 |
| 外部 RAM 扩展 | 使用 FSMC/FMC 连接外部 SRAM/SDRAM |
| XIP(片外执行) | 代码在外部 Flash 中执行 |
| 多核共享内存 | 双核 MCU 中定义共享内存区域 |
⚠️ 注意事项: 手动编写
.sct文件后,需要在 Keil 的 Options for Target → Linker 选项卡中取消勾选 “Use Memory Layout from Target Dialog”,并指定你的.sct文件路径。否则 Keil 会用自动生成的布局覆盖你的配置。
二、分散加载文件核心概念
2.1 加载域(Load Region)
加载域(Load Region)是分散加载文件的最外层结构,定义了程序烧录到 Flash 时的布局。所有加载域以 LR_ 作为前缀。
语法格式:
LR_名称 起始地址 [最大大小] [属性]
{
...执行域...
}
参数说明:
| 参数 | 说明 | 示例 |
|---|---|---|
| 名称 | 加载域标识符,自定义命名 | LR_IROM1 |
| 起始地址 | 加载域在 Flash 中的起始物理地址 | 0x08000000 |
| 最大大小 | 该加载域允许的最大字节数(可选) | 0x00080000 |
| 属性 | 加载域属性(可选) | NOCOMPRESS |
示例:
; 定义一个从 0x08000000 开始、最大 512KB 的加载域
LR_IROM1 0x08000000 0x00080000
{
...
}
💡 工程师经验: 一个
.sct文件可以包含多个加载域,这在多 App 分区方案中非常有用。每个加载域对应 Flash 上的一个独立区域,链接器会为每个加载域生成独立的加载描述信息。
2.2 执行域(Execution Region)
执行域(Execution Region)嵌套在加载域内部,定义了程序运行时各段在内存中的位置。所有执行域以 ER_ 作为前缀。
语法格式:
ER_名称 起始地址 [最大大小] [属性]
{
...模块选择规则...
}
参数说明:
| 参数 | 说明 | 示例 |
|---|---|---|
| 名称 | 执行域标识符,自定义命名 | ER_IROM1 |
| 起始地址 | 执行域的起始地址(可以是 Flash 或 RAM 地址) | 0x08000000 |
| 最大大小 | 该执行域允许的最大字节数(可选) | 0x00080000 |
| 属性 | 段类型属性,如 +RO、+RW、+ZI |
+RO |
示例:
; Flash 中的代码执行域
ER_IROM1 0x08000000 0x00080000 {
*.o (RESET, +First) ; 中断向量表放在最前面
*(InRoot$$Sections) ; C 库初始化代码
.ANY (+RO) ; 所有其他代码和只读数据
}
; RAM 中的数据执行域
ER_IRAM1 0x20000000 0x00020000 {
.ANY (+RW +ZI) ; 所有读写数据和零初始化数据
}
💡 工程师经验: 执行域的起始地址不一定要和加载域相同。当执行域的地址是 RAM 地址时,链接器会自动生成搬运代码,将数据从 Flash(加载地址)搬运到 RAM(执行地址)。这就是
.data段初始化的原理。
2.3 加载域 vs 执行域的本质区别
这是理解分散加载文件最核心的概念:
┌─────────────────────────────────────────────────────────────────┐
│ 加载域 (Load Region) │
│ = 烧录时 Flash 中的布局 │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌───────────────┐ │
│ │ RESET 向量表 │ │ 代码 +RO │ │ .data 初始值 │ │
│ │ 0x08000000 │ │ 0x080000C0 │ │ 0x08010000 │ │
│ └──────────────────┘ └──────────────────┘ └───────┬───────┘ │
│ │ │
│ 运行时搬运(__scatterload) │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ .data (RAM) │ │
│ │ 0x20000000 │ │
│ └──────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 执行域 (Execution Region) │
│ = 运行时内存中的布局 │
│ │
│ Flash 端: │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ RESET 向量表 │ │ 代码 +RO │ │
│ │ 0x08000000 │ │ 0x080000C0 │ │
│ └──────────────────┘ └──────────────────┘ │
│ │
│ RAM 端: │
│ ┌──────────────────┐ ┌──────────────────┐ ┌───────────────┐ │
│ │ .data +RW │ │ .bss +ZI │ │ Heap / Stack │ │
│ │ 0x20000000 │ │ 0x20000100 │ │ 0x2000XXXX │ │
│ └──────────────────┘ └──────────────────┘ └───────────────┘ │
└─────────────────────────────────────────────────────────────────┘
核心区别:
| 概念 | 加载域 (LR) | 执行域 (ER) |
|---|---|---|
| 含义 | 烧录时在 Flash 中的物理布局 | 运行时代码/数据在内存中的布局 |
| 对应术语 | LMA(Load Memory Address) | VMA(Virtual Memory Address) |
| 存储介质 | 通常是 Flash | 可以是 Flash 或 RAM |
| 数量关系 | 一个 .sct 可有多个 LR |
每个 LR 内可有多个 ER |
| 地址关系 | LR 地址 = ER 地址(代码段) | LR 地址 ≠ ER 地址(数据段) |
💡 工程师经验: 当执行域的起始地址与加载域的起始地址相同时(如代码段),代码直接在 Flash 中原地执行(XIP)。当执行域的起始地址是 RAM 地址时,链接器会在
.sct中记录加载地址和执行地址的对应关系,运行时由__scatterload自动完成搬运。这就是为什么.data段在 Flash 中有一份初始值副本,在 RAM 中又有一份运行时副本。
2.4 段类型:+RO / +RW / +ZI
ARMCC 链接器将程序内容分为三种基本段类型:
| 段类型 | 全称 | 含义 | 对应 C 语言概念 | 存储位置 |
|---|---|---|---|---|
| +RO | Read Only | 只读段 | const 变量、字符串常量、函数代码 |
Flash |
| +RW | Read Write | 可读写段 | 已初始化的全局/静态变量(.data) |
Flash(加载时)→ RAM(运行时) |
| +ZI | Zero Init | 零初始化段 | 未初始化的全局/静态变量(.bss) |
RAM(运行时清零) |
详细说明:
C 语言代码 段类型 加载域(Flash) 执行域(RAM)
───────── ────── ───────────── ────────────
const int g_val = 100; +RO ✅ 存放 ✅ 运行时读取
int g_init = 42; +RW ✅ 存放初始值 ✅ 运行时读写
int g_uninit; +ZI ❌ 不占空间 ✅ 运行时清零
void func(void) { ... } +RO ✅ 存放 ✅ 运行时执行
static int s_var = 1; +RW ✅ 存放初始值 ✅ 运行时读写
💡 工程师经验:
+ZI段在 Flash 中不占用任何空间,因为它在运行时直接在 RAM 中清零即可。这也是为什么.bss段不计入.bin文件大小的原因。但是,+ZI段在.map文件中会显示其大小,因为它确实消耗了 RAM 空间。在计算 Flash 占用时,只需要关注+RO和+RW的大小。
⚠️ 注意事项:
+RW段比较特殊:它在加载域中占用 Flash 空间(存储初始值),在执行域中占用 RAM 空间(运行时使用)。所以一个已初始化的全局变量会同时消耗 Flash 和 RAM。如果你发现 RAM 不够用,可以检查是否有大量已初始化的全局变量可以改为const(移到+RO)。
三、.sct 文件完整语法详解
3.1 基本语法结构
分散加载文件采用嵌套结构,从外到内依次是:加载域 → 执行域 → 模块选择规则。
; ═══════════════════════════════════════════════════════════
; 加载域(Load Region):定义烧录布局
; ═══════════════════════════════════════════════════════════
LR_名称 起始地址 [大小] [属性]
{
; ───────────────────────────────────────────────────────
; 执行域(Execution Region):定义运行时布局
; ───────────────────────────────────────────────────────
ER_名称 起始地址 [大小] [属性]
{
; 模块选择规则:决定哪些目标文件的哪些段放入此执行域
*.o (RESET, +First) ; 特定模块的特定段
*(InRoot$$Sections) ; C 库根段
.ANY (+RO) ; 任意模块的只读段
.ANY (+RW +ZI) ; 任意模块的读写段
}
}
💡 工程师经验:
.sct文件中的注释使用分号;开头,不是 C 语言的//。这是一个容易忽略的细节。
3.2 模块选择规则
模块选择规则(Module Selector)决定了哪些目标文件的哪些段被放入当前执行域。
语法格式:
模块名 (段名, 属性)
常用规则详解:
| 规则 | 含义 | 说明 |
|---|---|---|
*.o (RESET, +First) |
所有目标文件中的 RESET 段,放在最前面 |
中断向量表必须放在 Flash 起始地址 |
*(InRoot$$Sections) |
C 库内部初始化段 | 包含 __scatterload 等关键代码 |
.ANY (+RO) |
任意模块的只读段 | 匹配所有未被前面规则匹配的 +RO 段 |
.ANY (+RW +ZI) |
任意模块的读写段和零初始化段 | 匹配所有未被前面规则匹配的 +RW 和 +ZI 段 |
main.o (+RO) |
仅 main.o 的只读段 |
精确控制特定模块的放置 |
匹配优先级:
模块选择规则的匹配顺序非常重要,从上到下依次匹配,先匹配先生效:
ER_IROM1 0x08000000 0x00080000
{
; 1. 最高优先级:中断向量表,必须放在 0x08000000
*.o (RESET, +First)
; 2. 次高优先级:C 库初始化代码
*(InRoot$$Sections)
; 3. 通配:所有剩余的只读段(代码 + 常量)
.ANY (+RO)
}
💡 工程师经验:
+First属性确保RESET段(中断向量表)被放在执行域的最起始位置。这对于 Cortex-M 系列处理器至关重要,因为内核复位后会从0x00000000(或 Flash 映射地址0x08000000)读取初始栈指针和复位向量。如果向量表不在正确位置,程序将无法启动。
3.3 执行域属性
执行域可以携带多种属性,控制段的放置行为:
| 属性 | 含义 | 使用场景 |
|---|---|---|
+RO |
只读段 | 代码和常量 |
+RW |
可读写段 | 已初始化全局变量 |
+ZI |
零初始化段 | 未初始化全局变量 |
+FIRST |
放在域的最前面 | 中断向量表 |
+LAST |
放在域的最后面 | 校验和、版本信息 |
NOCOMPRESS |
不压缩 RW 数据 | 需要精确控制数据布局时 |
PADVALUE |
填充值 | 对齐填充 |
OVERLAY |
覆盖执行域 | 多个执行域共享同一块 RAM |
FIXED |
固定地址 | 执行域地址不可被链接器调整 |
属性组合示例:
; 不压缩 RW 数据,使用 0xFF 填充
ER_IRAM1 0x20000000 0x00020000 NOCOMPRESS PADVALUE 0xFF
{
.ANY (+RW +ZI)
}
⚠️ 注意事项: 默认情况下,ARMCC 链接器会压缩 RW 数据的初始值以节省 Flash 空间。如果使用调试器直接查看 Flash 中的 RW 初始值,可能会看到压缩后的数据而非原始值。如果需要看到原始值,可以在执行域上添加
NOCOMPRESS属性。
3.4 STM32F407 标准示例
以下是一个完整的 STM32F407VG(1MB Flash, 192KB RAM + 64KB CCMRAM)的标准 .sct 文件:
; *************************************************************
; *** Keil ARMCC Scatter-Loading Description File ***
; *** Target: STM32F407VG ***
; *** Flash: 1MB (0x08000000 - 0x080FFFFF) ***
; *** SRAM: 128KB (0x20000000 - 0x2001FFFF) ***
; *** CCM: 64KB (0x10000000 - 0x1000FFFF) ***
; *************************************************************
; ========================================
; 加载域:从 0x08000000 开始,最大 1MB
; ========================================
LR_IROM1 0x08000000 0x00100000
{
; ====================================
; 执行域 1:Flash 代码区
; 地址空间:0x08000000 - 0x080FFFFF
; ====================================
ER_IROM1 0x08000000 0x00100000
{
; 中断向量表必须放在最前面
*.o (RESET, +First)
; C 库初始化代码(__scatterload 等)
*(InRoot$$Sections)
; 所有代码和只读数据
.ANY (+RO)
}
; ====================================
; 执行域 2:SRAM 数据区
; 地址空间:0x20000000 - 0x2001FFFF
; ====================================
ER_IRAM1 0x20000000 0x00020000
{
; 所有已初始化和未初始化的全局变量
.ANY (+RW +ZI)
}
}
内存布局图:
Flash (0x08000000, 1MB) SRAM (0x20000000, 128KB)
┌────────────────────────┐ ┌────────────────────────┐
│ 0x08000000 │ │ 0x20000000 │
│ ┌────────────────────┐ │ │ ┌────────────────────┐ │
│ │ RESET (向量表) │ │ │ │ .data (已初始化变量)│ │
│ │ +First │ │ __scatterload │ │ 从 Flash 搬运过来 │ │
│ ├────────────────────┤ │ ──────────────▶ │ ├────────────────────┤ │
│ │ InRoot$$Sections │ │ 运行时搬运 │ │ .bss (未初始化变量) │ │
│ │ (__scatterload等) │ │ │ │ 运行时清零 │ │
│ ├────────────────────┤ │ │ ├────────────────────┤ │
│ │ .text (代码段) │ │ │ │ Heap (堆) │ │
│ │ .rodata (只读数据) │ │ │ │ 向高地址增长 │ │
│ │ +RO │ │ │ ├────────────────────┤ │
│ │ │ │ │ │ Stack (栈) │ │
│ ├────────────────────┤ │ │ │ 向低地址增长 │ │
│ │ .data 初始值副本 │ │ │ │ │ │
│ │ (用于搬运到 RAM) │ │ │ │ │ │
│ └────────────────────┘ │ │ └────────────────────┘ │
│ 0x080FFFFF │ │ 0x2001FFFF │
└────────────────────────┘ └────────────────────────┘
💡 工程师经验: 上述
.sct文件中,ER_IROM1的起始地址0x08000000与LR_IROM1相同,说明代码直接在 Flash 中执行(XIP)。而ER_IRAM1的起始地址0x20000000与LR_IROM1的0x08000000不同,说明.data段需要从 Flash 搬运到 RAM。
3.5 带多个执行域的示例(含 CCMRAM)
当需要使用 STM32F4 的 CCMRAM(Core Coupled Memory RAM)时,需要添加额外的执行域:
; *************************************************************
; *** STM32F407VG with CCMRAM ***
; *************************************************************
LR_IROM1 0x08000000 0x00100000
{
; Flash 代码区
ER_IROM1 0x08000000 0x00100000
{
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
; 主 SRAM 数据区 (128KB)
ER_IRAM1 0x20000000 0x00020000
{
.ANY (+RW +ZI)
}
; CCMRAM 数据区 (64KB)
; 地址: 0x10000000 - 0x1000FFFF
ER_IRAM2 0x10000000 0x00010000
{
; 只有使用 __attribute__((section("CCMRAM_SECTION")))
; 标记的变量才会被放到这里
*.o (CCMRAM_SECTION)
}
}
对应的 C 代码使用方法:
/* 将变量放到 CCMRAM 中 */
__attribute__((section("CCMRAM_SECTION"))) uint32_t fast_buffer[1024];
/* 将数组放到 CCMRAM 中 */
__attribute__((section("CCMRAM_SECTION")))
uint8_t dma_rx_buffer[256];
内存布局图:
Flash (0x08000000) SRAM (0x20000000, 128KB) CCMRAM (0x10000000, 64KB)
┌──────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ RESET 向量表 │ │ .data / .bss │ │ CCMRAM_SECTION │
│ 代码 +RO │ │ Heap / Stack │ │ (手动指定的变量) │
│ .data 初始值 │ │ │ │ │
└──────────────┘ └──────────────────┘ └──────────────────┘
⚠️ 注意事项: CCMRAM 连接在 D-Bus 上,DMA 无法访问!如果你将 DMA 缓冲区放到 CCMRAM 中,DMA 传输将失败或读取到错误数据。这是一个非常常见的坑。CCMRAM 的优势是 CPU 访问零等待,适合存放高频访问的变量(如查找表、中间计算结果等)。
💡 工程师经验: 在 Keil 中配置 CCMRAM 时,除了修改
.sct文件外,还需要确保启动代码中正确初始化了 CCMRAM 所在的地址范围。对于 STM32F4,CCMRAM 默认是可用的,但建议在系统初始化时显式使能。
四、链接器自动生成的代码
4.1 __main 函数
很多初学者以为程序从 main() 开始执行,但实际上,在 main() 之前,链接器已经做了大量工作。ARMCC 链接器会自动生成一个名为 __main 的函数作为程序的真正入口。
⚠️ 注意事项:
__main(双下划线)是链接器生成的函数,与用户编写的main()(单下划线)完全不同!不要混淆。
__main 的内部执行流程:
复位向量 (RESET)
│
▼
SystemInit() ← 启动文件 startup_stm32f407xx.s 中的 Reset_Handler 调用
│
▼
__main ← 链接器自动生成的入口函数
│
├──▶ __scatterload() ← 第一步:分散加载(搬运 .data,清零 .bss)
│
└──▶ __rt_entry() ← 第二步:C 运行时初始化
│
├──▶ __rt_stackheap_init() ← 初始化堆栈
├──▶ __rt_lib_init() ← C 库初始化
│ ├──▶ __fp_init() ← 浮点运算初始化
│ └──▶ 构造函数调用 ← C++ 全局对象构造
│
└──▶ main() ← 最终调用用户的 main()
│
▼
用户程序
💡 工程师经验: 你可以在调试时查看反汇编窗口,确认程序确实经过了
__main→__scatterload→__rt_entry→main()的完整流程。如果程序在main()之前就崩溃了,问题很可能出在__scatterload阶段(例如.sct配置错误导致搬运地址越界)。
4.2 __scatterload
__scatterload 是 ARMCC 链接器自动生成的核心函数,负责完成分散加载任务。它的主要工作包括:
- 读取分散加载描述表:链接器在编译时根据
.sct文件生成一张描述表,记录了每个需要搬运的段的加载地址和执行地址。 - 搬运
.data段:将已初始化全局变量从 Flash 搬运到 RAM。 - 清零
.bss段:将未初始化全局变量所在的 RAM 区域清零。
工作原理图示:
编译时(链接器):
┌─────────────────────┐
│ .sct 文件 │
│ (分散加载描述) │
└─────────┬───────────┘
│ armlink 读取
▼
┌─────────────────────────────────────────────────────┐
│ 分散加载描述表(Region Table) │
│ │
│ ┌──────────┬──────────────┬──────────────┬────────┐│
│ │ 段名称 │ 加载地址(LMA) │ 执行地址(VMA) │ 大小 ││
│ ├──────────┼──────────────┼──────────────┼────────┤│
│ │ .data │ 0x08010000 │ 0x20000000 │ 0x100 ││
│ │ .data2 │ 0x08010100 │ 0x20000100 │ 0x200 ││
│ │ .bss │ N/A │ 0x20000300 │ 0x300 ││
│ └──────────┴──────────────┴──────────────┴────────┘│
└─────────────────────────────────────────────────────┘
│ 编译到 .axf 中
▼
运行时(__scatterload):
┌─────────────────────────────────────────────────────┐
│ for each region in Region Table: │
│ if (region.type == RW): │
│ memcpy(region.VMA, region.LMA, region.size) │
│ if (region.type == ZI): │
│ memset(region.VMA, 0, region.size) │
└─────────────────────────────────────────────────────┘
实际搬运过程:
搬运前:
Flash: [0x42, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, ...] ← .data 初始值
RAM: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ...] ← 未初始化
搬运后:
Flash: [0x42, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, ...] ← .data 初始值(保留)
RAM: [0x42, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, ...] ← .data 运行时值
[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ...] ← .bss 已清零
💡 工程师经验:
__scatterload的执行时间取决于需要搬运的数据量。如果一个项目有大量的已初始化全局变量(比如大的查找表),启动时间会明显变长。优化方法:将大的只读查找表声明为const(放入+RO段,不需要搬运),或者使用__attribute__((section("AT_FLASH")))配合手动初始化。
4.3 __rt_entry
__rt_entry 是 C 运行时库的入口函数,在 __scatterload 完成后调用。它负责:
- 堆栈初始化:根据
.sct中的堆栈配置设置 Heap 和 Stack - C 库初始化:初始化
stdio、malloc等标准库功能 - 浮点初始化:配置浮点运算单元(如果使用 FPU)
- C++ 构造函数:调用全局 C++ 对象的构造函数(C++ 项目)
- 调用
main():最终跳转到用户编写的main()函数
__rt_entry 执行流程:
│
├── __rt_stackheap_init()
│ 设置 Heap Base, Heap Limit, Stack Base, Stack Limit
│
├── __rt_lib_init()
│ ├── __fp_init() ← FPU 初始化
│ ├── __init_alloc() ← 内存分配器初始化
│ ├── __init_stdio() ← 标准I/O初始化
│ └── __cpp_initialize__() ← C++ 构造函数
│
└── main() ← 跳转到用户 main()
💡 工程师经验: 在 Keil 的启动文件(如
startup_stm32f407xx.s)中,Stack_Size和Heap_Size是通过汇编定义的。链接器会根据这些值自动配置__rt_entry中的堆栈参数。如果你需要精确控制堆栈大小,可以直接在.sct文件中定义,或者在启动文件中修改Stack_Size和Heap_Size的值。
4.4 Keil 的启动流程总结
将上述内容串联起来,完整的启动流程如下:
上电复位
│
▼
CPU 从 0x00000000 读取初始 SP(MSP)
CPU 从 0x00000004 读取 Reset_Handler 地址
│
▼
Reset_Handler (startup_stm32f407xx.s)
│
├── 配置系统时钟 (调用 SystemInit)
├── 拷贝 .data 段(如果启动文件中有手动拷贝代码)
├── 清零 .bss 段(如果启动文件中有手动清零代码)
│
▼
调用 __main(链接器生成)
│
├── __scatterload
│ ├── 根据 .sct 描述表搬运 .data
│ └── 根据 .sct 描述表清零 .bss
│
├── __rt_entry
│ ├── 堆栈初始化
│ ├── C 库初始化
│ └── C++ 构造函数
│
└── main()
│
▼
用户程序开始运行
💡 工程师经验: 注意上面的流程中,ARMCC 的启动文件(
startup_stm32f407xx.s)中通常也包含.data拷贝和.bss清零的代码。但实际上,当使用分散加载文件时,__scatterload会完成这些工作。这意味着.data拷贝和.bss清零实际上会被执行两次。不过不用担心,重复执行不会造成问题(已初始化的值被覆盖为相同的值,已清零的区域再次清零)。Keil 官方模板中的启动文件保留了这些代码是为了兼容不使用.sct文件的情况。
五、Keil 自动链接符号
5.1 Image$$ 符号体系
ARMCC 链接器会为每个执行域自动生成一组符号,这些符号以 Image$$ 为前缀,可以在 C 代码中直接引用。这些符号对于获取段的地址和大小非常有用。
符号命名规则:
Image$$执行域名称$$属性
每个执行域自动生成以下 6 个符号:
| 符号 | 含义 | 说明 |
|---|---|---|
Image$$ER_IROM1$$Base |
执行域的起始地址 | 对应 +RO 段的起始地址 |
Image$$ER_IROM1$$Length |
执行域的总长度(字节) | 包含 +RO + RW + ZI |
Image$$ER_IROM1$$Limit |
执行域的结束地址 | Base + Length |
Image$$ER_IROM1$$ZI$$Base |
ZI 段的起始地址 | .bss 段的起始地址 |
Image$$ER_IROM1$$ZI$$Length |
ZI 段的长度(字节) | .bss 段的大小 |
Image$$ER_IROM1$$ZI$$Limit |
ZI 段的结束地址 | ZI B a s e + Z I Base + ZI Base+ZILength |
对于数据执行域(如 ER_IRAM1),还有额外的符号:
| 符号 | 含义 |
|---|---|
Image$$ER_IRAM1$$RW$$Base |
RW 段(.data)的起始地址 |
Image$$ER_IRAM1$$RW$$Length |
RW 段(.data)的长度 |
Image$$ER_IRAM1$$RW$$Limit |
RW 段(.data)的结束地址 |
💡 工程师经验: 这些符号在
.map文件的 “Image Symbol Table” 部分可以查看到完整的列表。如果你不确定某个符号是否存在,先查看.map文件。
5.2 在 C 代码中使用自动符号
在 C 代码中,需要通过 extern 声明来引用这些符号:
#include <stdint.h>
/* 声明链接器自动生成的符号 */
extern uint32_t Image$$ER_IROM1$$Base;
extern uint32_t Image$$ER_IROM1$$Length;
extern uint32_t Image$$ER_IROM1$$Limit;
extern uint32_t Image$$ER_IRAM1$$RW$$Base;
extern uint32_t Image$$ER_IRAM1$$RW$$Length;
extern uint32_t Image$$ER_IRAM1$$RW$$Limit;
extern uint32_t Image$$ER_IRAM1$$ZI$$Base;
extern uint32_t Image$$ER_IRAM1$$ZI$$Length;
extern uint32_t Image$$ER_IRAM1$$ZI$$Limit;
/* 使用示例:打印内存布局信息 */
void print_memory_layout(void)
{
uint32_t code_base = (uint32_t)&Image$$ER_IROM1$$Base;
uint32_t code_size = (uint32_t)&Image$$ER_IROM1$$Length;
uint32_t rw_base = (uint32_t)&Image$$ER_IRAM1$$RW$$Base;
uint32_t rw_size = (uint32_t)&Image$$ER_IRAM1$$RW$$Length;
uint32_t zi_base = (uint32_t)&Image$$ER_IRAM1$$ZI$$Base;
uint32_t zi_size = (uint32_t)&Image$$ER_IRAM1$$ZI$$Length;
printf("Code: 0x%08X - Size: %u bytes\r\n", code_base, code_size);
printf("RW: 0x%08X - Size: %u bytes\r\n", rw_base, rw_size);
printf("ZI: 0x%08X - Size: %u bytes\r\n", zi_base, zi_size);
}
⚠️ 注意事项: 声明
Image$$符号时,类型使用uint32_t(或unsigned int),但取地址时使用&运算符。这是因为链接器生成的符号是地址,在 C 中需要取其地址来获取实际值。这是 ARMCC 的特殊用法,不要尝试直接使用符号名而不加&。
5.3 实用技巧:计算可用堆空间
利用 Image$$ 符号,可以精确计算系统剩余的可用 RAM 空间:
#include <stdint.h>
#include <stdio.h>
/* 链接器符号声明 */
extern uint32_t Image$$ER_IRAM1$$ZI$$Limit; /* ZI 段结束地址 */
extern uint32_t Image$$RW_IRAM1$$ZI$$Limit; /* 另一种命名格式 */
/*
* 获取可用堆空间大小
* 计算方法:RAM 结束地址 - ZI 段结束地址 - 栈大小
*/
uint32_t get_free_heap_size(void)
{
extern uint32_t Image$$ER_IRAM1$$ZI$$Limit;
/* RAM 区域结束地址 */
const uint32_t RAM_END = 0x20020000; /* 0x20000000 + 128KB */
/* 栈大小(与启动文件中的 Stack_Size 一致) */
extern uint32_t Image$$ER_IRAM1$$ZI$$Limit;
/* 栈顶地址(RAM 的最高地址) */
uint32_t stack_top = RAM_END;
/* ZI 段结束地址(栈底地址) */
uint32_t zi_end = (uint32_t)&Image$$ER_IRAM1$$ZI$$Limit;
/* 可用堆空间 = 栈底 - ZI 段结束地址 */
uint32_t free_heap = stack_top - zi_end;
return free_heap;
}
/* 在 main() 中使用 */
int main(void)
{
printf("Free heap: %u bytes (%u KB)\r\n",
get_free_heap_size(),
get_free_heap_size() / 1024);
while (1) {
}
}
内存布局示意:
RAM (0x20000000 - 0x2001FFFF)
┌──────────────────────────────────┐ 0x20000000
│ .data (RW 段) │
├──────────────────────────────────┤
│ .bss (ZI 段) │
├──────────────────────────────────┤ ← Image$$ER_IRAM1$$ZI$$Limit
│ │
│ 可用 Heap 空间 │ ← get_free_heap_size() 计算的区域
│ │
├──────────────────────────────────┤
│ Stack (栈) │ ← 向低地址增长
└──────────────────────────────────┘ 0x2001FFFF (RAM_END)
💡 工程师经验: 在使用 RTOS(如 FreeRTOS、RT-Thread)时,堆空间的管理由 RTOS 接管。此时
Image$$符号可以帮助你确定可以给 RTOS 分配多少堆内存。建议在系统初始化时打印一次内存布局信息,方便后续排查内存不足的问题。
六、实战场景
6.1 Bootloader + App 双固件架构
在 OTA 升级场景中,通常需要将 Flash 划分为 Bootloader 和 App 两个区域。
Flash 分区规划(STM32F407VG, 1MB Flash):
Flash (0x08000000 - 0x080FFFFF)
┌──────────────────────────────┐ 0x08000000
│ │
│ Bootloader (64KB) │
│ 0x08000000-0x0800FFFF │
│ │
├──────────────────────────────┤ 0x08010000
│ │
│ App (896KB) │
│ 0x08010000-0x080EFFFF │
│ │
├──────────────────────────────┤ 0x080F0000
│ │
│ 参数区 (64KB) │
│ 0x080F0000-0x080FFFFF │
│ │
└──────────────────────────────┘ 0x08100000
Bootloader 的 .sct 文件:
; *************************************************************
; *** Bootloader Scatter File ***
; *** Flash: 0x08000000 - 0x0800FFFF (64KB) ***
; *** SRAM: 0x20000000 - 0x2001FFFF (128KB) ***
; *************************************************************
LR_IROM1 0x08000000 0x00010000
{
ER_IROM1 0x08000000 0x00010000
{
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
ER_IRAM1 0x20000000 0x00020000
{
.ANY (+RW +ZI)
}
}
App 的 .sct 文件:
; *************************************************************
; *** App Scatter File ***
; *** Flash: 0x08010000 - 0x080EFFFF (896KB) ***
; *** SRAM: 0x20000000 - 0x2001FFFF (128KB) ***
; *************************************************************
LR_IROM1 0x08010000 0x000E0000
{
ER_IROM1 0x08010000 0x000E0000
{
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
ER_IRAM1 0x20000000 0x00020000
{
.ANY (+RW +ZI)
}
}
App 中设置中断向量表偏移(VTOR):
/* 在 App 的 SystemInit() 或 main() 开头设置 VTOR */
#define APP_FLASH_BASE 0x08010000U
void main(void)
{
/* 设置向量表偏移到 App 起始地址 */
SCB->VTOR = APP_FLASH_BASE;
/* 后续初始化代码... */
HAL_Init();
SystemClock_Config();
/* 用户应用逻辑 */
while (1) {
}
}
💡 工程师经验: VTOR(Vector Table Offset Register)必须在 App 初始化的最早期设置,最好在
SystemInit()中就设置。如果在 VTOR 设置之前发生了中断,CPU 会跳转到 Bootloader 的中断处理函数,导致不可预期的行为。另外,App 的.sct中LR_IROM1的起始地址必须与 VTOR 的值一致。
⚠️ 注意事项: App 编译时需要确保链接地址正确。在 Keil 的 Options for Target → C/C++ 选项卡中,如果定义了
VECT_TAB_OFFSET宏,需要确保其值与.sct中的起始地址一致。例如,VECT_TAB_OFFSET=0x10000对应0x08010000。
6.2 将函数放到 RAM 中执行
在某些场景下,需要将函数放到 RAM 中执行:
- 提升性能:CCMRAM 和主 SRAM 的访问速度可能比 Flash 快
- Flash 擦写时执行:在 OTA 升级过程中擦写 Flash 时,代码不能在 Flash 中执行
- 降低功耗:某些低功耗场景下关闭 Flash 加速器
步骤一:修改 .sct 文件,添加 RAM 执行域
LR_IROM1 0x08000000 0x00100000
{
ER_IROM1 0x08000000 0x00100000
{
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
ER_IRAM1 0x20000000 0x00020000
{
.ANY (+RW +ZI)
}
; RAM 执行域:用于存放需要在 RAM 中执行的函数
; 注意:这个域的属性是 +RO(代码),但地址是 RAM
ER_RAM_CODE 0x20000000 0x00001000
{
*.o (RAM_CODE_SECTION)
}
}
步骤二:在 C 代码中使用 __attribute__((section))
/* 将函数放到 RAM 中执行 */
__attribute__((section("RAM_CODE_SECTION")))
void flash_erase_page(uint32_t page_addr)
{
/* 解锁 Flash */
HAL_FLASH_Unlock();
/* 清除错误标志 */
__HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_OPERR |
FLASH_FLAG_WRPERR | FLASH_FLAG_PGAERR |
FLASH_FLAG_PGPERR | FLASH_FLAG_PGSERR);
/* 擦除页 */
FLASH_EraseInitTypeDef erase_init;
uint32_t page_error = 0;
erase_init.TypeErase = FLASH_TYPEERASE_SECTORS;
erase_init.Sector = page_addr;
erase_init.NbSectors = 1;
erase_init.VoltageRange = FLASH_VOLTAGE_RANGE_3;
HAL_FLASHEx_Erase(&erase_init, &page_error);
/* 锁定 Flash */
HAL_FLASH_Lock();
}
/* 将关键算法放到 RAM 中执行以提升性能 */
__attribute__((section("RAM_CODE_SECTION")))
void fast_fft_process(int16_t *input, int16_t *output, uint32_t len)
{
/* FFT 算法实现... */
/* 在 RAM 中执行可以避免 Flash 等待周期 */
}
RAM 执行函数的内存布局:
Flash (0x08000000) RAM (0x20000000)
┌──────────────────┐ ┌──────────────────┐
│ RESET 向量表 │ │ RAM_CODE_SECTION │
│ 普通代码 +RO │ │ (flash_erase_page│
│ │ 搬运 │ fast_fft_process│
│ RAM_CODE 函数 │ ─────────▶ │ 等函数的副本) │
│ 初始值副本 │ __scatter │ │
│ (加载时存储) │ load ├──────────────────┤
├──────────────────┤ │ .data / .bss │
│ .data 初始值 │ │ Heap / Stack │
└──────────────────┘ └──────────────────┘
💡 工程师经验: 将函数放到 RAM 中执行会同时消耗 Flash 和 RAM:Flash 中存储函数的初始值副本(加载域),RAM 中存储运行时代码(执行域)。因此需要权衡性能提升与内存开销。通常只将少量关键函数放到 RAM 中,而不是整个程序。
⚠️ 注意事项: 使用 RAM 执行函数时,函数内部调用的其他函数也需要在 RAM 中,否则调用链中只要有一环在 Flash 中,就无法在 Flash 擦写时安全执行。建议将 RAM 执行函数设计为自包含的(不调用外部函数),或者将整个调用链都放到 RAM 中。
6.3 使用 CCMRAM 存放高速数据
STM32F4 系列的 CCMRAM(Core Coupled Memory)是连接在 CPU D-Bus 上的 64KB RAM,CPU 访问零等待状态,但 DMA 不可访问。
.sct 配置:
LR_IROM1 0x08000000 0x00100000
{
ER_IROM1 0x08000000 0x00100000
{
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
ER_IRAM1 0x20000000 0x00020000
{
.ANY (+RW +ZI)
}
; CCMRAM 执行域
ER_IRAM2 0x10000000 0x00010000
{
*.o (CCMRAM_SECTION)
}
}
C 代码使用示例:
/* CRC 查找表 - 放到 CCMRAM 加速计算 */
__attribute__((section("CCMRAM_SECTION")))
static const uint32_t crc32_table[256] = {
0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA,
/* ... 完整的 CRC32 查找表 ... */
};
/* 高频访问的缓冲区 */
__attribute__((section("CCMRAM_SECTION")))
static uint32_t adc_filter_buffer[1024];
/* 图像处理中间缓冲区 */
__attribute__((section("CCMRAM_SECTION")))
static uint8_t image_temp_buffer[4096];
/* ❌ 错误示范:DMA 缓冲区不能放到 CCMRAM!*/
/*
__attribute__((section("CCMRAM_SECTION")))
static uint8_t uart_dma_rx_buffer[256]; // DMA 无法访问!
*/
/* ✅ 正确做法:DMA 缓冲区放到主 SRAM */
static uint8_t uart_dma_rx_buffer[256]; /* 默认放到主 SRAM */
CCMRAM 使用注意事项清单:
✅ 适合放到 CCMRAM 的数据:
- CRC/加密查找表
- 高频访问的滤波器缓冲区
- 图像/音频处理中间结果
- 频繁读写的计算变量
❌ 不能放到 CCMRAM 的数据:
- DMA 缓冲区(UART/SPI/I2C DMA)
- ADC/DAC DMA 缓冲区
- 以太网 DMA 缓冲区
- 需要被其他总线 Master 访问的数据
⚠️ 注意事项: CCMRAM 在 STM32F4 中的地址是
0x10000000,它不在主 SRAM 的地址范围内。如果使用 MPU(Memory Protection Unit),需要为 CCMRAM 单独配置区域。此外,CCMRAM 在睡眠模式下可能会丢失数据(取决于具体型号),需要查阅数据手册确认。
6.4 自定义段放置
除了预定义的段类型,ARMCC 还支持通过 __attribute__((section)) 创建自定义段,并在 .sct 文件中精确控制其放置位置。
通用方法:
/* 定义自定义段变量 */
__attribute__((section("MY_CUSTOM_SECTION")))
uint32_t custom_var1 = 0x12345678;
__attribute__((section("MY_CUSTOM_SECTION")))
uint32_t custom_var2 = 0xABCDEF00;
__attribute__((section("MY_FAST_SECTION")))
uint32_t fast_var1;
对应的 .sct 配置:
LR_IROM1 0x08000000 0x00100000
{
ER_IROM1 0x08000000 0x00100000
{
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
ER_IRAM1 0x20000000 0x00020000
{
.ANY (+RW +ZI)
}
; 自定义段:精确控制变量位置
ER_CUSTOM 0x20000000 0x00001000
{
*.o (MY_CUSTOM_SECTION)
}
}
使用 Image$$ 符号访问自定义段:
extern uint32_t Image$$ER_CUSTOM$$Base;
extern uint32_t Image$$ER_CUSTOM$$Length;
void process_custom_section(void)
{
uint32_t *base = (uint32_t *)&Image$$ER_CUSTOM$$Base;
uint32_t len = (uint32_t)&Image$$ER_CUSTOM$$Length;
printf("Custom section at 0x%08X, size %u bytes\r\n",
(uint32_t)base, len);
/* 遍历自定义段中的所有变量 */
for (uint32_t i = 0; i < len / 4; i++) {
printf(" [%u] = 0x%08X\r\n", i, base[i]);
}
}
💡 工程师经验: 自定义段在模块化设计中非常有用。例如,你可以定义一个
CONFIG_SECTION,将所有配置参数集中放在一个段中,然后通过Image$$符号统一遍历和序列化,实现配置的保存和加载。
6.5 多 App 分区方案
在更复杂的 OTA 升级方案中,可能需要 App1、App2 两个分区实现 A/B 升级:
Flash 分区规划:
Flash (STM32F407VG, 1MB)
┌──────────────────────────────┐ 0x08000000
│ Bootloader (32KB) │
│ 0x08000000-0x08007FFF │
├──────────────────────────────┤ 0x08008000
│ │
│ App1 (464KB) │
│ 0x08008000-0x0807FFFF │
│ │
├──────────────────────────────┤ 0x08080000
│ │
│ App2 (464KB) │
│ 0x08080000-0x080FFFFF │
│ │
├──────────────────────────────┤ 0x08100000
│ │
│ 参数/日志区 (64KB) │
│ 0x08100000-0x0810FFFF │
│ │
└──────────────────────────────┘
App1 的 .sct 文件:
; *************************************************************
; *** App1 Scatter File ***
; *** Flash: 0x08008000 - 0x0807FFFF (464KB) ***
; *** SRAM: 0x20000000 - 0x2001FFFF (128KB) ***
; *************************************************************
LR_IROM1 0x08008000 0x00078000
{
ER_IROM1 0x08008000 0x00078000
{
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
ER_IRAM1 0x20000000 0x00020000
{
.ANY (+RW +ZI)
}
}
App2 的 .sct 文件:
; *************************************************************
; *** App2 Scatter File ***
; *** Flash: 0x08080000 - 0x080FFFFF (464KB) ***
; *** SRAM: 0x20000000 - 0x2001FFFF (128KB) ***
; *************************************************************
LR_IROM1 0x08080000 0x00078000
{
ER_IROM1 0x08080000 0x00078000
{
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
ER_IRAM1 0x20000000 0x00020000
{
.ANY (+RW +ZI)
}
}
💡 工程师经验: App1 和 App2 的
.sct文件结构完全相同,只是LR_IROM1和ER_IROM1的起始地址不同。在实际项目中,通常通过 Keil 的工程配置或构建脚本来切换不同的.sct文件。建议使用宏定义来管理不同的 App 版本,避免维护两份几乎相同的代码。
七、链接文件调试技巧
7.1 查看 Keil 生成的 .map 文件
.map 文件是链接器输出的最重要文件之一,包含了完整的内存布局信息。在 Keil 的 Options for Target → Listing 选项卡中勾选 “Linker Listing” 即可生成。
.map 文件关键部分解读:
===============================================================================
Memory Map of the image
Image Entry Point : 0x08000189
Load Region LR_IROM1 (Base: 0x08000000, Size: 0x00001234, Max: 0x00100000, ABSOLUTE)
Execution Region ER_IROM1 (Base: 0x08000000, Size: 0x00001000, Max: 0x00100000, ABSOLUTE)
Base Addr Size Type Attr Idx E Section Name Object
0x08000000 0x00000188 Data RO 776 RESET startup_stm32f407xx.o
0x08000188 0x00000000 Zero RO 0 * .ARM.Collect$$$$00000000 (from __scatter.o)
...
0x080001C0 0x00000234 Code RO 1234 i.main main.o
...
Execution Region ER_IRAM1 (Base: 0x20000000, Size: 0x00000800, Max: 0x00020000, ABSOLUTE)
Base Addr Size Type Attr Idx E Section Name Object
0x20000000 0x00000100 Data RW 890 .data main.o
0x20000100 0x00000300 Zero RW 891 .bss main.o
...
===============================================================================
Image Symbol Table
Local Symbols
Symbol Name Value Ov Type Size Object(Section)
...
Image$$ER_IROM1$$Base 0x08000000 Number 0 anon$$obj.o(ER_IROM1)
Image$$ER_IROM1$$Length 0x00001000 Number 0 anon$$obj.o(ER_IROM1)
Image$$ER_IROM1$$Limit 0x08001000 Number 0 anon$$obj.o(ER_IROM1)
Image$$ER_IROM1$$ZI$$Base 0x20000100 Number 0 anon$$obj.o(ER_IRAM1)
Image$$ER_IROM1$$ZI$$Length 0x00000300 Number 0 anon$$obj.o(ER_IRAM1)
Image$$ER_IROM1$$ZI$$Limit 0x20000400 Number 0 anon$$obj.o(ER_IRAM1)
...
===============================================================================
Image component sizes
Code (inc. data) RO Data RW Data ZI Data Debug
125,432 12,345 3,456 1,024 8,192 234,567 Grand Totals
...
Total RO Size (Code + RO Data) 128,888 ( 125.88 kB)
Total RW Size (RW Data + ZI Data) 9,216 ( 9.00 kB)
Total ROM Size (Code + RO Data + RW Data) 129,912 ( 126.87 kB)
===============================================================================
关键信息说明:
| 区域 | 含义 | 关注点 |
|---|---|---|
| Memory Map | 完整的内存布局 | 每个段的确切地址和大小 |
| Image Symbol Table | 所有链接符号 | Image$$ 符号的值 |
| Image component sizes | 各类数据大小汇总 | Flash 和 RAM 的总消耗 |
| Total RO Size | Flash 中代码+只读数据大小 | 决定 .bin 文件大小 |
| Total RW Size | RAM 中读写数据大小 | 决定 RAM 消耗 |
| Total ROM Size | Flash 总占用 | = RO + RW(RW 初始值也在 Flash 中) |
💡 工程师经验: 每次编译后都应该快速浏览
.map文件底部的 “Image component sizes” 部分,关注Total ROM Size和Total RW Size的变化。如果发现 Flash 或 RAM 占用突然增大,可以在这里快速定位是哪个模块的增长导致的。
7.2 使用 fromelf 工具分析段布局
fromelf 是 ARMCC 工具链自带的命令行工具,可以用来分析 .axf 文件的段信息。
常用命令:
# 查看所有段的大小
fromelf --text --section_sizes project.axf
# 查看详细的段信息
fromelf --text -z project.axf
# 生成反汇编文件
fromelf --text -c -o project_disasm.txt project.axf
# 生成二进制文件
fromelf --bin -o project.bin project.axf
# 生成 Intel HEX 文件
fromelf --i32 -o project.hex project.axf
# 查看符号表
fromelf --text -s project.axf
# 查看指定段的详细信息
fromelf --text --section=ER_IROM1 project.axf
输出示例:
==============================================================================
** Section #1 'ER_IROM1' (SHT_PROGBITS) [SHF_ALLOC + SHF_EXECINSTR]
Size : 128888 bytes (126.88 kB)
Max : 1048576 bytes (1024.00 kB)
Base : 0x08000000
Attrs : Code, Read-Only, Execute
==============================================================================
** Section #2 'ER_IRAM1' (SHT_NOBITS) [SHF_ALLOC + SHF_WRITE]
Size : 9216 bytes (9.00 kB)
Max : 131072 bytes (128.00 kB)
Base : 0x20000000
Attrs : Read-Write, Zero-Init
==============================================================================
💡 工程师经验: 在 CI/CD 流水线中,可以使用
fromelf --text --section_sizes自动提取段大小信息,并与预设的阈值进行比较。如果 Flash 或 RAM 占用超过阈值,自动触发编译告警或失败,防止固件超出硬件限制。
7.3 常见链接错误及解决方法
以下是 ARMCC 链接器最常见的错误及其解决方法:
| 错误代码 | 含义 | 常见原因 | 解决方法 |
|---|---|---|---|
| L6236E | No section matches selector | .sct 中的模块选择规则没有匹配到任何段 |
检查 .sct 中的段名是否与代码中的 section 属性一致 |
| L6218E | Undefined symbol | 引用了未定义的符号 | 检查是否缺少源文件或库文件;检查函数名拼写 |
| L6406E | No space in execution region | 执行域空间不足 | 增大执行域大小或减少代码/数据量 |
| L6220E | Load region size exceeds limit | 加载域超出最大大小 | 增大加载域的 Max 值或减少 Flash 占用 |
| L6248E | Cannot place region | 无法放置执行域 | 检查执行域地址是否在有效范围内 |
| L6306E | Type mismatch | 段类型不匹配 | 检查 .sct 中的属性是否与实际段类型一致 |
| L6667E | Invalid line in scatter file | .sct 文件语法错误 |
检查语法格式,注意括号匹配和地址格式 |
典型错误分析:
错误示例 1:L6406E
┌─────────────────────────────────────────────────────────┐
│ .\Objects\project.axf: Error: L6406E: No space in │
│ execution region ER_IROM1 with size 0x00010000; │
│ region needs to be at least 0x00012000 bytes │
└─────────────────────────────────────────────────────────┘
原因:代码大小(0x12000 = 72KB)超过了执行域分配的空间(0x10000 = 64KB)
解决:
方法1:增大执行域大小
ER_IROM1 0x08000000 0x00020000 ← 从 64KB 改为 128KB
方法2:优化代码,减小体积
- 开启编译优化 -O2
- 移除未使用的函数(-ffunction-sections + .ANY)
- 检查是否有大的查找表可以放到 Flash 外部
错误示例 2:L6236E
┌─────────────────────────────────────────────────────────┐
│ .\Objects\project.axf: Error: L6236E: No section │
│ matches selector - no section to be FIRST/LAST │
└─────────────────────────────────────────────────────────┘
原因:*.o (RESET, +First) 没有找到 RESET 段
解决:
方法1:确保启动文件(startup_stm32f407xx.s)已添加到工程
方法2:检查启动文件中是否正确定义了 RESET 段
方法3:如果使用自定义向量表,确保段名与 .sct 中一致
错误示例 3:L6218E
┌─────────────────────────────────────────────────────────┐
│ .\Objects\project.axf: Error: L6218E: Undefined symbol │
│ HAL_GPIO_WritePin (referred from main.o) │
└─────────────────────────────────────────────────────────┘
原因:main.c 调用了 HAL_GPIO_WritePin 但没有链接 HAL 库
解决:
方法1:将 stm32f4xx_hal_gpio.c 添加到工程
方法2:检查是否在 stm32f4xx_hal_conf.h 中启用了 HAL_GPIO_MODULE_ENABLED
方法3:检查文件是否被排除编译(灰色图标)
💡 工程师经验: 遇到链接错误时,首先阅读错误信息的完整描述,不要只看错误代码。ARMCC 的错误信息通常包含足够的上下文来定位问题。如果错误信息不够明确,可以查看
.map文件中的内存布局,确认各段的大小和地址是否合理。
7.4 使用 __attribute__((section)) 的注意事项
__attribute__((section("name"))) 是将变量或函数放到指定段的标准方法,但使用时需要注意以下事项:
1. 命名规则
/* ✅ 正确:段名使用大写字母和下划线 */
__attribute__((section("CCMRAM_SECTION")))
__attribute__((section("RAM_CODE_SECTION")))
__attribute__((section("MY_CUSTOM_DATA")))
/* ❌ 错误:段名包含特殊字符 */
__attribute__((section("my-section"))) /* 连字符不合法 */
__attribute__((section("my section"))) /* 空格不合法 */
2. 对齐要求
/* 指定对齐方式 */
__attribute__((section("MY_SECTION"), aligned(4)))
uint32_t aligned_var;
__attribute__((section("MY_SECTION"), aligned(32)))
uint32_t cache_aligned_buffer[8];
/* DMA 缓冲区通常需要 4 字节对齐 */
__attribute__((section("DMA_BUFFER"), aligned(4)))
uint8_t uart_rx_buffer[256];
3. 与 .sct 的配合
/* C 代码中的段名必须与 .sct 中的段名完全一致(区分大小写) */
/* C 代码 */
__attribute__((section("MY_FAST_DATA")))
uint32_t fast_array[128];
/* .sct 文件 */
ER_FAST 0x20000000 0x00001000
{
*.o (MY_FAST_DATA) /* 必须与 C 代码中的段名一致 */
}
4. 初始化变量的特殊处理
/* 已初始化变量放到自定义段时,链接器会自动处理搬运 */
__attribute__((section("MY_INIT_DATA")))
uint32_t config_data[] = {0x01, 0x02, 0x03, 0x04};
/* 未初始化变量放到自定义段时,链接器会自动清零 */
__attribute__((section("MY_ZERO_DATA")))
uint32_t work_buffer[256];
⚠️ 注意事项:
__attribute__((section))的作用域是符号级别的,不是文件级别的。每个需要放到自定义段的变量或函数都需要单独添加属性。如果忘记添加,变量会被放到默认的执行域中,而不是你期望的位置。建议在代码审查时特别关注这一点。
💡 工程师经验: 可以使用宏来简化段属性的书写,提高代码可读性:
/* 定义宏简化段属性 */
#define PLACE_IN_CCMRAM __attribute__((section("CCMRAM_SECTION")))
#define PLACE_IN_RAMCODE __attribute__((section("RAM_CODE_SECTION")))
#define PLACE_IN_FAST __attribute__((section("FAST_DATA_SECTION"), aligned(32)))
/* 使用宏 */
PLACE_IN_CCMRAM uint32_t fast_buffer[1024];
PLACE_IN_RAMCODE void critical_function(void) { /* ... */ }
PLACE_IN_FAST uint32_t cache_friendly_array[64];
总结
本文从分散加载文件的核心概念出发,深入剖析了 ARMCC 链接文件的语法规则、链接器自动生成的代码机制、Image$$ 符号体系,以及多个实战场景的配置方法。以下是核心要点回顾:
-
加载域(LR)= 烧录布局,执行域(ER)= 运行布局。当两者地址不同时,链接器自动生成搬运代码。
-
+RO 是代码和常量,+RW 是已初始化变量,+ZI 是未初始化变量。+RW 同时消耗 Flash(存储初始值)和 RAM(运行时使用)。
-
__main不是用户的main()。它是链接器生成的入口,负责调用__scatterload(搬运数据)和__rt_entry(C 运行时初始化)后,才跳转到用户的main()。 -
Image$$符号是获取段信息的利器。可以用来计算可用堆空间、遍历自定义段、实现配置参数的序列化等。 -
手动编写
.sct是 Bootloader、OTA、CCMRAM 等场景的必备技能。掌握模块选择规则和执行域属性的配置,可以精确控制程序的内存布局。 -
善用
.map文件和fromelf工具。每次编译后检查段大小变化,是预防内存溢出的最佳实践。
参考资源:
- ARM Compiler v5 armlink User Guide - Scatter File Syntax
- ARM Compiler v5 armasm User Guide
- STM32F407xx Reference Manual (RM0090)
- Keil MDK Documentation - Linker Reference
- ARM DUI 0377B - Using the Linker
如果这篇文章对你有帮助,欢迎点赞收藏关注!
📌 系列文章导航:
- 📘 上一篇:STM32F407 启动文件深度解析
- 📗 本文:Keil ARMCC 链接文件(.sct)深度解析
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)