系列目录:本篇是板级适配系列第 4 篇(下)的第 1 部分。承接第 10 篇(上)的「256KB 公寓楼」规则,带你逐间房进去看——
.isr_vector/.text/.data/.bss每段怎么布置、门牌号(VMA)怎么落、行李(LMA)暂存哪。第 2 部分(第 12 篇)讲接口符号契约、ASSERT 与通用照妖镜。第 09 篇结尾思考题的答案在第 12 篇揭晓。
🏠承接上篇导读:上篇说这栋 256KB 公寓楼「楼底(低地址)排前台/办公室/仓库、楼顶(高地址)是栈(杂物)」。本篇就带你逐间房进去看——每间房(段)具体怎么布置、门牌号(VMA)怎么落、行李(LMA)暂存哪。
3.1.isr_vector:门牌号必须 1KB 对齐
一句话理解:向量表必须排在最开头、且按 1KB 对齐,硬件才能正确找到它——同时正好接住第 09 篇的 VTOR。
.isr_vector : { . = ALIGN(1024); KEEP(*(.isr_vector)) /* __Vectors:初始 SP + Reset + 166 外设向量 */ . = ALIGN(4); } > SRAM📘语法小课堂(零基础必看:先搞懂这两个符号):上面这段短短几行,藏着两个最劝退新手的写法,这里一次讲透。
①
*有两张面孔,别混:
- 括号外面的
*= “所有输入文件”。比如*(.isr_vector)整句意思是:“从每一个被链进来的.o目标文件里,把名为.isr_vector的段掏出来,排到这儿。”*管的是"从哪些文件取"。- 括号里面的
*= “段名通配符”(匹配任意字符)。比如*(.text*)里第二个*意思是"段名以.text开头的都算"——.text、.text.startup、.text.our_code统统中招。所以*(.text) *(.text*)是"精确叫.text的 + 所有以.text开头的",合起来一网打尽。- 一句话记:
*在括号外管"从哪些文件",在括号内管"段名怎么匹配"。②
. = ALIGN(N)写前面还是后面,讲究大不同:
链接脚本里有个看不见的"位置计数器".(一个光标,记住当前排到哪个地址)。ALIGN(N)把.向上取整到 N 的整数倍。
- 写在段内容之前(如
.text开头那句. = ALIGN(4)):先把光标挪到 4 字节边界,再开始排本段 → 本段起点对齐。CPU 取 32 位指令/数据从对齐地址最顺,ARM 还强制代码必须落在 4 字节地址。- 写在段内容之后(如
.isr_vector末尾那句. = ALIGN(4)):本段东西都排完了,再把光标补齐到 4 字节边界 → 等于"把本段尾巴收齐,让紧接着的下一段从干净的对齐地址起步"。- 为什么
.isr_vector前后都有:前面ALIGN(1024)是VTOR 的硬规矩——向量表基址必须 1KB 对齐,硬件才认(呼应第 09 篇VTOR=0x10000000);末尾ALIGN(4)是防御性收尾,保证表尾也 4 对齐(其实每个向量 4 字节,天然对齐,写上只是明确意图 + 保险)。.text只要起点对齐就够了,所以只在前面写。
📌 这里的
> SRAM不是"大于 SRAM",而是链接脚本的"放进"语法——把该段排到当前位置计数器.所在的位置、并归属于 SRAM 内存区(这就是"门牌号写定"的那一刻:.此刻的地址成为本段 VMA)。
它是整个镜像的第一节,必然落在0x10000000——这正好回应第 09 篇:SystemInit 里SCB->VTOR = 0x10000000,VTOR 指向这里。C 代码和链接脚本的这个"数字对账",靠 .isr_vector 排在第一节实现。
ALIGN(1024)不是强迫症:Cortex-M 的 VTOR 要求向量表基址按表大小向上取 2 的幂对齐(本表约 728 字节 → 取 1024)。不对齐,硬件直接忽略你写的 VTOR 低几位,中断照样飞。ALIGN(4)则是把位置计数器向上凑到 4 字节倍数——ARM 是 32 位架构,指令取指和 32 位数据天然按 4 字节(一个字)对齐最干净。KEEP()防--gc-sections(死代码消除)把"没人引用"的向量表当垃圾删掉——向量表是硬件访问的,必须手动保住。
💡可复用经验:向量表必须排第一节 + 按 2 的幂对齐(本工程 1KB),才能接住
SystemInit里VTOR = 0x10000000。lds 摆放位置和 C 代码 VTOR 是同一组数字对账,改一边必须同步另一边。
3.2.text:代码段,先钉住启动文件
一句话理解:代码段第一句就把Reset_Handler钉在最前,保证链接器一定把它链进来当入口。
.text : { . = ALIGN(4); KEEP(*(.startup_copro_fw.Reset_Handler)) *(.text) *(.text*) } > SRAM第一行又是一个KEEP,把Reset_Handler所在的特殊节钉在 .text 最前面——ENTRY(Reset_Handler) 声明的入口,必须确保真的被链接进来。
3.3 构造器三区段:给__libc_init_array留的空插槽
一句话理解:这三段是给 C 库"全局构造器"预留的空插槽——本工程用不上,但符号必须留着。
.preinit_array/.init_array/.fini_array三节用PROVIDE_HIDDEN导出成对的 start/end 符号。启动文件bl __libc_init_array就按这三对符号遍历区间里的每个函数指针依次调用。我们工程没有 C++ 构造器,区间是空的,但符号必须在——链接器和 newlib 认符号不认"有没有货"。
3.4.data:本脚本最精妙的一处(读懂>与AT>,就懂了 LMA==VMA)
一句话理解:本工程.data的"加载地址"和"运行地址"是同一个,启动文件那段"搬运"退化成无害的自拷贝。决定它俩是否相等的,正是>和AT>这两个符号。
① 每个节有两个地址:
- VMA(运行地址):运行时 CPU 取指/读写用的地址——也就是位置计数器
.自动发给每个段的「入住门牌号」。 - LMA(加载地址):这个节的初值"躺在固件镜像里的哪个位置"。
📌别被 “Virtual” 骗了:VMA 不是 MMU 那种「虚拟地址」。本工程是无 MMU 的裸机 Cortex-M4,CPU 取指/读写时总线上发出的就是 VMA 这个数,它等于物理地址。LMA 同理,也是镜像里的物理存储地址。VMA 和 LMA 不是「虚拟 vs 物理」,而是「运行位置 vs 存储位置」:两者可相同(单区域),也可不同(双区域如
.data > RAM AT> FLASH)。
②>和AT>不是你以为的那个意思。这是最大的坑:
> REGION是"放进"语法——把当前段排进名为REGION的 MEMORY 区,指定该段VMA(运行地址)。AT>单独额外指定该段的LMA(加载地址)区域,只在"想让加载地址和运行地址不在同一块内存"时才出现。
官方双区域教程.data > RAM AT> FLASH,用上面的规则一拆就清清楚楚:.data > RAM指定 VMA(运行时在 RAM),AT> FLASH指定 LMA(初值躺在 Flash)。于是上电后启动代码必须把初值从 Flash 搬到 RAM,CopyDataInit 是真搬运。
本工程不一样:全 SRAM 单区域,没有第二个存储区可搬。.data只写了> SRAM、没写AT>,链接器默认让 LMA 跟随 VMA,得到LMA == VMA:初值天然就位在运行地址。
💡可复用经验:全 SRAM 单区域(无 Flash / DRAM 第二存储区)时,
.data直接> SRAM(不写AT>),链接器自动让LMA == VMA;保留官方CopyDataInit搬运逻辑即可——它会退化成幂等自拷贝、无害。别为了「优化」删官方启动逻辑。
3.5.bss:清零区间 + 一个"骗链接器"的符号
一句话理解:.bss是"未初始化的全局变量"区,上电必须清零;末尾那对end/_end符号是专门给标准库撑场面用的。
.bss : { __bss_start__ = .; _sbss = .; *(.bss) *(.bss*) *(COMMON) end = .; _end = .; } > SRAM_sbss/_ebss给启动文件的 FillZerobss 用(.bss 上电是随机值,必须清零)。end/_end这对符号是给 libnosys 的_sbrk看的:标准库的 malloc 用end当堆顶。LiteOS 用自己的 80KB 外部堆,根本不用它——但链接器要解析符号,不给就报错。
💡可复用经验:
.bss末尾的end/_end这类「给标准库撑场面」的空壳符号必须留——malloc/_sbrk要解析它。换工程看到这类符号别当垃圾删,删了就是undefined reference。
小结
| 段 | 作用 | 关键约束 |
|---|---|---|
.isr_vector | 向量表(第 1 节) | 排最前 + 1KB 对齐 + KEEP,接住 VTOR=0x10000000 |
.text | 代码 | Reset_Handler用 KEEP 钉最前 |
| 构造器区段 | 给__libc_init_array | 空插槽,符号必留 |
.data | 已初始化全局变量 | 无 AT>,LMA==VMA,CopyDataInit 退化自拷贝 |
.bss | 未初始化全局变量 | 上电清零;end/_end应付_sbrk |
接口符号契约、ASSERT 越界拦截、以及"拿到新工程怎么查"的通用照妖镜,留在第 12 篇继续。
参考文档
- ARM — 《ARMv7-M Architecture Reference Manual》:The Cortex-M4 address space/ 复位行为:地址
0为复位向量取指区、向量表基址由VTOR设定。 - GNU ld 手册(Linker Scripts):
MEMORY、ORIGIN/LENGTH、> REGION、AT>、ASSERT等语法的权威定义。在线:https://sourceware.org/binutils/docs/ld/Scripts.html - ST — RM0436 Reference Manual:第 2.5 节 Memory map,明确 M4 内部 SRAM 地址窗口(SRAM1=
0x10000000/ 128KB …),本篇0x10000000起址与256KB约束据此。
术语表(本文出现)
| 缩写/符号 | 含义 |
|---|---|
.isr_vector | 向量表段,须排在最前、1KB 对齐,接住 VTOR |
.text | 代码段 |
.data | 已初始化全局变量段 |
.bss | 未初始化全局变量段,上电须清零 |
| VMA | Virtual Memory Address,运行地址:段运行时所在地址(等于物理地址) |
| LMA | Load Memory Address,加载地址:段的初值躺在镜像里的位置 |
> REGION | 链接脚本"放进"语法(非大于号),指定该段 VMA(运行地址) |
AT> | 额外指定该段 LMA(加载地址)区域;不写则 LMA 跟随 VMA |
| ALIGN(N) | 把位置计数器.向上取整到 N 的整数倍;ALIGN(1024)满足 VTOR 对齐,ALIGN(4)满足 32 位字对齐 |
| KEEP | 保住指定段不被--gc-sections删掉(用于向量表、Reset_Handler) |
__libc_init_array | newlib 的 C 库初始化函数,遍历构造器区间调用构造器 |
_estack | 栈顶指针初值= ORIGIN(SRAM)+LENGTH(SRAM),MSP 从此向下长 |
_sidata/_sdata/_edata | .data的加载地址与运行起止,供启动文件用 |
_sbss/_ebss | .bss起止,供 FillZerobss 清零 |
end/_end | .bss末尾符号,给标准库_sbrk当堆顶 |
| VTOR | 向量表偏移寄存器,指向当前向量表(第 09 篇设成 0x10000000) |
| MSP | Main Stack Pointer,主栈指针,复位后默认用的栈 |
| SRAM | 片内静态 RAM(0x10000000 起,256KB),程序全住这 |
--gc-sections | 死代码消除,删未被引用的函数/段;与 KEEP 配合防误删 |
系列导航
上一篇:第 10 篇 · 链接脚本 lds(上):256KB 硬约束与内存地图 |本篇:第 11 篇 · 链接脚本 lds(下)① 逐段解剖各内存段| 下一篇:第 12 篇 · 链接脚本 lds(下)② 接口契约、ASSERT 与照妖镜板级适配与应用 07–25:07 工程全景与启动四步法 | 08 BSP 契约与实现 | 09 SystemInit 与 CMSIS 裁剪 | 10 lds 上 |11 lds 下①| 12 lds 下② | 13 启动文件与五步启动 | 14 弱符号覆盖与三特殊中断 | 15 target_config 清单(上) | 16 FPU 三件套与栈档 | 17 改错速查 | 18 LED 任务与 LOS_TaskCreate | 19 LED 实战与错误码 | 20 los_exc 与三破坏现场 | 21 FPU 栈帧与 backtrace | 22 八环节与四铁律 | 23 全文件对账与目录 | 24 六类判据与换板五步 | 25 经验与变量级增删改留
完整 01–06 基础篇与切换汇编 01–06 见各篇导航。
代码仓库:stm32mp157-liteos-m @ Gitee · 基线 v0.1-baseline