跟着下面的视频,系统梳理复习过往的嵌入式知识,同时也算是 电控成长指南 的一个 部分 实现,能有效且快速的建立一个嵌入式系统和工具链的整体认知
【《从CPU架构到操作系统实现》系列课程 - Git 开源】https://www.bilibili.com/video/BV1ksNCzXEny?vd_source=5a0790755035f26a67935abfbfcdfd5b
视频中详细且硬核的实例不会出现在此笔记中
概述
关于ARM架构
目前arm内核主要演变分为Cortex-X,Cortex-A,Cortex-R,Cortex-M
Cortex-A:应用处理器,设计用来处理复杂应用(例如高端嵌入操作系统Linux,iOS,Android,Windows),对标PC和手机处理器的性能
Cortex-X:比Cortex-M更高的性能
Cortex-R:Real-time,主要用于实时系统
Cortex-M:Microcontrol,微处理器
Cortex-M优点
- 低功耗
- 高性能
- 中断易用
- 代码密度高(原因在于指令集)
- 可调式
- 支持操作系统
哈佛架构,指令与数据在同一内存

指令集
多套指令集用于处理不同的场景

1.CISC(Complex Instruction Set Computer)
复杂指令系统计算机,CISC指令集包含大量复杂的指令,每条指令可以完成多个操作,这种设计使得编程简单,但硬件实现复杂,执行效率低
x86架构(Intel的8086,8088,80286,80386)
2.RISC(Reduced Instruction Set Computer)
精简指令系统计算机,RISC指令集包含较少的简单指令,每条指令只完成一个操作,使得硬件实现更简单,执行效率更高,但编程更复杂
ARM架构,RISC-V架构,MIPS架构
开发环境(DEV)
GUN-GCC编译器,CMake->makefile
STM32F103C8(C8T6)经典控制器(Cortex-M3)
编译
编译链工具
ARM-Cortex-M处理器的编译器很多,常用的是MDK-ARM(ARM公司自研的)(Keil),以及GCC(GNU-Compiler-Collection)
编译器之间的最大区别在于汇编伪代码,链接语法,还有使用参数

arm-none-eabi
arm是指arm架构,none是指编译出的可执行文件是运行在硬件上,而不是操作系统上,eabi(The Embedded-Application Binary Interface)嵌入式应用二进制接口
arm-none-eabi-objcopy是一个二进制生成工具
嵌入式编译烧录流程
个人更喜欢将编译过程拆解为 预编译,编译,汇编,链接
转换这里是将.elf文件转换为.bin文件
.bin文件是代码的纯机器码
预编译
将所有头文件和宏展开,同时去掉注释,生成.i文件(c中间文件)
编译
将.i文件编译为.s汇编文件,编译优化发生在此处
汇编
将.s文件汇编为.o二进制文件(机器码)
二进制文件中还包含很多的其他信息
在汇编时,可以指定汇编器的参数来选择指令集
链接
链接是将各个二进制文件里面的机器码所在的各个段拼接起来,以形成可执行文件.elf(带有调试信息)
在链接这个阶段才能检测出有不有重复定义和未定义的错误
对于GNU来说,可以编写.ld链接脚本来编排代码的分区地址
转换
通过转换器能将.elf文件转换为.hex和.bin文件,主要用于提取信息
.bin文件只包含二进制机器码
.hex文件是Intel定义的包含地址和二进制数值的ASC码文本文件
烧录
烧录的过程是将编译好的机器码写到CPU存储器中
反汇编
GCC中的反汇编工具Disassembler是arm-none-eabi-objdump
启动
主要是讲解STM32的启动流程
中断向量表
中断向量表的构建,根据不同的编译工具,有不同的方式构建
中断向量表的前两项(.woed,4字节)是最关键的
第一项是堆栈指针的值
第二项是Reset_Handler函数入口
在启动时,需要先设置堆栈指针,否则无法进行压栈的操作,从宏观上说,意味着C语言函数体不能调用子函数
VTOR寄存器,中断向量表偏移寄存器
启动原理

启动后,从零地址(零地址是经过映射的)处读取堆栈指针和程序计数器的值(此时程序计数器的值指向Reset_Handler),此外每个芯片的Boot code是定制化的,需要参考手册
最后Reset_Handler会指向main函数
以STM32F103为例

这里有三种启动模式,根据启动时BOOT1和BOOT0的电平来选择不同的启动地址(即 将该不同的地址映射为零地址)
三种启动模式分别是 从 flash 引导启动,从 system memory 引导启动 和 从 SRAM 引导启动(即 将该不同的地址映射为零地址)
- 从主 Flash 启动:将主 Flash 地址 0x0800 0000 映射到 0x0000 0000,代码启动后相当于从 0x0800 0000 开始运行,使用 JTAG 或者 SWD 模式下载程序时会下载到 Flash 并从中启动
- 从系统存储器启动:将系统存储器地址 0x1FFF F000 映射到 0x0000 0000,系统存储器是芯片内部的 ROM 区域,芯片出厂时在其中预置了 Bootloader,称为 ISP 程序。Bootloader 由厂家设置,一般无法修改
- 从内置 SRAM 启动:将 SRAM 地址 0x2000 0000 映射到 0x0000 0000,代码启动后相当于从 0x2000 0000 开始运行。由于 SRAM 是易失性存储器,因此这个模式一般仅用于程序调试
(以
STM32F103为例)
存储分布
以stm32f103c8t6为例
Flash区
Flash区,起始位置0x8000000
| 段名称 | 起始地址 | 结束地址 | 内容 |
|---|---|---|---|
| .isr_vector | 0x8000 0000 | 按需计算,四字节对齐 | 中断向量表 |
| .text | 紧邻.isr_vector末尾 | 按需计算,四字节对齐 | 程序 |
| .rodata | 紧邻.text末尾 | 按需计算,四字节对齐 | 常量 |
| .ARM.extab | 紧邻.rodata末尾 | 按需计算 | ARM异常处理信息 |
| .ARM.exidx* | 紧邻.ARM.extab末尾 | 按需计算 | ARM异常索引表 |
| .preinit_array | 紧邻.ARM.exidx* 末尾 | 按需计算 | C++预初始化数组 |
| .init_array | 紧邻.preinit_array末尾 | 按需计算 | C++构造函数数组 |
| .fini_array | 紧邻.init_array末尾 | 按需计算 | C++析构函数数组 |
| .data | 紧邻.fini_array末尾 | 按需计算,四字节对齐 | 初始化变量的初始值 |
RAM区
RAM区,起始位置0x20000000
| 段名称 | 起始地址 | 结束地址 | 内容 |
|---|---|---|---|
| .data | 0x2000 0000 | _edata,拷贝自Flash区 | 已初始化全局/静态变量 |
| .bss | _edata | _end | 未初始化全局/静态变量 |
| 系统堆底 | _end | _end | 堆底 |
| 系统堆栈最小预留空间 | _end | _end+预留空间 | 堆栈预留空间 |
| 空闲RAM | _end+预留空间 | _estack | 空闲空间 |
| 系统栈底 | _estack(RAM末尾) | _estack(RAM末尾) | 栈底 |

如果使用了FreeRTOS的话,FreeRTOS的内存布局如下

内存分配规则
- 全局变量、局部静态变量:根据是否初始化决定分配在.data段或.bss段
- 用const关键字声明的常量:分配在Flash中
- 使用C标准的malloc申请的内存:分配在系统堆中
- 不在任务中定义的临时变量:分配在系统栈中
- 使用
pvPortMalloc开辟的内存或调用FreeRTOS标准API自动分配的内存:分配在FreeRTOS堆中 - 在任务中定义的临时变量:分配在对应任务的任务栈中
- 在中断中使用*
pvPortMallocFromISR分配的内存:分配在FreeRTOS堆中 - 在中断中禁止使用C标准的malloc和free
- 中断中定义的临时变量:分配在系统
启动文件详解
一、STM32F103 启动文件(startup_stm32f103xb.s)核心定位
STM32 的启动文件(如startup_stm32f103xb.s)是汇编语言编写的底层程序,是硬件复位后执行的第一段代码,负责衔接 “硬件初始化” 与 “用户 C 代码(main函数)”。其核心功能包括:设置栈指针、构建中断向量表、初始化数据段(.data)、清零未初始化数据段(.bss)、配置系统时钟,最终引导程序进入main函数,是 STM32 程序运行的 “底层基石”。
二、启动文件整体结构拆解
该启动文件按功能可划分为 5 个核心模块,各模块职责明确且环环相扣:
- 汇编环境配置:指定处理器架构、指令集、浮点模式,确保汇编代码与硬件兼容。
- 关键符号声明:引用链接器脚本(
.ld)中定义的内存地址符号,建立启动文件与内存布局的关联。 - 复位处理函数(
Reset_Handler):复位后执行的核心逻辑,完成初始化并跳转到main。 - 中断向量表(
g_pfnVectors):存储所有异常 / 中断的处理函数地址,供内核响应中断时调用。 - 默认中断处理函数(
Default_Handler):未自定义的中断会默认进入此处,避免系统崩溃。
三、逐段解析启动文件代码
1. 汇编环境配置(开头基础设置)
|
|
- 作用:告诉汇编器(如
arm-none-eabi-as)“如何编译这段代码”,确保生成的指令能被 STM32F103 正确执行。 - 关键细节:
softvfp对应 STM32F103 的硬件特性 —— 该型号无硬件浮点处理单元(FPU),需通过软件模拟浮点运算,与之前解析的.ld文件中未配置 FPU 参数完全匹配。
2. 关键符号声明(与链接器脚本联动)
|
|
- 核心意义:通过
.word指令声明的符号,直接引用.ld文件中根据硬件内存布局(64K FLASH、20K RAM)计算出的地址。例如:_sdata和_edata确定了.data段在 RAM 中的范围,_sidata确定了其初始值在 FLASH 中的位置,为后续 “数据段复制” 提供地址依据。- 若缺少这些符号,启动文件无法知道 “数据该从哪里复制到哪里”,内存初始化会彻底失败。
3. 复位处理函数(Reset_Handler:启动核心逻辑)
Reset_Handler是复位后执行的第一个函数(由中断向量表指定),对应 STM32 启动流程中的 “软件初始化” 阶段,代码逻辑可分为 5 步:
|
|
关键步骤解读:
- 系统时钟初始化(
SystemInit):STM32 复位后默认使用 8MHz 的内部高速时钟(HSI),SystemInit函数会配置外部高速时钟(HSE)和锁相环(PLL),将系统时钟提升到 72MHz(F103 的常用工作频率),确保外设和 CPU 高效运行。 - .data 段复制:
.data段存储 “已初始化的全局 / 静态变量”(如int a = 10;),这些变量需要读写权限,因此运行时必须在 RAM 中;但编译时其初始值会存放在 FLASH(节省 RAM),需通过此步骤复制到 RAM。 - .bss 段清零:
.bss段存储 “未初始化的全局 / 静态变量”(如int b;),C 语言标准要求其初始值为 0,因此无需在 FLASH 中存储初始值,直接将 RAM 对应区域清零即可。 - 跳转到
main:这是启动文件的 “最终目标”—— 完成所有底层初始化后,将程序控制权交给用户代码,正式进入应用逻辑。
4. 中断向量表(g_pfnVectors:中断响应的 “地址簿”)
中断向量表是Cortex-M3 内核复位后访问的第一个数据结构,存储了所有异常(如复位、硬 fault)和外设中断(如串口、定时器)的处理函数地址。当发生中断 / 异常时,内核会从向量表中找到对应地址,跳转到处理函数执行。
|
|
核心特性:
- 位置固定:在
.ld文件中,.isr_vector段被强制分配到 FLASH 起始地址(0x8000000),这是 Cortex-M3 内核的硬件规定 —— 复位后内核会自动从0x8000000读取向量表。 - 第 0 个元素必为栈顶:内核复位后做的第一件事是 “初始化栈指针(MSP)”,因此向量表第 0 个元素必须是栈顶地址(
_estack,来自.ld 文件),否则后续函数调用、局部变量存储会因栈地址错误崩溃。 - 中断优先级隐含:向量表中元素的顺序对应中断 / 异常的优先级(靠前的优先级更高),例如 “硬 fault”(第 3 个元素)优先级高于 “SysTick”(第 15 个元素)。
5. 默认中断处理函数(Default_Handler:中断 “兜底” 逻辑)
当用户未为某个中断定义处理函数时,中断向量表会将该中断的地址指向Default_Handler,避免系统因 “找不到中断处理函数” 而进入未知状态。
|
|
弱别名机制(用户可重定义中断函数)
启动文件通过 “弱别名(.weak)” 允许用户在 C 代码中重定义中断处理函数,具体逻辑如下:
|
|
- 工作原理:
.weak标记表示 “该符号是弱定义的,若用户定义了同名符号,优先使用用户定义的版本”。- 用户只需在 C 代码中编写
void USART1_IRQHandler(void) { ... },链接时会自动覆盖启动文件中的弱定义,使串口 1 中断触发时执行用户代码。 - 若用户未定义,则使用默认的
Default_Handler(无限循环)。
四、启动文件与链接器脚本(.ld)的配合关系
启动文件与.ld文件是紧密耦合的 “搭档”,二者通过 “全局符号” 和 “段分配” 协同工作,缺一不可:
| 协作维度 | 启动文件角色 | 链接器脚本(.ld)角色 |
|---|---|---|
| 内存地址提供 | 引用.ld定义的_sdata、_ebss、_estack等符号 |
根据硬件内存布局(64K FLASH、20K RAM)计算并定义这些符号 |
| 段分配依据 | 将Reset_Handler放入.text段、向量表放入.isr_vector段 |
规定.text、.isr_vector等段的存储位置(如.isr_vector分配到 FLASH 起始地址) |
| 程序入口指定 | 实现Reset_Handler函数(入口逻辑) |
通过ENTRY(Reset_Handler)指定程序入口为该函数 |
| 内存合法性检查 | 依赖.ld确保_sdata、_ebss不超出 RAM 范围 |
检查堆、栈、数据段总大小是否超过 RAM/FLASH 容量,溢出则报错 |
五、总结:启动文件的核心价值
STM32 启动文件是 “从硬件到软件的桥梁”,其核心作用可概括为 3 点:
- 硬件初始化:完成栈指针设置、系统时钟配置,为程序运行提供基础硬件环境。
- 内存初始化:通过复制
.data段、清零.bss段,确保全局 / 静态变量符合 C 语言标准要求。 - 中断系统准备:构建中断向量表,提供默认中断处理逻辑,支持用户自定义中断函数。
- 引导用户代码:最终跳转到
main函数,将程序控制权交给用户,完成 “底层初始化→应用逻辑” 的过渡。 简言之:没有启动文件,STM32 复位后无法完成任何初始化,更无法进入main函数 —— 它是 STM32 程序运行的 “第一块拼图”。
STM32 启动流程、.ld 文件作用及示例解析
要理解 STM32 的启动流程、.ld文件(链接器脚本)的作用,以及你提供的具体.ld文件内容,我们需要从宏观启动逻辑到链接器脚本细节逐步拆解,最终建立两者的关联。
一、STM32 的启动流程(从复位到main函数)
STM32 的启动流程是硬件复位→内核初始化→软件初始化→进入main 的过程,核心依赖「中断向量表」和「启动文件」,而.ld文件为这个流程提供了内存地址映射和段分配规则。
1. 硬件复位:PC 指针的初始指向
当 STM32 上电或复位时,Cortex-M 内核会自动执行以下操作:
- 读取中断向量表的第一个元素(栈顶地址
_estack),初始化主栈指针(MSP)—— 这是内核执行的第一个动作,因为函数调用、局部变量都依赖栈。 - 将程序计数器(PC)指向中断向量表的第二个元素(
Reset_Handler的地址)—— 这是复位后的第一个执行函数(启动流程的入口)。
关键:中断向量表的默认地址是
0x8000000(STM32 FLASH 的起始地址),这也是.ld文件中FLASH的ORIGIN(起始地址)设置为0x8000000的原因。
2. 执行Reset_Handler(启动文件核心逻辑)
Reset_Handler是启动文件(如startup_stm32f103c8tx.s)中的汇编函数,是软件初始化的核心,其逻辑完全依赖.ld文件定义的全局符号(如_sdata、_ebss),具体步骤如下:
-
初始化数据段(.data):
.data段存放「初始化过的全局 / 静态变量」(如int a = 10;),这些变量需要读写权限,因此运行时必须在 RAM 中;但编译时会把它们的初始值存放在 FLASH 中(节省 RAM 空间)。
Reset_Handler会执行复制操作:从.ld文件定义的_sidata(.data 在 FLASH 的加载地址)复制到_sdata(.data 在 RAM 的运行地址),直到_edata(.data 的结束地址)。 -
清零未初始化数据段(.bss):
.bss段存放「未初始化的全局 / 静态变量」(如int b;),C 语言标准要求它们初始化为 0。
Reset_Handler会将_sbss(.bss 的起始地址)到_ebss(.bss 的结束地址)的 RAM 区域全部清零。 -
初始化系统时钟(SystemInit):
调用SystemInit函数(由 STM32 标准库 / LL 库提供),配置 HSE/PLL 等,将系统时钟从默认的 HSI(8MHz)切换到更高频率(如 72MHz)。 -
调用
main函数:
完成所有初始化后,跳转到 C 语言的main函数,进入用户应用逻辑。
3. 异常 / 中断的触发(补充)
若程序运行中触发中断(如定时器、串口),内核会再次读取「中断向量表」,根据中断号找到对应的中断服务函数(ISR)地址,跳转到 ISR 执行,执行完成后返回断点继续运行。
二、.ld 文件(链接器脚本)的核心作用
.ld文件是链接器(ld)的配置文件,其本质是告诉链接器:
- 芯片的内存布局:FLASH(只读、存代码 / 常量)和 RAM(可读写、存变量 / 栈 / 堆)的起始地址和大小。
- 代码 / 数据的段分配规则:将编译生成的各个「段(Section)」(如
.text、.data)分配到对应的内存区域(FLASH/RAM)。 - 定义全局符号:生成
_sdata、_ebss、_estack等符号,供启动文件和用户代码使用(如启动文件依赖这些符号初始化内存)。 - 内存合法性检查:确保堆、栈、数据段的总大小不超过 RAM/FLASH 的实际容量,避免内存溢出。
简单说:没有
.ld文件,链接器无法知道 “代码该放哪里、变量该放哪里”,无法生成可执行的.elf/.hex文件。
三、解析 STM32F103C8Tx 的.ld 文件
以下按文件结构逐段解析,结合 STM32F103C8Tx 的硬件参数(64K FLASH、20K RAM)说明每部分的作用。
1. 入口点与栈顶定义
|
|
ENTRY(Reset_Handler):强制链接器将Reset_Handler作为程序第一个执行的函数,与启动流程中 PC 指向Reset_Handler完全对应。_estack:定义栈顶地址(STM32F103C8Tx 的 RAM 是0x20000000~0x20004FFF,因此_estack=0x20005000),启动文件会将这个地址加载到 MSP(主栈指针)。- 堆 / 栈大小:
_Min_Heap_Size和_Min_Stack_Size是最小要求,若用户代码中堆 / 栈使用超过这个值,链接器会报错(防止 RAM 溢出),可根据需求调整(如栈不够时增大_Min_Stack_Size)。
2. 内存区域定义(MEMORY 块)
|
|
这是.ld文件的核心,定义了芯片的物理内存属性:
RAM (xrw):xrw:权限(x= 可执行,r= 可读,w= 可写)——RAM 支持读写,理论上可执行(但通常不放代码)。ORIGIN = 0x20000000:STM32 所有 Cortex-M3 内核芯片的 RAM 起始地址(硬件规定)。LENGTH = 20K:STM32F103C8Tx 的 RAM 实际大小(20KB = 0x5000 字节,范围0x20000000~0x20004FFF)。
FLASH (rx):rx:权限(r= 可读,x= 可执行,w= 不可写)——FLASH 是只读存储器,只能存放代码和常量。ORIGIN = 0x8000000:STM32 FLASH 的起始地址(硬件规定,复位后 PC 默认指向这里)。LENGTH = 64K:STM32F103C8Tx 的 FLASH 实际大小(64KB = 0x10000 字节,范围0x8000000~0x800FFFF)。
3. 段分配规则(SECTIONS 块)
SECTIONS块是最复杂的部分,定义了「编译生成的段」如何映射到「MEMORY 块定义的物理内存」,每一个子段都对应特定的代码 / 数据类型。
Flash区,起始位置0x8000000
| 段名称 | 起始地址 | 结束地址 | 内容 |
|---|---|---|---|
| .isr_vector | 0x8000 0000 | 按需计算,四字节对齐 | 中断向量表 |
| .text | 紧邻.isr_vector末尾 | 按需计算,四字节对齐 | 程序 |
| .rodata | 紧邻.text末尾 | 按需计算,四字节对齐 | 常量 |
| .ARM.extab | 紧邻.rodata末尾 | 按需计算 | ARM异常处理信息 |
| .ARM.exidx* | 紧邻.ARM.extab末尾 | 按需计算 | ARM异常索引表 |
| .preinit_array | 紧邻.ARM.exidx* 末尾 | 按需计算 | C++预初始化数组 |
| .init_array | 紧邻.preinit_array末尾 | 按需计算 | C++构造函数数组 |
| .fini_array | 紧邻.init_array末尾 | 按需计算 | C++析构函数数组 |
| .data | 紧邻.fini_array末尾 | 按需计算,四字节对齐 | 初始化变量的初始值 |
RAM区,起始位置0x20000000
| 段名称 | 起始地址 | 结束地址 | 内容 |
|---|---|---|---|
| .data | 0x2000 0000 | _edata,拷贝自Flash区 | 已初始化全局/静态变量 |
| .bss | _edata | _end | 未初始化全局/静态变量 |
| 系统堆底 | _end | _end | 堆底 |
| 系统堆栈最小预留空间 | _end | _end+预留空间 | 堆栈预留空间 |
| 空闲RAM | _end+预留空间 | _estack | 空闲空间 |
| 系统栈底 | _estack(RAM末尾) | _estack(RAM末尾) | 栈底 |
(1).isr_vector:中断向量表(放 FLASH)
|
|
- 作用:存放 STM32 的中断向量表(如复位向量、定时器中断向量、串口中断向量等),是复位后内核第一个访问的段。
KEEP(*(.isr_vector)):中断向量表由启动文件定义(如startup_stm32f103c8tx.s中的g_pfnVectors数组),KEEP关键字确保链接器不会因为 “看似未被调用” 而删除这个段 —— 如果删除,复位后内核找不到Reset_Handler地址,程序会崩溃。>FLASH:明确将该段分配到MEMORY块定义的FLASH区域(符合硬件要求,向量表默认在 FLASH 起始地址)。
(2).text:代码段(放 FLASH)
|
|
- 作用:存放所有可执行代码(C 函数、汇编函数),是 FLASH 中占用空间最大的段。
_etext = .:.代表当前地址,_etext是全局符号,标记.text段的结束地址 —— 后续的.rodata段会从_etext之后的地址开始分配,避免段重叠。
(3).rodata:只读常量段(放 FLASH)
|
|
- 作用:存放只读数据,因为这些数据不需要修改,放在 FLASH 中可节省 RAM 空间。
- 示例:
const char str[] = "STM32";会被编译到.rodata段,运行时直接从 FLASH 读取,不会复制到 RAM。
(4).ARM.extab/.ARM:ARM 架构兼容段(放 FLASH)
|
|
- 作用:主要用于ARM 架构与 Thumb 架构的兼容,以及 C++ 的异常处理(若项目用纯 C,这些段几乎为空,但保留可避免链接错误)。
READONLY:标记为只读,与 FLASH 的权限匹配(GCC11 + 支持,低版本需删除)。
(5).preinit_array/.init_array/.fini_array:初始化 / 结束函数数组(放 FLASH)
|
|
- 作用:存放程序启动 / 退出时自动执行的函数数组(主要用于 C++),例如:
init_array:main函数执行前,会自动调用这里的函数(如全局对象A a;的构造函数)。fini_array:main函数退出后,会自动调用这里的函数(如全局对象的析构函数)。
PROVIDE_HIDDEN:若用户代码未定义__init_array_start等符号,链接器自动提供(隐藏符号,避免冲突);KEEP确保这些段不被优化删除。
(6).data:初始化数据段(放 RAM,加载地址在 FLASH)
|
|
- 核心概念:
VMA(虚拟 / 运行地址)和LMA(加载地址):>RAM:.data段的运行地址(VMA) 在 RAM(因为变量需要读写)。AT>FLASH:.data段的加载地址(LMA) 在 FLASH(编译时将变量初始值存到 FLASH,节省 RAM)。
- 与启动流程关联:启动文件的
Reset_Handler会从_sidata(FLASH 的 LMA)复制数据到_sdata(RAM 的 VMA),直到_edata—— 这就是 “初始化.data 段” 的具体实现。 .RamFunc:某些对执行速度要求高的函数(如高频中断服务函数),可通过__attribute__((section(".RamFunc")))标记,链接时会放到 RAM 中执行(RAM 访问速度比 FLASH 快)。
(7).bss:未初始化数据段(放 RAM)
|
|
- 作用:存放未初始化的全局 / 静态变量,C 语言标准要求这些变量初始化为 0,因此不需要在 FLASH 中存储初始值(节省 FLASH 空间)。
- 与启动流程关联:启动文件的
Reset_Handler会将_sbss到_ebss的 RAM 区域清零 —— 这就是 “清零.bss 段” 的具体实现。 COMMON:对应未声明但外部引用的全局变量(如extern int c;),链接时会分配到.bss 段。
(8)._user_heap_stack:堆和栈的占位段(放 RAM)
|
|
- 作用:为堆和栈预留空间,并检查 RAM 是否足够 —— 若堆 + 栈 + 已分配段(.data+.bss)的总大小超过 RAM 容量,链接器会报错。
end/_end:标记 RAM 中 “已分配段(.data+.bss)” 的结束地址,堆从end开始向上生长,栈从_estack(栈顶)开始向下生长。- 堆 / 栈生长方向:
- 堆:从
end向上(地址增大方向)生长,供malloc/free使用。 - 栈:从
_estack向下(地址减小方向)生长,供函数调用、局部变量使用。 - 若堆和栈生长时重叠,会导致内存溢出(硬 fault),因此需合理设置
_Min_Heap_Size和_Min_Stack_Size。
- 堆:从

如果使用了FreeRTOS的话,FreeRTOS的内存布局如下

(9)/DISCARD/:丢弃无用段(减小可执行文件大小)
|
|
- 作用:嵌入式系统通常不需要标准库的完整功能(如
printf的文件输出、malloc的复杂内存管理),丢弃这些段可大幅减小.elf/.hex文件的大小。 - 注意:若项目需要使用标准库功能(如
printf、sqrt),需删除对应行,否则会出现 “未定义引用” 错误(需搭配newlib-nano等嵌入式精简库)。
四、总结:启动流程与.ld 文件的关联
- 硬件复位 → 内核读取
FLASH起始地址(0x8000000)的.isr_vector段 → 找到Reset_Handler地址。 - 执行
Reset_Handler→ 利用.ld定义的_sdata/_sidata/_edata复制.data段 → 利用_sbss/_ebss清零.bss段。 - 调用
main→ 用户代码中的变量(.data/.bss)已在 RAM 中初始化,函数(.text)在 FLASH 中执行,堆 / 栈在 RAM 中预留空间。 可以说:.ld 文件是 STM32 启动流程的 “地图”,没有它,启动文件不知道如何初始化内存,链接器不知道如何分配代码和数据,程序无法正常运行。
寄存器
认识一个CPU最直接的是去认识它的寄存器组
(Register bank)

arm系统寄存器组

还有5个特殊寄存器

浮点数寄存器

通用寄存器
通用寄存器通用性强,常用于存储临时的数据
其中R0-R7是低组寄存器,R8-R12是高组寄存器,高组寄存器部分指令无法访问
PC程序计数器&程序如何跑起来
arm架构中的R15寄存器,是程序计数器(program counter),该寄存器可读可写
在arm cortex-M的CPU内,每一次PC自增4(即向下移动了4个字节),但是在使用thumb指令集时,thumb指令集的部分指令是2个字节,这与处理器的执行流水线pipeline的特性有关,实际取址的时候是一次去4字节,也就是对于2字节的指令是一次取两条
CPU在不断执行程序,本质就是程序计数器的不断更新,PC指向的就是当前要执行的指令的地址
堆栈指针&双堆栈指针机制
栈是高址向低址方向生长,堆是低址向高址方向生长

arm cortex-M架构中R13是堆栈指针寄存器
在物理上该寄存器是两个不同的寄存器MSP主堆栈指针和PSP进程堆栈指针,在程序运行时选用哪一个寄存器会有Control控制寄存器的值来决定
默认启动时,处理器使用的是主堆栈指针,可以修改Control控制寄存器的第一位的值来修改处理器使用的堆栈指针
双堆栈指针-MSP主堆栈指针,PSP进程堆栈指针
可以用来实现隔离系统内核和应用程序任务
LR链接寄存器&函数调用的本质
函数调用的本质是程序执行流的动态转移与上下文管理,其核心在于通过 栈 来实现代码模块化执行和状态的保存与恢复
arm架构的CPU有专门设立一个寄存器R14(link register),用于存放子函数的返回地址
以arm的bl指令为例(用于函数调用)
bl <label>是跳转到标签地址,同时将返回地址存入LR寄存器
需要注意的是,arm cortex-M架构的处理器指令都是半字对齐(两字节对齐),所以其反汇编的地址都是偶数,存在lr寄存器的返回地址也都是偶数,但是lr寄存器的第0位必须设置为1来指示thumb指令集的状态,所以lr寄存器的值会是返回地址+1
在多级函数调用时,lr寄存器的值会被压入栈中
另外lr寄存器还能记录异常返回值。当处理器进入中断时,链接寄存器会更新为异常返回值,用来实现异常返回机制,用来记录区分一些工作模式现场,这些信息是恢复中断上下文需要的
地址分配
存储器系统
cortex-M处理器是32位地址,因此会有
4GB的地址空间,所有的指令和数据通过这个地址访问
硬件资源都是通过地址映射的方式访问
另外cortex-M架构没有设置MMU内存管理单元,所以没有MMU内存管理单元,一般也不会搭载Linux,Windows这种通用类型的系统
地址映射图

F103C8的地址映射图

系统架构图

cortex-M3M4内核架构图

三级流水线外的设备属于CPU的内核外设,例如NVIC,SysTick系统滴答定时器,MPU等,用0xE004 0000-0xE00F F000地址访问
软件上就是通过读写外设地址空间来控制这些外设,对应的地址空间单元映射到外设寄存器
嵌入式编程本质就是在操作存储器,每一段CPU代码指令都是在读取某个地址空间的单元,高级编程语言就是不断向上封装抽象
C语言每条语句追溯到汇编都是在读写存储器的地址空间单元

关于内存和编译优化
指针是内存地址的容器
关于i++与++i执行效率的问题,在O0优化的情况下,不同编译器的具体实现是不一样的,执行效率是不能直接比较的
编译优化会使汇编的指令在执行速度上提高,例如会使用mov去代替ldr,或者将一些简单的函数改为内嵌,以减少函数调用带来的开销
中断
在微机原理中已有涉及,后面只做简单回顾用
关于中断
中断的事件一般由硬件触发(实际有硬中断和软中断),会改变程序执行流

NVIC属于内核外设,专门管理中断
在cortex-M芯片中,前15项中断向量都一样

处理器具有向量表重定位功能,通过向量表偏移寄存器(VTOR)来获取向量表的起始地址
中断触发时,处理器通过中断向量表去获取中断处理函数的入口地址
中断返回&中断上下文
电脑为什么能边玩游戏边聊天?CPU这个骚操作,99%的程序员都说不清!| CPU / 多任务 / 进程上下文 / 中断机制_哔哩哔哩_bilibili
该视频中最后讲解的就类似于arm架构采用的方式
在阅读中断的汇编时,会发现中断没有保存和恢复上下文的代码,实际上是硬件帮忙实现上述功能
关于上下文,如程序上的函数调用需要保存上下文,在操作系统中需要处理线程上下文(任务上下文),其本质都在于保存和恢复若干寄存器的值

cortex-M3M4中断需要入栈保存上下文的8个寄存器
选择保存上述8个寄存器主要是根据arm架构的过程调用标准,C语言的函数实现会改动这些寄存器,为例能让C函数能作为异常(中断)处理函数,中断(异常)机制需要能自动地保存这些寄存器,这些寄存器也被称之为调用者保存寄存器
处理器恢复上下文,回到原执行流,需要lr寄存器存储异常返回值来记录区分一些工作模式现场,这些信息是恢复中断上下文需要的
错误处理
介绍arm cortex-M处理器架构如何捕获和记录错误故障
有专门的寄存器去记录故障,通过查找其对应的地址查看寄存器的位来获取错误信息

在发生错误时,对应错误寄存器的对应位被置1,同时触发错误中断处理
程序执行非法指令操作导致系统进入不可恢复的故障状态,系统软件就会介入终止程序执行流,其实现机制是中断