在STM32的开发中,除了像CMSISI库和驱动库这些源码文件外,还会用到startup.s 文件和链接脚本(通常是 .ld 文件),通常这两个件都是IDE根据相应的MCU型号自动获取放入工程中,普通开发不需要关注,但是这两个文件对程序的初始化、内存布局以及执行流程有着重要影响,做一些高级应用时需要手动配置修改。
1. startup.s 的影响
startup.s 是汇编语言编写的启动文件,通常用于初始化硬件和设置运行环境。以下是它可能影响的代码部分:
堆栈初始化:
/* 设置初始堆栈指针 */
ldr sp, =_estack
解释:startup.s 会初始化堆栈指针(SP),确保 C/C++ 程序能够正确使用堆栈进行函数调用和局部变量存储。
全局变量初始化:
/* 将 .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 段中的全局变量。如果这些步骤未正确完成,程序中依赖于全局变量的代码可能会出现异常行为。
主函数调用:
/* 调用 main 函数 */
bl main
解释:startup.s 最终调用 main 函数,启动用户代码的执行。
2. 链接文件的影响
链接文件定义了程序的内存布局,包括各个段的位置和大小。以下是它可能影响的代码部分:
内存段分配:
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
}
解释:
链接文件将代码段(.text)、只读数据段(.rodata)放置在 Flash 中。
可写数据段(.data)和未初始化数据段(.bss)放置在 SRAM 中。
如果链接文件配置错误,可能导致程序无法正确访问数据或执行代码。
符号定义:
PROVIDE(_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
/* 启动文件 */
.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)
/* 链接脚本 */
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)
#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)
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 = .;
}
语法要点速查
例子:配置一个堆栈在 SRAM 顶部
假设 20KB RAM 从 0x20000000 开始,我们将 .stack 放到最顶:
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
你可以直接编辑此文件来自定义内存布局。
小结
链接脚本里同名段(比如 .text)理论上可以定义多个,只要它们分别位于不同的地址位置,链接器会把它们当作同一个逻辑段的多个“分片”,在内存中连续或不连续放置。
举例说明
.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 运行等。
举例
arm-none-eabi-ld ... -Ttext=0x08004000 ...
让程序代码从 Flash 地址 0x08004000 开始加载,而不是从 0x08000000;
常见于需要把程序放在 Flash 某个偏移位置的场景。
什么时候用?
有 Bootloader,需要主程序放在 Bootloader后面固定偏移位置;
需要把程序放在某个特殊内存区域;
多程序分区加载和升级设计。
注意
-Ttext 只影响 .text 段的起始地址,不影响其他段(如 .data, .bss);
链接脚本可能还需要配合修改,确保其他段地址合理。
简单总结:
1. 没有第一段空的 .text : {},链接器会怎样?
链接器会把第一个定义的 .text 段内容放在链接脚本里定义的默认起始地址;
如果没有空的占位段,后面实际代码段会紧贴起始地址放置,不会留出预留空间;
这可能导致无法插入其他必须放在起始地址的段(比如中断向量表),或者启动代码和程序代码无法分区。
2. 可能的影响
如果中断向量表 .isr_vector 也放在 .text 里,没空段也没关系,代码会连续放置,程序照常运行;
如果你需要预留空间给 Bootloader 或特殊段,没空段就没法“留地方”,升级和分区就麻烦;
如果启动代码和程序代码有地址冲突,程序可能启动失败或异常。
3. 总结
简单说:
如果你的链接脚本和启动流程设计不依赖那个空段,没它程序也能跑;但如果你需要分区、升级、或对齐,空段是很有用的“预留地”。
简单示例:
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 段的区别
2. 为什么 .data 要放 FLASH?
.data 变量需要初始化为非零的初始值。
这些初始值必须存储在非易失存储器(通常是 FLASH)中。
程序启动时,把 FLASH 中的 .data 初始值“搬运”到 RAM。
3. .bss 的初始化
.bss 是未初始化的变量,标准要求它们启动时被清零。
.bss 不占 FLASH 空间,直接在 RAM 中分配空间,启动时由启动代码清零。
4. 搬运时机:启动代码(启动汇编/初始化C库)
搬运 .data 和清零 .bss 是由启动代码完成的,通常写在启动汇编文件或 C 库初始化函数中。
这个代码在 Reset_Handler 或 _start 入口处运行,最先执行。
典型伪代码流程:
// 搬运 .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. 具体示例链接脚本片段(简化版)
.data : AT (LOADADDR(.data)) {
_sdata = .; /* 运行时起始地址,RAM */
*(.data)
_edata = .;
} >RAM AT >FLASH
.bss : {
_sbss = .;
*(.bss)
*(COMMON)
_ebss = .;
} >RAM
总结
bss段
程序代码(比如 .text 段里的启动代码)在编译和链接阶段就已经知道 .bss 段的起始和结束地址了,这是通过链接器脚本定义的符号来实现的。
原理
链接器脚本定义了 .bss 段,并在其中定义符号,如:
_sbss = .; /* .bss段起始地址 */
*(.bss*)
_ebss = .; /* .bss段结束地址 */
这些符号 _sbss、_ebss 在链接后成为固定地址常量,写入可执行文件的符号表和重定位表里。
编译器/汇编器在编译 .text 里的代码时,会引用这些符号作为外部变量(extern),链接后程序代码中使用的就是具体地址。
结果
启动代码中类似:
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 段和它的边界符号
链接器脚本(.ld 文件)中有类似如下定义:
.bss (NOLOAD) : {
_sbss = .; /* 记录 .bss 起始地址 */
*(.bss*)
*(COMMON)
_ebss = .; /* 记录 .bss 结束地址 */
} > RAM
链接器的作用:
把所有未初始化的全局/静态变量放进 .bss 段;
设置 _sbss 和 _ebss 这两个符号,代表 .bss 段的起止地址;
在 ELF 文件的符号表中记录这两个符号的位置;
最终,.text 段中引用这两个符号的位置,会被替换成它们的实际地址值。
第二步:启动代码引用这些符号,用于清零 .bss 段
在启动代码中(通常是 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 是链接脚本中管理存储与运行地址分离的核心指令,对嵌入式系统的数据初始化和存储器布局至关重要。
————————————————
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/is0815/article/details/148544956
|
|