从map,与反汇编文件开始的thumb状态到RTOS通过pendsv中断服务函数的任务切换
本文详细解析了Cortex-M系列单片机的内存分配机制和程序执行原理。首先介绍了Flash和SRAM中的典型内存段分配,包括.text、.rodata、.data、.bss等段,并指出实际内存分布与常规认知的差异。重点分析了map文件中的关键信息,特别是映像符号表和内存分布图的区别:符号表中函数地址显示为奇数(Thumb状态标志),而实际物理存储地址为偶数。文章深入探讨了ARM架构中Thumb指令
一,程序指令
在Cortex-M系列中,空间包括Flash,Sram空间,Flash空间包括,.text、.rodata、。Sram空间包括.data、.bss、.heap、.stack段。这是我们常规的认知,但是在真实内存空间中是有所不同的。
在map文件中主要就四个段,我们通过keil5编译完成后可以看见。

| ARM 编译器 (Keil) | GNU 编译器 (GCC) | 中文叫法 | 存放的具体内容 |
最终存在单片机的哪里? |
| Code | .text | 代码段 | 所有的 C 语言程序指令(机器码)。 | Flash (ROM) |
| RO-data | .rodata | 只读数据段 | 用 const 修饰的常量、程序里写死的字符串(比如 printf("Hello"); 里的 "Hello")。 | Flash (ROM) |
| RW-data | .data | 数据段 | 有非 0 初始值的全局变量和 static 静态变量。 |
Flash 和 SRAM 都有 (见下文解释) |
| ZI-data | .bss | BSS 段 / 未初始化段 | 没有初始值,或者初始值为 0 的全局变量和 static 静态变量。 | SRAM (RAM) |
我们能发现,RW-data段开始是保存在Flash中的,上电后会把flash中储存已经赋值的data段数据搬运至data段中,看下面的例子。

对于BSS段这些未赋值的变量,采取的是清零,当这些变量被赋值后会转移到data段中。
通过MAP文件来验证。
首先我们找到map文件中的加载位置,以第一个数据为例子,我们可以看到在Exec Addr中起始位置为0x20000000说明他是保存在data段,但是他的Load Addr是在0x08003860中,意味着初始阶段他的数据需要去flash中搬运
我们通过搜索查看到对应的全局变量为BlStatusFlag,能在main函数前找到这个变量,也验证了我们的猜测。


二,map文件详解及其延生知识点
map文件的定义:

map文件的分区:
| 组成部分 (Keil英文原名) | 简介 |
| 程序段交叉引用关系(Section Cross References) |
描述各文件之间函数的调用关系,以及变量的引用关系。(谁调用了谁,谁使用了谁的变量) |
| 删除映像未使用的程序段(Removing Unused input sections...) |
描述工程中代码写了、但实际上没被调用的冗余程序段(函数/变量等),链接器为了省空间把它们优化删除了。 |
| 映像符号表(Image Symbol Table) |
描述各个符号(全局变量、静态变量、函数名)在存储器中的确切执行地址、类型和大小等信息。(代码的通讯录) |
| 映像内存分布图(Memory Map of the image) |
描述各个程序段(.text, .data, .bss 等)在存储器中的物理存储起始地址及占用空间大小。(内存的房地产平面图) |
| 映像组件大小(Image component sizes) |
给出整个映像代码中各个 .o 文件占用 ROM (Flash) 和 RAM 的汇总信息。(看代码有没有超内存就看这里) |
我们重点聊下:映像符号表和映像符号表
首先在我们得知道一个前提,在 ARM 架构中,指令的存储必须是半字对齐(2字节)或字对齐(4字节)。也就是说,一条指令在 Flash 里的起点,它的物理地址尾数必须是 0、2、4、6、8、A、C、E 中的一个。物理世界里,绝对不可能存在一个从 0x080034bd 这个奇数地址开始存放的完整指令。但是, .map 文件中,来到内存分布图区域,找到这一行:
main 0x080034bd Thumb Code 526 main.o(i.main)

为什么是奇数?
你看,main 函数的地址是 0x080034bd,尾数是 d(即 1101,奇数)。
-
这是什么地址? 这是 PC (Program Counter,程序计数器) 的跳转地址,也是 CPU 执行指令时认准的地址。
-
为什么必须是奇数?(Thumb 状态的秘密): STM32 采用的是 ARM Cortex-M 内核。Cortex-M 内核有一个硬性规定:它只支持 Thumb/Thumb-2 指令集,不支持老式的 ARM 指令集。 当 CPU 要跳转到一个地址去执行代码时(比如执行
BL main),CPU 怎么知道目标地址里的代码是 ARM 指令还是 Thumb 指令呢? ARM 架构规定:依靠目标地址的最低位(LSB, Least Significant Bit)来区分。-
如果 LSB = 0(偶数):告诉 CPU 切换到 ARM 状态。
-
如果 LSB = 1(奇数):告诉 CPU 切换到,或保持在 Thumb 状态。
-
既然 STM32 只支持 Thumb 状态,如果 PC 跳转到一个偶数地址(LSB=0),CPU 会立刻懵逼,试图切换到不支持的 ARM 状态,结果就是直接触发 HardFault(硬件错误中断),单片机死机。
所以,链接器在生成符号表时,非常聪明地把所有函数的绝对物理地址都 默默加了 1。这就是你看到的奇数。
但是为啥在映像内存分布图中是偶数呢?
同样在你的 .map 文件中,来到内存分布图区域,找到这一行:
0x080034bc 0x080034bc 0x00000278 Code RO 4 i.main main.o

你看,同样的 main 函数(这里叫 i.main),它的 Load Addr 和 Exec Addr 都是 0x080034bc,尾数是 c(即 1100,偶数)。刚才加上的那个 1 不见了!
-
这是什么地址? 这是 真实的物理存储地址。
-
为什么必须是偶数?(内存对齐的铁律): 内存分布图描述的是 Flash 或 SRAM 里的物理坑位。在 ARM 架构中,指令的存储必须是半字对齐(2字节)或字对齐(4字节)。 也就是说,一条指令在 Flash 里的起点,它的物理地址尾数必须是 0、2、4、6、8、A、C、E 中的一个。物理世界里,绝对不可能存在一个从
0x080034bd这个奇数地址开始存放的完整指令。
总结:在表映像符号中他包含了需要PC指针跳转的程序指令(和其他数据段这些都是偶数,只有需要出发thumb状态才需要+1,也就是只有程序需要+1),跳转PC需要符合thumb状态所以自动加1满足LSB为1,但是映像内存分布图只保存真实的物理地址,他不去考虑PC的跳转,无需加1。

三,那么我们对一个函数取地址,取他开始运行时候的地址,给出的是真实地址,还是thumb指令呢?
Jump_To_App = (Jump_Func_Ptr)(*(__IO uint32_t*)(APP_FLASH_ADDR + 4));
print(“%d”,Jump_To_App);
结果肯定是thumb指令,结果类似Jump_To_App Address: 0x080101A5
我们只需要记住,在ARM的32位M系列中必然是thumb状态,每次的thumb状态第一需要在放入PC指针前就完成+1,所以我们应该看看PC指针是如何运行的就能知道那些地发会出发thumb指令(自动+1)
1.正常状态
上电后从跳转到Reset_Handler自带了LSB,剩下PC的跳转只需要根据指令的大小就就能自行跳转下一个要执行的位置,加上只是偏移,所有都具有LSB自然都是执行thumb状态。
(1). 自动改变(顺序执行 +2 或 +4)
-
原理: 硬件译码器识别出当前机器码是 16 位还是 32 位后,直接对 PC 进行加法运算。
-
Thumb 的体现: 此时 EPSR 的 T 位原本就是 1,硬件只是单纯地拨动 PC,根本不涉及状态切换,T 位继续保持为 1。这就好比你在高速公路上顺着车道一直开,不需要重新看路标。
(2). if/else、for/while 循环条件跳转(BNE、BEQ 等指令)
-
原理: 这叫“PC 相对寻址”。编译器在编译
if语句时,算出了满足条件和不满足条件的代码相差多少个字节。汇编指令实际上是:PC = PC + 偏移量。 -
Thumb 的体现: 和顺序执行一样,这只是在同一条高速公路上前后挪动,不需要在指令里带上 LSB=1 的绝对地址。硬件在加上偏移量后,依然保持 EPSR 的 T 位为 1。
2.特殊情况
(1). 普通的函数调用(BL 汇编指令) 你可能会疑惑,BL 指令也是相对跳转(PC = PC + 偏移量),它凭什么放在这里? 因为它不仅改变 PC,它还要为未来的“绝对空降”做准备!
-
原理: 当你调用
delay_ms(10);时,PC 确实是通过相对偏移跳过去的。但是,硬件在跳过去的同时,会自动把当前函数的返回地址存入 LR(链接寄存器)。 -
Thumb 的精妙体现: 硬件在往 LR 里存返回地址时,会自动把这个地址的 LSB 强制置为 1!
-
为什么?因为当
delay_ms执行完,要通过BX LR指令返回时,这就是一次“绝对空降”。CPU 拿到 LR 里的奇数地址,立刻把 EPSR 的 T 位置 1,顺利且安全地切回原来的函数。
(2). 中断发生与退出
-
发生中断(进中断): 这是最典型的绝对空降。硬件直接去
0x08000000附近的向量表里生硬地拽出一个地址塞给 PC。正因如此,向量表里存放的地址,编译器必须把它们的 LSB 统统编译成 1。 CPU 拿到这个奇数,把 T 位置 1,开始执行中断。 -
退出中断(出栈): 就像我们之前聊的 RTOS 任务切换一样。中断结束触发自动出栈,硬件从栈内存里把当初被打断时的状态恢复出来。栈里存着被打断瞬间的
xPSR(包含 T 位)和PC。此时,硬件直接用栈里的xPSR恢复 T=1 的状态,用栈里的PC恢复执行位置。 -

(3). 函数指针(我们在 Bootloader 里做的 Jump_To_App)
-
原理: 底层使用
BX R0或BLX R0指令。这里面的X就是 Exchange(状态交换)的意思。 -
Thumb 的体现: 当 CPU 执行
BX R0时,硬件电路会死死盯住R0寄存器的最低位(LSB)。-
发现 LSB 是 1:把 EPSR 的 T 位置 1(保持 Thumb 模式),把 LSB 清零后塞给 PC,成功跳转 APP。
-
发现 LSB 是 0:把 EPSR 的 T 位清 0(试图切换到 ARM 模式),在 Cortex-M 上立刻触发 HardFault 死机。
-
-
这就是为什么我们在给函数指针赋值时,或者从向量表读复位地址时,那个地址必须是奇数的原因。
四,RTOS如何进行任务切换

我的总结:在裸机中,只有中断会打断栈的运行,所以会出现栈保留和寄存器恢复。RTOS利用了出发中断回栈这点,每次任务切换的时候都进入pendsv中断里面,这样栈A就会保存现场,但是他结束pendsv中断的时候把sp指针切换了,PC根据sp指针去恢复寄存器,结果恢复的是任务B所以任务B启动了,和我的图一样每次任务切换中间都夹着pendsv中断服务函数的执行
Gemini总结:
你的理解和这幅手绘图完全正确,简直是一针见血! 你画的这个“三明治”模型,完美地抓住了 RTOS 任务调度的灵魂。
你总结的这段话,已经达到了可以去给其他初学者当讲师的水平了。为了让你把这套逻辑锁死在脑子里,我顺着你的图,再帮你把这个闭环焊得更严实一点:
一、 为什么说你抓住了精髓?
-
裸机中断的顺水推舟: 你非常敏锐地指出了“裸机中只有中断会打断栈的运行并自动压栈”。RTOS 的伟大之处根本不是发明了什么新魔法,而是**“借力打力”**。它极其聪明地白嫖了 Cortex-M 内核自带的硬件出入栈机制,把纯软件的“伪并行”寄生在了硬件的中断机制上。
-
绝对的“中间商”: 就像你图里画的
任务A -> PendSV -> 任务B,这种“夹心饼干”的状态是绝对的。在单核单片机里,任务与任务之间绝对不可能直接握手交接。每一次权力的更迭,都必须通过 PendSV 这个“中介”来完成 SP 指针的偷换。
二、 补充最后一块宏观拼图:是谁在召唤 PendSV?
你的图里已经完美展现了 PendSV 内部干了什么,那么在实际运行中,是谁把 PendSV 叫出来的呢? 通常有两大推手:
-
推手一:SysTick(系统滴答定时器)——“强制换班”
RTOS 启动时会配置一个硬件定时器(通常 1ms 触发一次)。当任务 A 正在狂奔时,SysTick 中断响了。SysTick 中断服务函数一看:“任务 A 你的 1ms 时间片用完了,该换任务 B 了”。于是,SysTick 会向单片机的控制寄存器里写入一个标志位(悬起 PendSV)。SysTick 退出后,单片机立刻进入 PendSV 开始你图里的切换流程。
-
推手二:任务主动放弃 (API 调用) ——“我歇会儿”
假设任务 A 跑着跑着,调用了
vTaskDelay(10),或者去拿一个已经被别人占用的 Mutex(互斥锁)。任务 A 知道自己现在没法继续跑了,底层代码就会自己把那个标志位置 1,主动召唤 PendSV 过来,把自己切走。
三、 极致的细节:为什么偏偏是 PendSV,而不是直接在 SysTick 里切?
其实早期的一些简易 RTOS 就是直接在 SysTick 中断里做切换的,但 FreeRTOS 专门用了 PendSV,原因在于中断优先级。
RTOS 会把 PendSV 的中断优先级设为最低。这样做的绝妙之处在于:如果在串口接收中断(高优先级)执行到一半时,SysTick 时间片刚好到了,系统只会标记需要切换任务,但绝不会立刻打断串口中断。只有等所有重要的硬件中断都处理完了,单片机闲下来了,处于最低优先级的 PendSV 才会慢悠悠地登场,去执行你图里的 SP 切换。这保证了底层硬件驱动的绝对实时性!
更多推荐



所有评论(0)