[信息] 简说stm32的startup.s文件和ld链接脚本

[复制链接]
 楼主| 晓伍 发表于 2025-6-17 16:24 | 显示全部楼层 |阅读模式
在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 = .;
}



语法要点速查

42349684b9d0c4c99a.png

例子:配置一个堆栈在 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


你可以直接编辑此文件来自定义内存布局。


小结

8383684b9cfb74765.png

链接脚本里同名段(比如 .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);
链接脚本可能还需要配合修改,确保其他段地址合理。
简单总结:


88301684b9ce5612b8.png

1. 没有第一段空的 .text : {},链接器会怎样?
链接器会把第一个定义的 .text 段内容放在链接脚本里定义的默认起始地址;
如果没有空的占位段,后面实际代码段会紧贴起始地址放置,不会留出预留空间;
这可能导致无法插入其他必须放在起始地址的段(比如中断向量表),或者启动代码和程序代码无法分区。
2. 可能的影响
如果中断向量表 .isr_vector 也放在 .text 里,没空段也没关系,代码会连续放置,程序照常运行;
如果你需要预留空间给 Bootloader 或特殊段,没空段就没法“留地方”,升级和分区就麻烦;
如果启动代码和程序代码有地址冲突,程序可能启动失败或异常。
3. 总结

21404684b9cdb48a31.png

简单说:

如果你的链接脚本和启动流程设计不依赖那个空段,没它程序也能跑;但如果你需要分区、升级、或对齐,空段是很有用的“预留地”。

简单示例:


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 段的区别

21221684b9ccb0aa33.png

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


总结

90857684b9cba9135a.png


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 的清零。
总结

8738684b9caaeb36d.png

我们可以分两步来看清楚这个过程:


第一步:链接器脚本定义 .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

您需要登录后才可以回帖 登录 | 注册

本版积分规则

95

主题

4336

帖子

1

粉丝
快速回复 在线客服 返回列表 返回顶部