尧图精选

向量表、链接脚本、汇编启动代码、C 运行时库

🕒 发布时间:2026/9/4 8:57:59 📁 来源:尧图网络
一、硬件上电后的一瞬间1. 向量表基址与取指ARM Cortex‑M 内核规定上电或复位后CPU 自动从地址0x00000000读取初始 MSP主堆栈指针从地址0x00000004读取复位向量Reset_Handler的入口地址。这两个操作用硬件完成不依赖任何软件。但很多 MCU如 STM32的 Flash 起始地址是0x08000000因此芯片会根据启动模式BOOT0/BOOT1将0x00000000别名映射到内部 Flash、系统存储器或 SRAM。上电后默认映射为 Flash0x00000000别名区指向0x08000000。于是硬件实际读到的是 Flash 开头的两个 32 位字0x08000000 : 初始栈顶值 Stack Top 0x08000004 : Reset_Handler 地址2. 栈指针的初始值细节初始栈顶值通常由链接脚本定义为_estack它等于SRAM 的结束地址 1因为 Cortex‑M 的栈是满递减的初始 SP 指向栈区最高地址的下一个字。例如 SRAM 位于0x20000000 - 0x2001FFFF则_estack 0x20020000。此时 CPU 内部MSP 0x20020000PC Reset_Handler地址Flash 中的某条指令接下来CPU 以特权线程模式使用 MSP 开始执行汇编代码。二、Reset_Handler 启动代码逐条分析以典型的 STM32 启动文件为例简化但关键步骤保留.section .text.Reset_Handler .weak Reset_Handler .type Reset_Handler, %function Reset_Handler: /* 1. 关闭全局中断保证初始化过程不被干扰 */ cpsid i /* 2. 使能 FPU如果芯片有在 M4/M7 上常见 */ ldr r0, 0xE000ED88 /* CPACR 地址 */ ldr r1, [r0] orr r1, r1, #(0xF 20) /* 设置 CP10、CP11 为全访问使能 FPU */ str r1, [r0] dsb isb /* 3. 初始化 .data 段从 Flash 复制到 SRAM */ ldr r0, _sidata /* Flash 中 .data 的加载地址 */ ldr r1, _sdata /* SRAM 中 .data 的运行起始地址 */ ldr r2, _edata /* SRAM 中 .data 的结束地址 */ subs r2, r2, r1 /* 计算 .data 段大小 */ beq copy_data_done /* 若没有 .data 则跳过 */ copy_data_loop: ldrb r3, [r0, #1]! /* 每次复制一个字节或可改为半字/字复制 */ strb r3, [r1, #1]! subs r2, r2, #1 bne copy_data_loop copy_data_done: /* 4. 清零 .bss 段 */ ldr r0, _sbss ldr r1, _ebss movs r2, #0 subs r1, r1, r0 beq zero_bss_done zero_bss_loop: strb r2, [r0, #1]! /* 按字节零填充实际可优化为按字 */ subs r1, r1, #1 bne zero_bss_loop zero_bss_done: /* 5. 调用 C 运行环境初始化 */ bl __libc_init_array /* 6. 调用用户 main */ bl main /* 7. 若 main 返回进入死循环 */ b .详细解释每一步关闭中断cpsid i将 PRIMASK 置 1屏蔽所有可配置优先级的中断NMI 和 HardFault 除外。这是因为.data复制和.bss清零期间如果发生中断可能会访问尚未初始化的全局变量导致错误。FPU 初始化在 M4/M7 中CPACR寄存器的 CP10/CP11 控制 FPU 访问。必须在此处打开否则后续浮点运算会引发 HardFault。此处代码置于.data复制之前因为复制过程中可能不会用到浮点但习惯上尽早打开。.data复制_sidata是闪存中.data段的加载地址 (LMA)紧接在.rodata常量区之后。_sdata和_edata是 SRAM 中.data段的虚拟地址 (VMA)的起止。循环将初始化值从 Flash 复制到 RAM。.bss清零_sbss起始于_edata处.bss紧接.data后面。_ebss是.bss结束地址。循环写入 0。__libc_init_array这是 C 库newlib/glibc 等提供的初始化函数负责执行.preinit_array中的函数指针留给系统和 C 库。.init_array中的函数指针全局 C 对象的构造函数、__attribute__((constructor))修饰的 C 函数。若有.fini_array等后续处理也在此处设置。执行完后所有的全局/静态 C 对象都已经构造完毕C 环境完全就绪。调用mainbl main跳转到用户入口传递的 R0/R1 参数通常被忽略裸机环境下main常定义为int main(void)。捕获返回main返回后进入死循环防止 PC 跑飞。实际工程中可能添加软件复位等处理。三、链接脚本如何决定内存布局一个典型的链接脚本部分如下展示了各个段在 Flash 和 SRAM 中的分配MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 1024K SRAM (rwx) : ORIGIN 0x20000000, LENGTH 128K } /* 定义栈顶SRAM 最高地址 */ _estack ORIGIN(SRAM) LENGTH(SRAM); /* 例如 0x20020000 */ SECTIONS { /* 代码段位于 Flash 低地址 */ .text : { KEEP(*(.vectors)) /* 向量表必须放在最前面 */ *(.text*) /* 程序代码 */ *(.rodata*) /* 只读数据即常量区 */ . ALIGN(4); _etext .; /* 代码结束可作为 _sidata 的参考 */ } FLASH /* .data 段的 LMA 在 FlashVMA 在 SRAM */ .data : { _sdata .; /* SRAM 中 .data 起始 */ *(.data*) . ALIGN(4); _edata .; /* SRAM 中 .data 结束 */ } SRAM AT FLASH /* 加载地址在 Flash 中的 .data 初始化值紧接 .text 后 */ _sidata LOADADDR(.data); /* 通常就是 _etext 或其后 */ /* .bss 段紧接 .data 之后 */ .bss : { _sbss .; /* SRAM 中 .bss 起始 */ *(.bss*) *(COMMON) . ALIGN(4); _ebss .; /* SRAM 中 .bss 结束 */ } SRAM /* 堆区定义可选 */ .heap : { __heap_base .; . . _min_heap_size; /* 保留最小堆空间 */ __heap_limit .; } SRAM /* 栈区不占据静态空间通常只定义栈顶符号从高地址向下增长 */ /* 栈底由 __heap_limit 和 _estack 之间的空隙自动形成 */ }从该脚本可看出代码区.text和常量区.rodata位于 Flash 低地址.rodata紧接.text后。.data的 VMA处于 SRAM 的低地址区域从0x20000000开始。.bss紧接.data之后位于 SRAM 较高地址。堆紧接.bss之后通过__heap_base和__heap_limit划定范围若启用动态内存。栈未在链接脚本中占用静态空间其边界由__initial_sp即_estackSRAM 顶部和堆区保留空间的上界自然确定。栈向下增长堆向上增长最大化利用自由空间。四、从高地址到低地址的精确内存图假设 SRAM 范围0x20000000 - 0x2001FFFF128 KBFlash 范围0x08000000 - 0x080FFFFF1 MB。那么从高地址SRAM 顶端到低地址Flash 底端的映射为高地址SRAM末端 --------------------------- -- _estack 0x20020000 初始 SP | | | 栈区 (Stack) | 向下增长 SP 递减 | | --------------------------- -- 堆顶动态变化 | | | 堆区 (Heap) | 向上增长 malloc 分配时递增 | | --------------------------- -- __heap_base紧接 .bss | | | .bss 段 (BSS) | 未初始化/零初始化的全局/静态变量 | | --------------------------- -- _sbss (_edata 的值) | | | .data 段 (Data) | 已初始化全局/静态变量内容从 Flash 复制 | | --------------------------- -- _sdata (通常 0x20000000) ... SRAM 低地址 ... ... Flash 区域 ... --------------------------- -- 0x08000000 偏移紧接 .data 的 LMA 后 | .data 初始值 (LMA) | 存放在 Flash由启动代码复制 --------------------------- | .rodata (常量区) | const 变量、字符串字面量 --------------------------- | .text (代码区) | Reset_Handler, main 等所有指令 --------------------------- | 向量表 (Vector Table) | 初始 SP, Reset_Handler, 异常向量 --------------------------- -- 0x08000000 Flash 基址 低地址从高地址向低地址观察的顺序正是栈区 → 堆区 → 全局区(.bss 高 .data 低) → 常量区 → 代码区。五、为什么 .bss 在高地址.data 在低地址连续性启动代码需要先复制.data再清零.bss。两者在 SRAM 中地址连续_sbss _edata可以让memcpy/循环直接从_sdata操作到_edata然后继续从_sbss到_ebss逻辑清晰不会出现间隙或碎片。空间利用效率堆区通常紧接.bss之后向上增长栈从顶部向下增长。将.data和.bss整体放在 SRAM 的低端堆和栈放在剩余的高端空间可以最大化中间的自由内存供两者动态使用避免静态数据段将可用空间分割。链接器默认行为链接器按脚本中的 SRAM分配.data和.bss时由于是连续放置自然形成.data在低地址、.bss在高地址。无需额外调整。与常量和代码的相对位置常量区 (.rodata) 和代码区 (.text) 位于 Flash与 SRAM 的地址物理分开所以高低关系只适用于 SRAM 和 Flash 组合的整个地址空间。若将两者看作统一地址空间的连续区间许多哈佛架构的芯片确实在同一 4GB 地址空间映射那么 Flash 通常映射在低地址SRAM 映射在高地址因而形成 SRAM 高位到 Flash 低位的顺序。六、堆和栈的初始化与生长方向栈MSP初始值在向量表中给出通常是 SRAM 最高地址的下一字。压栈时SP 先递减再存储这是“满递减”栈。栈区向低地址生长。堆由 C 库管理_sbrk()函数使用链接脚本定义的__heap_base和__heap_limit来分配空间。堆的起始地址在.bss之后每次分配内存时堆指针向上移动递增即向高地址生长。因此堆向上、栈向下两者之间的空隙是它们共用的动态区域。如果堆栈碰撞越界重叠就会发生不可预知的行为这是嵌入式系统中常见的内存问题。七、C 运行时库初始化的深入细节__libc_init_array的具体行为以 newlib 为例void__libc_init_array(void){size_tcount,i;// 调用 .preinit_array 中的函数count__preinit_array_end-__preinit_array_start;for(i0;icount;i)__preinit_array_start[i]();// 调用 .init_array 中的函数C 构造函数count__init_array_end-__init_array_start;for(i0;icount;i)__init_array_start[i]();}链接时编译器收集所有标记为constructor的函数指针、C 全局对象的构造函数并放入.init_array段。这些数组的起止符号通常由链接脚本提供__preinit_array_start .; *(.preinit_array*) __preinit_array_end .; __init_array_start .; *(.init_array*) __init_array_end .;有些工具链还会处理.fini_array析构函数但它们由atexit注册通常在main返回后才执行。八、整体流程的时钟视角从外部看整个过程的时间顺序是上电复位POR→ 内部 RC 起振CPU 运行。硬件取向量1 ~ 2 个周期。Reset_Handler 前几条指令关闭中断、FPU 等→ 数十个周期。.data复制和.bss清零取决于段大小可能数千周期。C 库初始化调用构造函数→ 依赖全局对象数量。main执行。在许多工程中用户常会在main之前增加一个SystemInit()调用配置外部时钟HSE/PLL这会消耗一段等待时钟稳定的时间毫秒级然后才真正进入用户逻辑。九、特殊段与变体.ccmram有些 MCU 有核心耦合内存CCM RAM需要独立链接段通常紧接.bss或位于另一地址空间需单独初始化。.noinit不希望上电后被清零的变量如掉电保存标志可以放到保留区链接脚本跳过清零操作。双核或双栈使用 MSP 和 PSP 的 RTOS 系统会在进入main后再初始化进程栈和线程模式但启动时仍然沿用上述流程。小结从硬件自动取向量到main执行背后是向量表、链接脚本、汇编启动代码、C 运行时库的精妙配合。你给出的内存布局描述——“高地址栈、堆、全局区.bss 高 .data 低、常量区、代码区”——正是这一配合在物理地址上的投影。理解了这个过程就能从源头掌握嵌入式系统的运行基石也为分析堆栈溢出、数据未初始化等问题奠定了坚实基础。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →