从CPU架构到操作系统实现

跟着下面的视频,系统梳理复习过往的嵌入式知识,同时也算是 电控成长指南 的一个 部分 实现,能有效且快速的建立一个嵌入式系统和工具链的整体认知

【《从CPU架构到操作系统实现》系列课程 - Git 开源】https://www.bilibili.com/video/BV1ksNCzXEny?vd_source=5a0790755035f26a67935abfbfcdfd5b

视频中详细且硬核的实例不会出现在此笔记中

概述

关于ARM架构

目前arm内核主要演变分为Cortex-XCortex-ACortex-RCortex-M

Cortex-A:应用处理器,设计用来处理复杂应用(例如高端嵌入操作系统LinuxiOSAndroidWindows),对标PC和手机处理器的性能

Cortex-X:比Cortex-M更高的性能

Cortex-RReal-time,主要用于实时系统

Cortex-MMicrocontrol,微处理器

Cortex-M优点

  1. 低功耗
  2. 高性能
  3. 中断易用
  4. 代码密度高(原因在于指令集)
  5. 可调式
  6. 支持操作系统 哈佛架构,指令与数据在同一内存

指令集

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

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文件是代码的纯机器码

一文看懂hex文件、bin文件、axf文件的区别 - 知乎

预编译

将所有头文件和宏展开,同时去掉注释,生成.i文件(c中间文件)

编译

.i文件编译为.s汇编文件,编译优化发生在此处

汇编

.s文件汇编为.o二进制文件(机器码) 二进制文件中还包含很多的其他信息

在汇编时,可以指定汇编器的参数来选择指令集

链接

链接是将各个二进制文件里面的机器码所在的各个段拼接起来,以形成可执行文件.elf(带有调试信息)

在链接这个阶段才能检测出有不有重复定义和未定义的错误

对于GNU来说,可以编写.ld链接脚本来编排代码的分区地址

转换

通过转换器能将.elf文件转换为.hex.bin文件,主要用于提取信息

.bin文件只包含二进制机器码 .hex文件是Intel定义的包含地址和二进制数值的ASC码文本文件

烧录

烧录的过程是将编译好的机器码写到CPU存储器中

反汇编

GCC中的反汇编工具Disassemblerarm-none-eabi-objdump

启动

主要是讲解STM32的启动流程

中断向量表

中断向量表的构建,根据不同的编译工具,有不同的方式构建

中断向量表的前两项(.woed,4字节)是最关键的 第一项是堆栈指针的值 第二项是Reset_Handler函数入口

在启动时,需要先设置堆栈指针,否则无法进行压栈的操作,从宏观上说,意味着C语言函数体不能调用子函数

VTOR寄存器,中断向量表偏移寄存器

启动原理

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

最后Reset_Handler会指向main函数

STM32F103为例

这里有三种启动模式,根据启动时BOOT1BOOT0的电平来选择不同的启动地址(即 将该不同的地址映射为零地址),一般情况下是第一种

RAM内存分布

启动文件详解

一、STM32F103 启动文件(startup_stm32f103xb.s)核心定位

STM32 的启动文件(如startup_stm32f103xb.s)是汇编语言编写的底层程序,是硬件复位后执行的第一段代码,负责衔接 “硬件初始化” 与 “用户 C 代码(main函数)”。其核心功能包括:设置栈指针、构建中断向量表、初始化数据段(.data)、清零未初始化数据段(.bss)、配置系统时钟,最终引导程序进入main函数,是 STM32 程序运行的 “底层基石”。

二、启动文件整体结构拆解

该启动文件按功能可划分为 5 个核心模块,各模块职责明确且环环相扣:

  1. 汇编环境配置:指定处理器架构、指令集、浮点模式,确保汇编代码与硬件兼容。
  2. 关键符号声明:引用链接器脚本(.ld)中定义的内存地址符号,建立启动文件与内存布局的关联。
  3. 复位处理函数(Reset_Handler:复位后执行的核心逻辑,完成初始化并跳转到main
  4. 中断向量表(g_pfnVectors:存储所有异常 / 中断的处理函数地址,供内核响应中断时调用。
  5. 默认中断处理函数(Default_Handler:未自定义的中断会默认进入此处,避免系统崩溃。

三、逐段解析启动文件代码

1. 汇编环境配置(开头基础设置)
1
2
3
4
  .syntax unified       ; 使用统一汇编语法(兼容ARM与Thumb指令集)
  .cpu cortex-m3        ; 目标处理器为Cortex-M3(STM32F103内核型号)
  .fpu softvfp          ; 浮点模式:软件模拟(STM32F103无硬件FPU)
  .thumb                ; 启用Thumb指令集(Cortex-M3仅支持Thumb/Thumb2指令)
  • 作用:告诉汇编器(如arm-none-eabi-as)“如何编译这段代码”,确保生成的指令能被 STM32F103 正确执行。
  • 关键细节softvfp对应 STM32F103 的硬件特性 —— 该型号无硬件浮点处理单元(FPU),需通过软件模拟浮点运算,与之前解析的.ld文件中未配置 FPU 参数完全匹配。
2. 关键符号声明(与链接器脚本联动)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.global g_pfnVectors    ; 全局符号:中断向量表(供链接器识别并分配地址)
.global Default_Handler ; 全局符号:默认中断处理函数(供中断向量表引用)

/* 引用链接器脚本(.ld)中定义的内存地址符号 */
.word _sidata  ; .data段在FLASH中的加载地址(LMA,初始值存放位置)
.word _sdata   ; .data段在RAM中的运行起始地址(VMA,变量实际存储位置)
.word _edata   ; .data段在RAM中的运行结束地址
.word _sbss    ; .bss段在RAM中的起始地址(未初始化变量存储区)
.word _ebss    ; .bss段在RAM中的结束地址

.equ  BootRAM, 0xF108F85F  ; 定义RAM启动模式的特殊地址(仅用于特定boot配置)
  • 核心意义:通过.word指令声明的符号,直接引用.ld文件中根据硬件内存布局(64K FLASH、20K RAM)计算出的地址。例如:
    • _sdata_edata确定了.data段在 RAM 中的范围,_sidata确定了其初始值在 FLASH 中的位置,为后续 “数据段复制” 提供地址依据。
    • 若缺少这些符号,启动文件无法知道 “数据该从哪里复制到哪里”,内存初始化会彻底失败。
3. 复位处理函数(Reset_Handler:启动核心逻辑)

Reset_Handler是复位后执行的第一个函数(由中断向量表指定),对应 STM32 启动流程中的 “软件初始化” 阶段,代码逻辑可分为 5 步:

asm

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
  .section .text.Reset_Handler  ; 将函数放入.text段(代码段,.ld中分配到FLASH)
  .weak Reset_Handler           ; 弱定义:允许用户代码重定义该函数(覆盖默认逻辑)
  .type Reset_Handler, %function ; 标记为函数类型(汇编器识别函数边界)
Reset_Handler:

  /* 步骤1:初始化系统时钟 */
    bl  SystemInit  ; 跳转到STM32库函数SystemInit(配置HSE/PLL,将时钟从8MHz HSI切换到72MHz)

  /* 步骤2:复制.data段(从FLASH到RAM) */
  ldr r0, =_sdata   ; r0 = .data段在RAM的起始地址(VMA)
  ldr r1, =_edata   ; r1 = .data段在RAM的结束地址
  ldr r2, =_sidata  ; r2 = .data段在FLASH的加载地址(LMA)
  movs r3, #0       ; r3 = 偏移量(初始为0,每次复制4字节后递增)
  b LoopCopyDataInit ; 跳转到循环复制入口

CopyDataInit:
  ldr r4, [r2, r3]  ; 从FLASH读取数据:r4 = *(r2 + r3)(源地址=加载地址+偏移量)
  str r4, [r0, r3]  ; 写入RAM:*(r0 + r3) = r4(目标地址=运行地址+偏移量)
  adds r3, r3, #4   ; 偏移量+4(32位数据,每次复制1个word)

LoopCopyDataInit:
  adds r4, r0, r3   ; r4 = 当前复制位置(运行地址+偏移量)
  cmp r4, r1        ; 比较当前位置与结束地址:是否复制完成?
  bcc CopyDataInit  ; 若未完成(r4 < r1),继续循环复制
  
  /* 步骤3:清零.bss段(未初始化变量区) */
  ldr r2, =_sbss    ; r2 = .bss段在RAM的起始地址
  ldr r4, =_ebss    ; r4 = .bss段在RAM的结束地址
  movs r3, #0       ; r3 = 0(用于清零)
  b LoopFillZerobss ; 跳转到循环清零入口

FillZerobss:
  str  r3, [r2]     ; 将0写入当前地址:*r2 = 0
  adds r2, r2, #4   ; 地址+4(每次清零1个word)

LoopFillZerobss:
  cmp r2, r4        ; 比较当前地址与结束地址:是否清零完成?
  bcc FillZerobss   ; 若未完成(r2 < r4),继续循环清零

  /* 步骤4:初始化C++静态构造函数(兼容C++项目) */
    bl __libc_init_array  ; 调用C库函数,执行全局对象构造(纯C项目可忽略,不影响运行)

  /* 步骤5:跳转到用户main函数 */
  bl main   ; 跳转到用户C代码的main函数(启动流程结束,交权给用户)
  bx lr     ; 若main函数返回(实际中main通常是死循环),跳回当前位置(无实际意义)
.size Reset_Handler, .-Reset_Handler  ; 定义函数大小(.表示当前地址,计算函数长度)
关键步骤解读:
  • 系统时钟初始化(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)和外设中断(如串口、定时器)的处理函数地址。当发生中断 / 异常时,内核会从向量表中找到对应地址,跳转到处理函数执行。

asm

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
  .section .isr_vector,"a",%progbits  ; 将向量表放入.isr_vector段(.ld中分配到FLASH起始地址0x8000000)
  .type g_pfnVectors, %object         ; 标记为数据对象(而非函数)
  .size g_pfnVectors, .-g_pfnVectors  ; 定义向量表大小(计算表的总字节数)

g_pfnVectors:
  .word _estack                       ; 第0个元素:栈顶地址(MSP初始值,来自.ld文件)
  .word Reset_Handler                 ; 第1个元素:复位处理函数地址(复位后PC指向这里)
  .word NMI_Handler                   ; 第2个元素:不可屏蔽中断(NMI)处理函数
  .word HardFault_Handler             ; 第3个元素:硬fault异常处理函数(严重错误,如内存访问越界)
  .word MemManage_Handler             ; 第4个元素:内存管理异常处理函数
  .word BusFault_Handler              ; 第5个元素:总线fault异常处理函数(如访问不存在的外设地址)
  .word UsageFault_Handler            ; 第6个元素:用法fault异常处理函数(如指令错误)
  .word 0                             ; 第7-10个元素:保留(未使用)
  .word 0
  .word 0
  .word 0
  .word SVC_Handler                   ; 第11个元素:系统服务调用(SVC)处理函数(OS常用)
  .word DebugMon_Handler              ; 第12个元素:调试监控处理函数
  .word 0                             ; 第13个元素:保留
  .word PendSV_Handler                ; 第14个元素:PendSV异常处理函数(任务切换常用)
  .word SysTick_Handler               ; 第15个元素:系统滴答定时器(SysTick)处理函数
  /* 以下为外设中断向量(第16个元素开始) */
  .word WWDG_IRQHandler               ; 窗口看门狗中断
  .word PVD_IRQHandler                ; 电源电压检测中断
  .word TAMPER_IRQHandler             ; 篡改检测中断
  .word RTC_IRQHandler                ; RTC时钟中断
  .word FLASH_IRQHandler              ; FLASH操作中断
  .word RCC_IRQHandler                ; RCC时钟控制中断
  .word EXTI0_IRQHandler              ; 外部中断0(如PA0)中断
  ...(省略其余外设中断,共60+个,对应F103所有外设...
  .word BootRAM                       ; RAM启动模式的特殊地址(仅用于从RAM启动,默认从FLASH启动时无用)
核心特性:
  • 位置固定:在.ld文件中,.isr_vector段被强制分配到 FLASH 起始地址(0x8000000),这是 Cortex-M3 内核的硬件规定 —— 复位后内核会自动从0x8000000读取向量表。
  • 第 0 个元素必为栈顶:内核复位后做的第一件事是 “初始化栈指针(MSP)”,因此向量表第 0 个元素必须是栈顶地址(_estack,来自.ld 文件),否则后续函数调用、局部变量存储会因栈地址错误崩溃。
  • 中断优先级隐含:向量表中元素的顺序对应中断 / 异常的优先级(靠前的优先级更高),例如 “硬 fault”(第 3 个元素)优先级高于 “SysTick”(第 15 个元素)。
5. 默认中断处理函数(Default_Handler:中断 “兜底” 逻辑)

当用户未为某个中断定义处理函数时,中断向量表会将该中断的地址指向Default_Handler,避免系统因 “找不到中断处理函数” 而进入未知状态。

1
2
3
4
5
    .section .text.Default_Handler,"ax",%progbits  ; 放入.text段,属性为可执行(ax)
Default_Handler:
Infinite_Loop:
  b Infinite_Loop  ; 无限循环(死循环),防止程序跑飞
  .size Default_Handler, .-Default_Handler  ; 定义函数大小
弱别名机制(用户可重定义中断函数)

启动文件通过 “弱别名(.weak)” 允许用户在 C 代码中重定义中断处理函数,具体逻辑如下:

1
2
  .weak NMI_Handler                  ; 弱定义:NMI_Handler可被用户代码覆盖
  .thumb_set NMI_Handler,Default_Handler  ; 若用户未定义,NMI_Handler默认指向Default_Handler
  • 工作原理
    1. .weak标记表示 “该符号是弱定义的,若用户定义了同名符号,优先使用用户定义的版本”。
    2. 用户只需在 C 代码中编写void USART1_IRQHandler(void) { ... },链接时会自动覆盖启动文件中的弱定义,使串口 1 中断触发时执行用户代码。
    3. 若用户未定义,则使用默认的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 点:

  1. 硬件初始化:完成栈指针设置、系统时钟配置,为程序运行提供基础硬件环境。
  2. 内存初始化:通过复制.data段、清零.bss段,确保全局 / 静态变量符合 C 语言标准要求。
  3. 中断系统准备:构建中断向量表,提供默认中断处理逻辑,支持用户自定义中断函数。
  4. 引导用户代码:最终跳转到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文件中FLASHORIGIN(起始地址)设置为0x8000000的原因。

2. 执行Reset_Handler(启动文件核心逻辑)

Reset_Handler是启动文件(如startup_stm32f103c8tx.s)中的汇编函数,是软件初始化的核心,其逻辑完全依赖.ld文件定义的全局符号(如_sdata_ebss),具体步骤如下:

  1. 初始化数据段(.data)
    .data段存放「初始化过的全局 / 静态变量」(如int a = 10;),这些变量需要读写权限,因此运行时必须在 RAM 中;但编译时会把它们的初始值存放在 FLASH 中(节省 RAM 空间)。
    Reset_Handler会执行复制操作:从.ld文件定义的_sidata(.data 在 FLASH 的加载地址)复制到_sdata(.data 在 RAM 的运行地址),直到_edata(.data 的结束地址)。

  2. 清零未初始化数据段(.bss)
    .bss段存放「未初始化的全局 / 静态变量」(如int b;),C 语言标准要求它们初始化为 0。
    Reset_Handler会将_sbss(.bss 的起始地址)到_ebss(.bss 的结束地址)的 RAM 区域全部清零。

  3. 初始化系统时钟(SystemInit)
    调用SystemInit函数(由 STM32 标准库 / LL 库提供),配置 HSE/PLL 等,将系统时钟从默认的 HSI(8MHz)切换到更高频率(如 72MHz)。

  4. 调用main函数
    完成所有初始化后,跳转到 C 语言的main函数,进入用户应用逻辑。

3. 异常 / 中断的触发(补充)

若程序运行中触发中断(如定时器、串口),内核会再次读取「中断向量表」,根据中断号找到对应的中断服务函数(ISR)地址,跳转到 ISR 执行,执行完成后返回断点继续运行。

二、.ld 文件(链接器脚本)的核心作用

.ld文件是链接器(ld)的配置文件,其本质是告诉链接器:

  1. 芯片的内存布局:FLASH(只读、存代码 / 常量)和 RAM(可读写、存变量 / 栈 / 堆)的起始地址大小
  2. 代码 / 数据的段分配规则:将编译生成的各个「段(Section)」(如.text.data)分配到对应的内存区域(FLASH/RAM)。
  3. 定义全局符号:生成_sdata_ebss_estack等符号,供启动文件和用户代码使用(如启动文件依赖这些符号初始化内存)。
  4. 内存合法性检查:确保堆、栈、数据段的总大小不超过 RAM/FLASH 的实际容量,避免内存溢出。 简单说:没有.ld文件,链接器无法知道 “代码该放哪里、变量该放哪里”,无法生成可执行的.elf/.hex文件。

三、解析 STM32F103C8Tx 的.ld 文件

以下按文件结构逐段解析,结合 STM32F103C8Tx 的硬件参数(64K FLASH、20K RAM)说明每部分的作用。

1. 入口点与栈顶定义
1
2
3
4
5
6
7
8
/* Entry Point */
ENTRY(Reset_Handler)  // 定义程序的入口函数:Reset_Handler(与启动文件对应)

/* Highest address of the user mode stack */
_estack = ORIGIN(RAM) + LENGTH(RAM);  // 栈顶地址 = RAM起始地址 + RAM总大小
/* Generate a link error if heap and stack don't fit into RAM */
_Min_Heap_Size = 0x200;      // 最小堆大小(512字节,供malloc使用)
_Min_Stack_Size = 0xF00;     // 最小栈大小(3840字节,供函数调用/局部变量使用)
  • 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 块)
1
2
3
4
5
6
/* Specify the memory areas */
MEMORY
{
RAM (xrw)      : ORIGIN = 0x20000000, LENGTH = 20K  // RAM区域
FLASH (rx)      : ORIGIN = 0x8000000, LENGTH = 64K   // FLASH区域
}

这是.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 块定义的物理内存」,每一个子段都对应特定的代码 / 数据类型。

(1).isr_vector:中断向量表(放 FLASH)
1
2
3
4
5
6
.isr_vector :
{
  . = ALIGN(4);          // 地址4字节对齐(Cortex-M内核要求,否则触发硬 fault)
  KEEP(*(.isr_vector))   // 保留中断向量表,防止链接器优化删除(核心!)
  . = ALIGN(4);
} >FLASH  // 分配到FLASH区域
  • 作用:存放 STM32 的中断向量表(如复位向量、定时器中断向量、串口中断向量等),是复位后内核第一个访问的段。
  • KEEP(*(.isr_vector)):中断向量表由启动文件定义(如startup_stm32f103c8tx.s中的g_pfnVectors数组),KEEP关键字确保链接器不会因为 “看似未被调用” 而删除这个段 —— 如果删除,复位后内核找不到Reset_Handler地址,程序会崩溃。
  • >FLASH:明确将该段分配到MEMORY块定义的FLASH区域(符合硬件要求,向量表默认在 FLASH 起始地址)。
(2).text:代码段(放 FLASH)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
.text :
{
  . = ALIGN(4);
  *(.text)           // 所有.c/.s文件的代码(如main函数、自定义函数)
  *(.text*)          // 扩展代码段(如C++的成员函数)
  *(.glue_7)         // ARM指令与Thumb指令的胶水代码(Cortex-M用Thumb指令,兼容用)
  *(.glue_7t)        // 同上
  *(.eh_frame)       // C++异常处理相关(若用C可忽略)

  KEEP (*(.init))    // 程序初始化函数(如构造函数)
  KEEP (*(.fini))    // 程序结束函数(如析构函数)

  . = ALIGN(4);
  _etext = .;        // 定义符号:.text段的结束地址(供后续段定位)
} >FLASH
  • 作用:存放所有可执行代码(C 函数、汇编函数),是 FLASH 中占用空间最大的段。
  • _etext = ..代表当前地址,_etext是全局符号,标记.text段的结束地址 —— 后续的.rodata段会从_etext之后的地址开始分配,避免段重叠。
(3).rodata:只读常量段(放 FLASH)
1
2
3
4
5
6
7
.rodata :
{
  . = ALIGN(4);
  *(.rodata)         // 只读常量(如const int a = 10;、字符串常量"hello"
  *(.rodata*)        // 扩展只读常量段
  . = ALIGN(4);
} >FLASH
  • 作用:存放只读数据,因为这些数据不需要修改,放在 FLASH 中可节省 RAM 空间。
  • 示例const char str[] = "STM32";会被编译到.rodata段,运行时直接从 FLASH 读取,不会复制到 RAM。
(4).ARM.extab/.ARM:ARM 架构兼容段(放 FLASH)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
.ARM.extab (READONLY) : 
{
  . = ALIGN(4);
  *(.ARM.extab* .gnu.linkonce.armextab.*)
  . = ALIGN(4);
} >FLASH

.ARM (READONLY) : 
{
  . = ALIGN(4);
  __exidx_start = .;
  *(.ARM.exidx*)     // ARM异常索引表(C++异常处理用)
  __exidx_end = .;
  . = ALIGN(4);
} >FLASH
  • 作用:主要用于ARM 架构与 Thumb 架构的兼容,以及 C++ 的异常处理(若项目用纯 C,这些段几乎为空,但保留可避免链接错误)。
  • READONLY:标记为只读,与 FLASH 的权限匹配(GCC11 + 支持,低版本需删除)。
(5).preinit_array/.init_array/.fini_array:初始化 / 结束函数数组(放 FLASH)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
.preinit_array (READONLY) : 
{
  . = ALIGN(4);
  PROVIDE_HIDDEN (__preinit_array_start = .);
  KEEP (*(.preinit_array*))  // 预初始化函数(如动态库预初始化)
  PROVIDE_HIDDEN (__preinit_array_end = .);
  . = ALIGN(4);
} >FLASH

.init_array (READONLY) : 
{
  . = ALIGN(4);
  PROVIDE_HIDDEN (__init_array_start = .);
  KEEP (*(SORT(.init_array.*)))  // 初始化函数(如C++全局对象的构造函数)
  KEEP (*(.init_array*))
  PROVIDE_HIDDEN (__init_array_end = .);
  . = ALIGN(4);
} >FLASH

.fini_array (READONLY) : 
{
  . = ALIGN(4);
  PROVIDE_HIDDEN (__fini_array_start = .);
  KEEP (*(SORT(.fini_array.*)))  // 结束函数(如C++全局对象的析构函数)
  KEEP (*(.fini_array*))
  PROVIDE_HIDDEN (__fini_array_end = .);
  . = ALIGN(4);
} >FLASH
  • 作用:存放程序启动 / 退出时自动执行的函数数组(主要用于 C++),例如:
    • init_arraymain函数执行前,会自动调用这里的函数(如全局对象A a;的构造函数)。
    • fini_arraymain函数退出后,会自动调用这里的函数(如全局对象的析构函数)。
  • PROVIDE_HIDDEN:若用户代码未定义__init_array_start等符号,链接器自动提供(隐藏符号,避免冲突);KEEP确保这些段不被优化删除。
(6).data:初始化数据段(放 RAM,加载地址在 FLASH)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/* used by the startup to initialize data */
_sidata = LOADADDR(.data);  // 定义符号:.data在FLASH的加载地址(LMA)

.data :
{
  . = ALIGN(4);
  _sdata = .;        // 定义符号:.data在RAM的运行地址(VMA)
  *(.data)           // 初始化的全局/静态变量(如int a = 10;)
  *(.data*)          // 扩展初始化数据段
  *(.RamFunc)        // 要放到RAM中执行的函数(如某些快速中断处理函数)
  *(.RamFunc*)       // 同上

  . = ALIGN(4);
  _edata = .;        // 定义符号:.data在RAM的结束地址
} >RAM AT> FLASH  // 运行地址(VMA)在RAM,加载地址(LMA)在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)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
.bss :
{
  _sbss = .;         // 定义符号:.bss在RAM的起始地址
  __bss_start__ = _sbss;
  *(.bss)            // 未初始化的全局/静态变量(如int b;)
  *(.bss*)           // 扩展未初始化数据段
  *(COMMON)          // 未初始化的全局变量(如extern int c;)

  . = ALIGN(4);
  _ebss = .;         // 定义符号:.bss在RAM的结束地址
  __bss_end__ = _ebss;
} >RAM
  • 作用:存放未初始化的全局 / 静态变量,C 语言标准要求这些变量初始化为 0,因此不需要在 FLASH 中存储初始值(节省 FLASH 空间)。
  • 与启动流程关联:启动文件的Reset_Handler会将_sbss_ebss的 RAM 区域清零 —— 这就是 “清零.bss 段” 的具体实现。
  • COMMON:对应未声明但外部引用的全局变量(如extern int c;),链接时会分配到.bss 段。
(8)._user_heap_stack:堆和栈的占位段(放 RAM)
1
2
3
4
5
6
7
8
9
._user_heap_stack :
{
  . = ALIGN(8);      // 8字节对齐(堆/栈通常要求更高对齐,避免内存访问错误)
  PROVIDE ( end = . );    // 定义符号:RAM已分配区域的结束地址
  PROVIDE ( _end = . );
  . = . + _Min_Heap_Size; // 堆区域:从当前地址分配_Min_Heap_Size大小
  . = . + _Min_Stack_Size;// 栈区域:在堆之后分配_Min_Stack_Size大小
  . = ALIGN(8);
} >RAM
  • 作用:为堆和栈预留空间,并检查 RAM 是否足够 —— 若堆 + 栈 + 已分配段(.data+.bss)的总大小超过 RAM 容量,链接器会报错。
  • end/_end:标记 RAM 中 “已分配段(.data+.bss)” 的结束地址,堆从end开始向上生长,栈从_estack(栈顶)开始向下生长。
  • 堆 / 栈生长方向
    • 堆:从end向上(地址增大方向)生长,供malloc/free使用。
    • 栈:从_estack向下(地址减小方向)生长,供函数调用、局部变量使用。
    • 若堆和栈生长时重叠,会导致内存溢出(硬 fault),因此需合理设置_Min_Heap_Size_Min_Stack_Size
(9)/DISCARD/:丢弃无用段(减小可执行文件大小)
1
2
3
4
5
6
/DISCARD/ :
{
  libc.a ( * )    // 丢弃标准C库的所有段
  libm.a ( * )    // 丢弃标准数学库的所有段
  libgcc.a ( * )  // 丢弃GCC编译器库的所有段
}
  • 作用:嵌入式系统通常不需要标准库的完整功能(如printf的文件输出、malloc的复杂内存管理),丢弃这些段可大幅减小.elf/.hex文件的大小。
  • 注意:若项目需要使用标准库功能(如printfsqrt),需删除对应行,否则会出现 “未定义引用” 错误(需搭配newlib-nano等嵌入式精简库)。

四、总结:启动流程与.ld 文件的关联

  1. 硬件复位 → 内核读取FLASH起始地址(0x8000000)的.isr_vector段 → 找到Reset_Handler地址。
  2. 执行Reset_Handler → 利用.ld定义的_sdata/_sidata/_edata复制.data段 → 利用_sbss/_ebss清零.bss段。
  3. 调用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-MCPU内,每一次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),用于存放子函数的返回地址

armbl指令为例(用于函数调用) 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这种通用类型的系统

地址映射图

系统架构图

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,同时触发错误中断处理

程序执行非法指令操作导致系统进入不可恢复的故障状态,系统软件就会介入终止程序执行流,其实现机制是中断

待续

experience
使用 Hugo 构建
主题 StackJimmy 设计