在STM32的开发中,除了像CMSISI库和驱动库这些源码文件外,还会用到startup.s
文件和链接脚本(通常是 .ld
文件),通常这两个件都是IDE根据相应的MCU型号自动获取放入工程中,普通开发不需要关注,但是这两个文件对程序的初始化、内存布局以及执行流程有着重要影响,做一些高级应用时需要手动配置修改。
1. startup.s
的影响
startup.s
是汇编语言编写的启动文件,通常用于初始化硬件和设置运行环境。以下是它可能影响的代码部分:
-
堆栈初始化:
asm/* 设置初始堆栈指针 */ ldr sp, =_estack
解释:
startup.s
会初始化堆栈指针(SP),确保 C/C++ 程序能够正确使用堆栈进行函数调用和局部变量存储。 -
全局变量初始化:
asm/* 将 .data 段从 Flash 复制到 RAM */ ldr r0, =__etext ldr r1, =__data_start__ ldr r2, =__data_end__ mov r3, #0 copy_data: ldrb r4, [r0], #1 strb r4, [r1], #1 add r3, r3, #1 cmp r3, r2 bne copy_data /* 清零 .bss 段 */ ldr r0, =__bss_start__ ldr r1, =__bss_end__ mov r2, #0 zero_bss: strb r2, [r0], #1 cmp r0, r1 bne zero_bss
解释:
startup.s
负责将.data
段从 Flash 复制到 RAM,并清零.bss
段中的全局变量。如果这些步骤未正确完成,程序中依赖于全局变量的代码可能会出现异常行为。 -
主函数调用:
asm/* 调用 main 函数 */ bl main
解释:
startup.s
最终调用main
函数,启动用户代码的执行。
2. 链接文件的影响
链接文件定义了程序的内存布局,包括各个段的位置和大小。以下是它可能影响的代码部分:
-
内存段分配:
ldMEMORY { FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K SRAM (rw) : ORIGIN = 0x20000000, LENGTH = 64K } SECTIONS { .text : { *(.text*) } > FLASH .rodata : { *(.rodata*) } > FLASH .data : { _sidata = LOADADDR(.data); *(.data*) } > SRAM AT> FLASH .bss : { *(.bss*) } > SRAM }
解释:
- 链接文件将代码段(
.text
)、只读数据段(.rodata
)放置在 Flash 中。 - 可写数据段(
.data
)和未初始化数据段(.bss
)放置在 SRAM 中。 - 如果链接文件配置错误,可能导致程序无法正确访问数据或执行代码。
- 链接文件将代码段(
-
符号定义:
ldPROVIDE(_estack = ORIGIN(SRAM) + LENGTH(SRAM)); PROVIDE(__etext = .); PROVIDE(__data_start__ = .); PROVIDE(__data_end__ = .); PROVIDE(__bss_start__ = .); PROVIDE(__bss_end__ = .);
解释:链接文件定义了一些关键符号,例如堆栈顶部地址
_estack
和数据段的起始/结束地址。这些符号被startup.s
使用来完成初始化。
示例代码
以下是一个简单的 STM32 项目结构,展示 startup.s
和链接文件如何协作影响代码:
startup.s
asm
/* 启动文件 */
.syntax unified
.cpu cortex-m4
.fpu softvfp
.thumb
.global Reset_Handler
.type Reset_Handler, %function
Reset_Handler:
/* 初始化堆栈指针 */
ldr sp, =_estack
/* 复制 .data 段 */
ldr r0, =__etext
ldr r1, =__data_start__
ldr r2, =__data_end__
copy_data_loop:
cmp r0, r2
beq clear_bss
ldrb r3, [r0], #1
strb r3, [r1], #1
b copy_data_loop
clear_bss:
/* 清零 .bss 段 */
ldr r0, =__bss_start__
ldr r1, =__bss_end__
zero_loop:
cmp r0, r1
beq call_main
movs r2, #0
strb r2, [r0], #1
b zero_loop
call_main:
/* 调用 main 函数 */
bl main
hang:
b hang
链接文件 (STM32.ld
)
ld
/* 链接脚本 */
MEMORY {
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
SRAM (rw) : ORIGIN = 0x20000000, LENGTH = 64K
}
SECTIONS {
.text : {
*(.text*)
} > FLASH
.rodata : {
*(.rodata*)
} > FLASH
.data : {
_sidata = LOADADDR(.data);
*(.data*)
} > SRAM AT> FLASH
.bss : {
*(.bss*)
} > SRAM
PROVIDE(_estack = ORIGIN(SRAM) + LENGTH(SRAM));
}
主程序 (main.c
)
c
#include <stdio.h>
int global_var = 42;
void main() {
static int static_var = 10;
printf("Global Var: %d\n", global_var);
printf("Static Var: %d\n", static_var);
while (1);
}
解释
-
startup.s
的作用:- 初始化堆栈指针。
- 将
.data
段从 Flash 复制到 SRAM。 - 清零
.bss
段。 - 调用
main
函数。
-
链接文件的作用:
- 定义内存区域(Flash 和 SRAM)。
- 分配代码段(
.text
)、只读数据段(.rodata
)、可写数据段(.data
)和未初始化数据段(.bss
)。 - 提供符号地址(如
_estack
和__data_start__
)供startup.s
使用。
链接脚本(Linker Script)用于控制程序在内存中的布局,嵌入式开发中常用于指定代码段、堆栈、向量表等放置在哪些具体的内存地址。STM32 或其他 ARM Cortex-M 芯片中,链接脚本非常关键。
📚 常见用于嵌入式的链接脚本格式
你可能使用:
- GNU 链接器脚本(
.ld
)--- GCC/CubeIDE/Makefile 项目用这个。 - Keil scatter file(
.sct
)--- Keil MDK 项目用这个。 - IAR linker configuration(
.icf
)--- IAR 项目用这个。
下面主要讲 GNU .ld
脚本语法(最常见、最可控)👇
✅ 链接脚本基本结构(.ld
)
ld
ENTRY(Reset_Handler)
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 64K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}
SECTIONS
{
/* 向量表和启动代码 */
.isr_vector :
{
KEEP(*(.isr_vector))
} >FLASH
/* 程序代码段 */
.text :
{
*(.text) /* 所有代码 */
*(.text*) /* 所有函数 */
*(.rodata) /* 常量 */
*(.rodata*) /* 字符串常量等 */
KEEP(*(.init))
KEEP(*(.fini))
} >FLASH
/* 初始化数据,运行时从 FLASH 复制到 RAM */
.data : AT(__etext)
{
__data_start__ = .;
*(.data)
*(.data*)
__data_end__ = .;
} >RAM
/* 未初始化变量 */
.bss :
{
__bss_start__ = .;
*(.bss)
*(.bss*)
*(COMMON)
__bss_end__ = .;
} >RAM
/* 堆区 */
.heap (COPY):
{
__heap_start__ = .;
. = . + 4K;
__heap_end__ = .;
} >RAM
/* 栈区 */
.stack (COPY):
{
__stack_start__ = .;
. = . + 2K;
__stack_end__ = .;
} >RAM
/* 结束地址 */
_end = .;
}
🔍 语法要点速查
语法 | 含义 |
---|---|
MEMORY |
定义物理内存区域和大小 |
SECTIONS |
定义逻辑段,决定哪些代码/变量放哪 |
> |
表示把段放入哪个 MEMORY 区域 |
AT(x) |
指定链接地址和加载地址不一致(常用于 .data ) |
KEEP() |
强制保留被 GCC 可能优化掉的段(如 .isr_vector ) |
. |
当前地址指针,可控制偏移和大小 |
__symbol__ = .; |
保存当前地址为符号,用于初始化、拷贝等 |
*(.text) |
收集所有 .text 段的内容(函数体) |
💡 例子:配置一个堆栈在 SRAM 顶部
假设 20KB RAM 从 0x20000000
开始,我们将 .stack
放到最顶:
ld
MEMORY
{
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}
SECTIONS
{
...
.stack (COPY):
{
. = ORIGIN(RAM) + LENGTH(RAM) - 1K; /* 栈在最后 1KB */
__stack_top = .;
. = . + 1K;
__stack_bottom = .;
} >RAM
}
🧩 用途示例
你可以通过链接脚本来:
- 测试 SRAM 某段是否可用(限制变量位置)
- 把栈搬到 SRAM 最尾部(避免和测试冲突)
- 把某段变量放入专用 RAM 区(比如
.dma
用于 DMA) - 定义 bootloader + app 的 FLASH 分区
🛠 工具辅助
如果你使用的是 STM32CubeIDE 或 Makefile + GCC,链接脚本通常位于:
project/STM32xxxx_FLASH.ld
你可以直接编辑此文件来自定义内存布局。
📌 小结
内容 | 示例 |
---|---|
定义内存区域 | MEMORY { FLASH(rx): ORIGIN = 0x08000000, LENGTH = 64K } |
放入段 | .text : { *(.text) } >FLASH |
指定偏移 | . = . + 4K; |
定义符号 | __bss_start__ = .; |
强制保留 | KEEP(*(.isr_vector)) |
链接脚本里同名段(比如 .text
)理论上可以定义多个,只要它们分别位于不同的地址位置,链接器会把它们当作同一个逻辑段的多个"分片",在内存中连续或不连续放置。
举例说明
ld
.text : { /* 第一部分 */
*(.text_start)
}
. = ALIGN(512);
.isr_vector : { /* 中断向量 */
KEEP(*(.isr_vector*))
}
. = ALIGN(1024);
.text ALIGN(4) : { /* 第二部分 */
*(.text_main)
}
. = ALIGN(2048);
.text ALIGN(4) : { /* 第三部分 */
*(.text_extra)
}
结果
- 链接器会把所有
.text
段内容合并成逻辑上的一个.text
段; - 物理上这
.text
段被拆成了 3 个片段,分别位于不同地址(由你写的对齐和地址移动决定); - 这中间可以插入别的段(比如
.isr_vector
),造成不连续内存布局。
使用场景
- 放启动代码、向量表、主程序代码、额外代码段等,需要特定地址对齐或空间;
- 对应硬件启动要求或内存映射设计。
注意点
- 多段
.text
分片可能使调试器显示段不连续,调试体验可能稍差; - 需要确认链接器脚本和启动文件匹配,避免地址冲突;
- 链接器输出的 map 文件能帮你查看段实际地址布局。
.text : {} 第一个空段
-Ttext=<addr>
是链接器(ld)的一个命令行参数,用来 指定程序的 .text
段在内存中的起始加载地址。
具体作用:
- 链接器默认把
.text
段放在链接脚本里定义的默认地址(通常是 Flash 起始地址,比如0x08000000
); - 使用
-Ttext=<addr>
可以覆盖链接脚本里.text
段的默认起始地址 ,让代码从你指定的<addr>
地址开始放置; - 这样可以灵活安排程序代码在内存中的位置,比如给 Bootloader 留空间,把主程序放在偏移地址,或者放到 RAM 运行等。
举例
bash
arm-none-eabi-ld ... -Ttext=0x08004000 ...
- 让程序代码从 Flash 地址
0x08004000
开始加载,而不是从0x08000000
; - 常见于需要把程序放在 Flash 某个偏移位置的场景。
什么时候用?
- 有 Bootloader,需要主程序放在 Bootloader后面固定偏移位置;
- 需要把程序放在某个特殊内存区域;
- 多程序分区加载和升级设计。
注意
-Ttext
只影响.text
段的起始地址,不影响其他段(如.data
,.bss
);- 链接脚本可能还需要配合修改,确保其他段地址合理。
简单总结:
作用 | 说明 |
---|---|
指定 .text 段起始地址 |
灵活调整代码在内存中的加载位置 |
适应 Bootloader 或分区设计 | 保证程序代码地址满足硬件或升级需求 |
能用,但可能会有一些限制和风险,具体看你的链接脚本和硬件需求。
1. 没有第一段空的 .text : {}
,链接器会怎样?
- 链接器会把第一个定义的
.text
段内容放在链接脚本里定义的默认起始地址; - 如果没有空的占位段,后面实际代码段会紧贴起始地址放置,不会留出预留空间;
- 这可能导致无法插入其他必须放在起始地址的段(比如中断向量表),或者启动代码和程序代码无法分区。
2. 可能的影响
- 如果中断向量表
.isr_vector
也放在.text
里,没空段也没关系,代码会连续放置,程序照常运行; - 如果你需要预留空间给 Bootloader 或特殊段,没空段就没法"留地方",升级和分区就麻烦;
- 如果启动代码和程序代码有地址冲突,程序可能启动失败或异常。
3. 总结
情况 | 结论 |
---|---|
简单项目,无特殊预留需求 | 可以不用空 .text : {} |
有 Bootloader 或特殊布局需求 | 建议用空 .text 占位,方便布局 |
简单说:
如果你的链接脚本和启动流程设计不依赖那个空段,没它程序也能跑;但如果你需要分区、升级、或对齐,空段是很有用的"预留地"。
简单示例:
c
RAM_START = 0x20000000;
RAM_END = 0x20005000;
_estack = RAM_END;
/* ENTRY(main) */
SECTIONS
{
/* This is for ability to change link address with `-Ttext=<addr>` ld option */
.text : {}
/* Align interrupts vectors table to 512-byte boundary */
. = ALIGN(512);
/* C generated vectors sections of name `.isr_vector.__vec_*` */
INCLUDE vectors.ld
/* ASM/C generated vectors */
.isr_vector : { KEEP(*(.isr_vector*)) KEEP(*(.iv)) KEEP(*(.vt)) }
/* Code and read-only data; can be aligned to (2) */
.text ALIGN(4) : { *(.text*) *(.rodata*) }
/* Data alignment is not stricly required */
/* Save .text end address; .data init values retain here */
_sidata = ALIGN(4);
/* Move .data and .bss to ram if . isn't already there */
. = . < RAM_START ? RAM_START : . ;
/* Link .data always to RAM */
.data ALIGN(4) : AT (_sidata) { _sdata = . ; *(.data*) _edata = . ; }
/* Link .bss always to RAM after the .data */
.bss ALIGN(4) : { _sbss = . ; *(.bss*) *(COMMON) _ebss = . ; }
/* Remove sections that are not required */
/DISCARD/ : { *(.ARM.attributes) *(.comment*) *(.note*) }
}
启动代码
负责把.data
和 .bss
段的数据搬运到 RAM:
1. .data
和 .bss
段的区别
段名 | 含义 | 初始存储位置 | 运行时存储位置 |
---|---|---|---|
.data |
初始化的全局/静态变量 | FLASH(非易失存储) | RAM(可读写) |
.bss |
未初始化的全局/静态变量 | 不占FLASH空间(全0) | RAM,且初始化为0 |
2. 为什么 .data
要放 FLASH?
.data
变量需要初始化为非零的初始值。- 这些初始值必须存储在非易失存储器(通常是 FLASH)中。
- 程序启动时,把 FLASH 中的
.data
初始值"搬运"到 RAM。
3. .bss
的初始化
.bss
是未初始化的变量,标准要求它们启动时被清零。.bss
不占 FLASH 空间,直接在 RAM 中分配空间,启动时由启动代码清零。
4. 搬运时机:启动代码(启动汇编/初始化C库)
- 搬运
.data
和清零.bss
是由启动代码完成的,通常写在启动汇编文件或 C 库初始化函数中。 - 这个代码在
Reset_Handler
或_start
入口处运行,最先执行。
典型伪代码流程:
c
// 搬运 .data 段初始化值:从FLASH复制到RAM
for (p = &_ldata; p < &_edata; p++) {
*p = *p_flash++;
}
// 清零 .bss 段
for (p = &_sbss; p < &_ebss; p++) {
*p = 0;
}
这里:
_ldata
是 FLASH 中.data
初始值起始地址_edata
是.data
末尾地址(RAM 中)_sbss
和_ebss
是.bss
的 RAM 区间地址
5. 链接脚本的作用
链接脚本会:
- 把
.data
的 加载地址(LOADADDR)放在 FLASH(含初始值) - 把
.data
的 运行地址(VMA,Virtual Memory Address)放在 RAM(变量实际存放处) - 定义
_ldata
、_sdata
、_edata
等符号,供启动代码使用
6. .bss
在链接脚本中只分配 RAM 空间
它不存初始值,启动时清零即可。
7. 具体示例链接脚本片段(简化版)
ld
.data : AT (LOADADDR(.data)) {
_sdata = .; /* 运行时起始地址,RAM */
*(.data)
_edata = .;
} >RAM AT >FLASH
.bss : {
_sbss = .;
*(.bss)
*(COMMON)
_ebss = .;
} >RAM
总结
内容 | 说明 |
---|---|
.data 初始值 |
存储在 FLASH,程序启动时复制到 RAM |
.data 运行区 |
RAM,CPU 读写变量空间 |
.bss |
RAM,启动时清零,无FLASH初始值 |
搬运时机 | 复位启动代码(Reset_Handler)执行时,C 库初始化部分 |
bss段
程序代码(比如 .text
段里的启动代码)在编译和链接阶段就已经知道 .bss
段的起始和结束地址了,这是通过链接器脚本定义的符号来实现的。
原理
-
链接器脚本定义了
.bss
段,并在其中定义符号,如:ld_sbss = .; /* .bss段起始地址 */ *(.bss*) _ebss = .; /* .bss段结束地址 */
-
这些符号
_sbss
、_ebss
在链接后成为固定地址常量,写入可执行文件的符号表和重定位表里。 -
编译器/汇编器在编译
.text
里的代码时,会引用这些符号作为外部变量(extern
),链接后程序代码中使用的就是具体地址。
结果
启动代码中类似:
c
extern uint32_t _sbss;
extern uint32_t _ebss;
void Reset_Handler(void) {
uint32_t *p = &_sbss;
while (p < &_ebss) {
*p++ = 0;
}
// 继续执行其他初始化
}
_sbss
和_ebss
不是"运行时查符号表",而是编译链接后就固定成具体的地址值;- CPU 执行时用的就是这些实际的 RAM 地址,完成
.bss
的清零。
总结
阶段 | 符号作用 |
---|---|
链接时 | 符号被解析为具体内存地址 |
编译时 | 代码引用符号作为变量或地址 |
运行时 | 直接使用地址,清零 .bss 段 |
我们可以分两步来看清楚这个过程:
✅ 第一步:链接器脚本定义 .bss
段和它的边界符号
链接器脚本(.ld
文件)中有类似如下定义:
ld
.bss (NOLOAD) : {
_sbss = .; /* 记录 .bss 起始地址 */
*(.bss*)
*(COMMON)
_ebss = .; /* 记录 .bss 结束地址 */
} > RAM
链接器的作用:
- 把所有未初始化的全局/静态变量放进
.bss
段; - 设置
_sbss
和_ebss
这两个符号,代表.bss
段的起止地址; - 在 ELF 文件的符号表中记录这两个符号的位置;
- 最终,
.text
段中引用这两个符号的位置,会被替换成它们的实际地址值。
✅ 第二步:启动代码引用这些符号,用于清零 .bss
段
在启动代码中(通常是 C 或汇编),你会看到类似如下代码:
c
extern uint32_t _sbss;
extern uint32_t _ebss;
void Reset_Handler(void) {
uint32_t *dst = &_sbss;
while (dst < &_ebss) {
*dst++ = 0;
}
}
- 这段代码使用
_sbss
和_ebss
,就是在清空.bss
区域; - 编译时,这些符号被当成外部变量;
- 链接时,它们被替换成
.bss
段实际的起止地址。
✅ 本质
你说的"启动代码清 .bss
的代码,和 .bss
段的符号是链接时对应上的"------本质上是:
链接器把
.bss
段确定了地址范围,并生成对应的符号_sbss
、_ebss
,这些符号的地址被内嵌到.text
段的启动代码中,所以两者天然就对得上。
✅ 可视化理解(Flash / RAM 布局)
FLASH:
0x08000000 ───────────────────
[ .isr_vector ]
[ .text ] ← 启动代码在这里,包含 bss 清零指令
[ .rodata ]
[ .data (initial values) ]
RAM:
0x20000000 ───────────────────
[ .data ] ← 从 Flash 拷贝来的初始化数据
[ .bss ] ← 启动时清零的区域(由 _sbss/_ebss 指定)
[ heap/stack ]
在链接脚本中,AT 关键字用于显式指定数据段或代码段的 加载内存地址(Load Memory Address, LMA),与其运行时地址(Virtual Memory Address, VMA)分离。具体解析如下:
一、AT 的作用与语法
核心功能
AT 指令定义数据在 存储介质(如 Flash)中的物理存放位置:
LMA:程序烧录时数据存储的位置地址(如 Flash)。
VMA:程序运行时数据被加载到内存(如 RAM)的地址。
语法格式
在 .ld 文件中的典型用法:
ld
Copy Code
.section_name : {
/* 段内容 */
} > VMA_REGION AT> LMA_REGION // 方式1:指定存储器区域
或:
ld
Copy Code
.section_name : AT(LMA_ADDRESS) {
/* 段内容 */
} > VMA_REGION // 方式2:直接指定地址
二、关键应用场景
初始化全局变量(.data 段)
需将初始值存储在 Flash(LMA),运行时复制到 RAM(VMA):
ld
Copy Code
.data : ALIGN(4) {
_sdata = .; // VMA 起始地址(RAM)
(.data .data. )
_edata = .; // VMA 结束地址(RAM)
} > RAM AT> FLASH // VMA 在 RAM,LMA 在 FLASH
启动代码通过 _sidata = LOADADDR(.data) 获取初始值位置并复制到 RAM。
自定义数据段存储
将特定数据(如配置表)固定存储到 Flash 的独立区域:
ld
Copy Code
MEMORY {
FLASH_CONFIG (rx) : ORIGIN = 0x0800C000, LENGTH = 16K
}
.config_data : {
*(.config_section)
} AT> FLASH_CONFIG // LMA 指定到 FLASH_CONFIG
三、AT Vs. 默认行为
特性 默认行为(无 AT) 显式 AT 指令
LMA 地址 等于 VMA(可能导致数据未初始化) 独立于 VMA(通常为 Flash)
初始化需求 需手动初始化数据 启动代码自动从 LMA 复制到 VMA
典型用例 纯 RAM 运行 嵌入式系统初始化变量/常量
四、技术原理
分离 LMA 与 VMA:AT 确保数据在编译阶段被写入正确的存储位置(如 Flash),运行时再加载到目标地址(如 RAM)。
符号关联:通过 LOADADDR(.section) 获取 LMA,ADDR(.section) 获取 VMA ,供启动代码使用。
对齐处理:常结合 ALIGN(n) 确保地址对齐(如 ALIGN(4) 满足 32 位访问)。
综上,AT 是链接脚本中管理存储与运行地址分离的核心指令,对嵌入式系统的数据初始化和存储器布局至关重要。