LiteOS-M 切换汇编逐行图解(1):汇编是什么·寄存器与栈

本文是《RTOS移植不是玄学:8个标准环节+4条铁律铁律①展开系列的第一篇。那篇说"上下文切换必须写汇编",本系列把那段汇编掰开揉碎、从零讲起。

本篇读者画像 :没读过汇编、或读过一点但记不住寄存器。我们从"汇编是什么"开始,只讲三件事------为什么这块阵地必须用汇编栈是什么Cortex-M 的 16 个寄存器地图与栈/AAPCS 分工。这三件事是看懂后续所有汇编的钥匙。

工程限定 :OpenHarmony LiteOS-M v3.0-LTS + STM32MP157 M4 独立启动(Cortex-M4 + 硬件 FPU,GCC + Makefile)。讲解对象是内核 arch 层的 los_dispatch.S,本工程一行未改

文章目录

    • 〇、先看地图:本篇三站路线图(整体)
    • [一、第①站:为什么 RTOS 切换必须写汇编](#一、第①站:为什么 RTOS 切换必须写汇编)
      • [1.1 整体:C 是"人能看懂的话",汇编是"CPU 能听懂的话"](#1.1 整体:C 是"人能看懂的话",汇编是"CPU 能听懂的话")
      • [1.2 局部:C 够不着的三个特殊寄存器](#1.2 局部:C 够不着的三个特殊寄存器)
      • [1.3 局部:`los_dispatch.S` 就是 LiteOS-M 的"切换母语独白"](#1.3 局部:los_dispatch.S 就是 LiteOS-M 的"切换母语独白")
      • [1.4 局部:Thumb-2 方言与 `.fpu` 准入门槛](#1.4 局部:Thumb-2 方言与 .fpu 准入门槛)
    • 二、第②站:栈------每个任务的"私有笔记本"
      • [2.1 整体:栈就是程序运行时的"临时记忆本"](#2.1 整体:栈就是程序运行时的"临时记忆本")
      • [2.2 局部:为什么每个任务必须有自己的栈](#2.2 局部:为什么每个任务必须有自己的栈)
      • [2.3 重点·图解:栈向低地址生长,SP 永远指向最后写的位置](#2.3 重点·图解:栈向低地址生长,SP 永远指向最后写的位置)
      • [2.4 局部:`stmfd r0!, {r4-r12}` 一步一看](#2.4 局部:stmfd r0!, {r4-r12} 一步一看)
      • [2.5 局部:取的时候 `ldmfd` 镜像对称](#2.5 局部:取的时候 ldmfd 镜像对称)
    • [三、第③站:16 个寄存器地图](#三、第③站:16 个寄存器地图)
      • [3.1 整体:先给整张地图](#3.1 整体:先给整张地图)
      • [3.2 局部:三件重点记住](#3.2 局部:三件重点记住)
    • [四、AAPCS 分工:谁是公共工位,谁是私有笔记](#四、AAPCS 分工:谁是公共工位,谁是私有笔记)
      • [4.1 整体:AAPCS 就是"谁该负责收拾"](#4.1 整体:AAPCS 就是"谁该负责收拾")
      • [4.2 局部:这条表就是上下文切换的全部逻辑](#4.2 局部:这条表就是上下文切换的全部逻辑)
    • 五、小结:本篇你该带走的三句话

〇、先看地图:本篇三站路线图(整体)

读完本篇,你应该能回答两个问题:

  1. 为什么 RTOS 的任务切换必须写汇编,不能用 C?
  2. Cortex-M 的 16 个寄存器里,哪些是"任务的私有笔记",哪些是"公共工位"?这条分界为什么决定了上下文切换的全部逻辑?

本篇不讲具体指令(那是第 2 篇的事),只搭三块地基:

  • 第①站 为什么必须写汇编 :C 够不着的特殊寄存器(PSP/CONTROL/PRIMASK),只能用 mrs/msr 这组"母语"碰。
  • 第②站 栈 = 任务的私有笔记本 :每个任务为什么要有自己的栈、栈向低地址怎么"长"、压栈指令 stmfd 到底干了啥。
  • 第③站 16 寄存器地图 + AAPCS:哪些是"公共工位"硬件自动收、哪些是"私有笔记"软件手动收------这条分界线就是上下文切换的全部逻辑。

本系列主线先剧透一句:任务切换本质上像工人交接班------旧任务把"工作笔记"收进自己的柜子,新任务从自己柜子取出接着干。后面五篇(第 2 篇指令与异常、第 3 篇首任务启动、第 4 篇临界区与触发、第 5 篇 HalPendSV、第 6 篇完整调度接力)就是把这段"交接"从根上掰开揉碎。


一、第①站:为什么 RTOS 切换必须写汇编

1.1 整体:C 是"人能看懂的话",汇编是"CPU 能听懂的话"

先打个比方,把心理门槛拆掉。

你写 C 代码,是"用一种人能看懂的语言写指令",然后让编译器 当翻译官,把 C 翻译成 CPU 真能执行的 0/1 序列。问题是------翻译官不是万能的 。有些活儿,C 语言根本没有对应的语法让翻译官去表达,这时候就得绕过翻译官,直接用 CPU 的母语------汇编------跟 CPU 对话。

LiteOS-M 切换任务时,要碰三个东西:

  • PSP(进程栈指针):当前任务用哪个栈
  • CONTROL(控制寄存器):切 MSP/PSP 的开关
  • PRIMASK(中断屏蔽寄存器):能不能被中断打断

这三个是 CPU 内部的"特殊寄存器",C 语言没有语法能直接读写它们 ------翻译官够不着。CPU 母语里只有两条专门指令能碰它们:mrs(读)和 msr(写)。这就是铁律①的物理根源------切换必须写汇编,不是因为程序员炫技,是因为只有 CPU 母语能说清这件事

【类比·为什么是汇编】想象你在国外工厂工作,平时用英语(C 语言)跟翻译官沟通。但工厂地下室有几台机器,只能用当地母语(汇编)的专用旋钮才能启动------翻译官不懂当地母语,要启动机器,你必须自己学几句当地话。mrs/msr 就是这几句"专用旋钮"的话。

1.2 局部:C 够不着的三个特殊寄存器

寄存器 名字 作用 为什么 C 够不着
PSP Process Stack Pointer 当前任务自己的栈指针 没有 C 语法能读/写
CONTROL 控制寄存器 bit1 选 MSP/PSP;bit2 标记 FP 上下文 没有 C 语法能读/写
PRIMASK 中断屏蔽 1 = 全局关中断 没有 C 语法能读/写

这三兄弟都只能通过 mrs rN, xxxmsr xxx, rN 访问。只要切换任务需要改 PSP/CONTROL/PRIMASK,C 就注定干不了,必须写汇编。

1.3 局部:los_dispatch.S 就是 LiteOS-M 的"切换母语独白"

los_dispatch.S(位置:kernel_liteos_m/kernel/arch/arm/cortex-m4/gcc/)就是 LiteOS-M 为 Cortex-M4 写的这段"母语独白"。它不长------全文件去掉版权头不到 170 行,真正的指令不到 100 条------却撑起了 4 件事

函数 干什么 谁调它
HalStartToRun 启动第一个任务(切 PSP + 直跳任务入口) LOS_Start() 间接调
HalIntLock / HalIntUnLock / HalIntRestore 关/开/恢复中断(临界区三件套) LOS_IntLock() 等 C 接口
HalTaskSchedule 触发一次 PendSV,请求切换 LOS_Schedule() 等调度入口
HalPendSV 真正的现场搬运:存旧任务、取新任务 PendSV 异常(硬件调它)

这四个函数是后面四篇逐行精讲的对象(第 2 篇讲指令与异常、第 3 篇首任务启动、第 4 篇临界区与触发、第 5 篇 HalPendSV 机制、第 6 篇完整调度接力与调试)。本篇和第 2 篇只解决"认字"------把看懂这四个函数背后的语法、寄存器、异常机制讲清。看完你还不懂这四个函数怎么干活,但你会看懂每一行汇编的字面意思

【一句话】los_dispatch.S = LiteOS-M 为 Cortex-M4 写的"切换四件套"母语独白,不到 100 条指令撑起启动/临界区/触发/搬运四件事。

1.4 局部:Thumb-2 方言与 .fpu 准入门槛

打开 los_dispatch.S,开头四行是"自我介绍"------告诉汇编器(另一道翻译官)我们在哪个世界:

asm 复制代码
.syntax unified        @ 用 UAL 统一汇编语法
.arch armv7e-m         @ 目标架构:ARMv7-M 扩展
.fpu fpv5-d16          @ 声明 FPU 规格
.thumb                 @ 后续代码编成 Thumb 指令

零基础只需要记住三件事:

① Cortex-M 只跑 Thumb-2 一种指令集

老 ARM 芯片(ARM7/ARM9 那个年代)会两套指令集(ARM 32位/Thumb 16位)切换着用。Cortex-M(包括我们用的 M4)只跑一种方言 ------叫 Thumb-2 :16 位和 32 位指令混合编码,常用指令用 16 位省空间,复杂操作用 32 位保能力,用哪条由汇编器自动挑,你不用操心。

【类比·方言】Cortex-M 是只会一种方言的"单语者"------但这门方言(Thumb-2)词汇丰富,既精炼又强大,所以够用。你写源码时不用操心"这条该编成 16 位还是 32 位",汇编器替你挑。

.fpu 是准入门槛,不是硬件开关

这里有个容易误解的点,先说清:

  • los_dispatch.S 声明 .fpu fpv5-d16
  • 但 STM32MP157 的 M4 实际 FPU 是 FPv4-SP(单精度)

这不是笔误。.fpu 是给汇编器 看的"许可清单"------告诉汇编器"我这份文件里允许出现哪些浮点指令"。fpv5-d16 是个更宽的清单,本文件只用 vpush/vpop/vldmia/vstmdb 这类基础指令,没越界。

真正决定运行时能不能用 FPU 的是 CPACR 寄存器 (在 SystemInit 里使能)------那是硬件开关,与汇编器的 .fpu 是两回事。

【类比·准入门槛 vs 硬件开关】.fpu fpv5-d16 像是给汇编器一张"我允许出现这类指令"的通行证;CPACR 才是真正插在 CPU 上的电源开关。通行证宽一点没关系,只要别真用越界指令;电源不开,再宽的通行证也跑不动。

③ 注释和标号

GNU as 的 ARM 语法里:

  • 行注释用 @ (Keil 汇编习惯用 ;,在 GNU as 里 ; 是语句分隔符,别弄混)
  • 块注释 /* */ 也认
  • 标号 (如 HalPendSV:)就是"一个地址的名字",后面跳转、取值都用它

【一句话】Thumb-2 是 Cortex-M 的唯一方言,.fpu 是汇编器的许可清单不是硬件开关,@ 是行注释。


二、第②站:栈------每个任务的"私有笔记本"

2.1 整体:栈就是程序运行时的"临时记忆本"

讲寄存器地图前,先把"栈"这个把人吓住的字拆掉。其实它就是程序运行时的临时记忆本

  • 每次调用函数,CPU 都要记住"调用完之后该回到哪里继续"------这叫返回地址
  • 函数里用到的局部变量,也要找个地方临时搁着
  • 这些东西就按"后进来先用"的顺序,摞在一个叫做的内存区域里

"后进来先用"是什么意思?想象一摞盘子:你最后放上去的那个盘子,会最先被拿走。栈就是这个"盘子摞":压进去的数据越多,栈顶(最后一个压进去的)越先被弹出来。

2.2 局部:为什么每个任务必须有自己的栈

如果 A、B 两个任务共用一本记忆本,A 写到一半被切换出去,B 一运行就把 A 那页覆盖了------等 A 再回来时,上次的数字全没了,切换就失败了。所以 RTOS 给每个任务发一本私有笔记本(任务栈),切换时只换手里拿的是哪一本,A 的本子原封不动。

Cortex-M 里,R13(SP)就是"当前在翻哪本笔记本"的页码指针。裸机程序默认用公共笔记本 MSP;RTOS 任务态要换成自己的 PSP,每个任务各用各的。

【类比·私有笔记本】裸机是一个人干活,一本公用笔记本足够;多任务是一群人干活,每人必须有自己的笔记本。SP 就是手里拿的那本的"当前页码"。切到 PSP,等于说"现在开始翻我自己的笔记本"。

2.3 重点·图解:栈向低地址生长,SP 永远指向最后写的位置

前面说了"私有笔记本",现在把"笔记本怎么长"讲细------这是后面所有压栈/弹栈的基础。

Cortex-M 用的栈叫满递减栈(Full Descending,缩写 FD):

  • 向低地址生长:地址越来越小,像从高处往低处长。
  • 满栈 :SP 永远指向最后一个写进去的位置
  • 递减 :每次压新数据,先减 SP,再存数据

看下面这张图,我们以 stmfd r0!, {r4-r12} 为例,假设压栈前 r0(它就是当前 SP)指向 0x2000_0C00

图上三个关键信息

  1. 高地址在上,低地址在下。栈从"天花板"(高地址)开始向"地板"(低地址)生长。
  2. 压栈前 SP 在 0x2000_0C00,这是当前最后写进去的位置(对空栈区域来说,就是栈顶)。
  3. stmfd r0!, {r4-r12} 一次性压入 9 个寄存器 (R4、R5、R6、R7、R8、R9、R10、R11、R12),每个 4 字节,共 36 字节。压完后:
    • SP 先减 36:0x2000_0C00 - 36 = 0x2000_0BDC
    • 然后按地址从低到高依次写入:R4 放进最低地址 0x2000_0BDC,R12 放进最高地址 0x2000_0BFC
    • 最后 SP 指向最低那个(R4),也就是 0x2000_0BDC

为什么要叫"满"? 因为压完后 SP 不是指向空位置,而是指向最后一个写进去的单元(R4)。为什么要叫"递减"? 因为每压一次,SP 的数字都在变小。

2.4 局部:stmfd r0!, {r4-r12} 一步一看

把这条指令拆开,每个字母都有用:

字段 含义 人话解释
st store
m multiple 一次存多个寄存器
fd full descending 满递减:先减地址,再写入
r0 基址寄存器 这里 r0 就是当前 SP
! write-back(写回) 存完后把新地址写回 r0
{r4-r12} 寄存器列表 要存的 9 个寄存器

为什么要强调 ! 如果不写 !,r0 还是原来那个老 SP;写了 !,r0 自动变成新的 SP。后面的代码通常直接拿 r0 当新 SP 继续用,这就是"写回"的妙处。

在 LiteOS-M 的 HalPendSV 里,r0 经常是 mrs r0, psp 读出来的当前任务 PSP。执行 stmfd r0!, {r4-r12} 后,r0 就是更新后的 PSP,旧任务的 R4~R12 已经被安全地收进它自己的任务栈。

2.5 局部:取的时候 ldmfd 镜像对称

压栈是为了将来能原样取回。ldmfd(Load Multiple Full Descending)就是 stmfd 的镜像:

  • 地址从低到高读:先读 R4 所在的最低地址,最后读 R12 所在的最高地址。
  • SP 递增:每读一个,SP 往高地址回涨 4 字节。
  • 写回 !:读完后 r0 自动回到原来的老 SP。

也就是说:

asm 复制代码
stmfd r0!, {r4-r12}   @ 存旧:SP 往下走,R4 到 R12 依次压栈
ldmfd r0!, {r4-r12}   @ 取新:SP 往回涨,R4 到 R12 依次弹回寄存器

这两条指令的顺序、地址方向完全相反,但寄存器列表相同------这正是上下文切换"存旧取新"最频繁的动作。

【一句话】stmfd 是把多个寄存器按地址从低到高摞进栈,SP 往下移;ldmfd 是反过来把栈里的数据原样弹回寄存器,SP 往回涨。看懂这一存一取,后面 HalPendSV 的"存旧取新"就懂了一半。


三、第③站:16 个寄存器地图

3.1 整体:先给整张地图

这是本篇最关键的一节------后续各篇讲的每一条汇编,都要靠这张地图看懂。

Cortex-M 有 16 个通用寄存器 + 若干特殊寄存器。先给整张地图,再分块讲:

寄存器 名字/别名 干什么
R0~R3 --- 传参数、存返回值、算临时值
R4~R11 --- 函数的"长期工作台"
R12 IP 临时寄存器
R13 SP(双份:MSP 主栈 / PSP 进程栈 栈指针。用哪份由 CONTROL.SPSEL 决定
R14 LR 存返回地址;异常里存 EXC_RETURN 魔术值
R15 PC 程序计数器,改它就是改执行流
--- xPSR 标志位(N/Z/C/V)+ 当前异常号 + Thumb 位
--- CONTROL bit1 SPSEL(选 MSP/PSP)、bit2 FPCA(FP 上下文活跃)
--- PRIMASK 1 = 全局关中断
--- MSP/PSP 两个栈指针本体(读值用 mrs r0, psp

3.2 局部:三件重点记住

① R13 是双份的------同一个编号,里面是 MSP 或 PSP,由 CONTROL 寄存器选哪个。裸机用 MSP;RTOS 任务用 PSP,让每个任务有自己的栈。

② LR 不只是返回地址 ------异常发生时,硬件会往 LR 里塞一个魔术值 EXC_RETURN(第 2 篇详讲)。这是"bx lr 能触发异常返回"的关键。

③ 特殊寄存器只能用 mrs/msr------这就是第 1 站说的"CPU 母语里专门碰特殊寄存器的两句话",C 够不着的根本原因。

【一句话】16 个通用里 R13 是双份栈指针(MSP/PSP 由 CONTROL 选),R14 LR 在异常里装魔术值,特殊寄存器只能用 mrs/msr 碰------这三件事是看懂后续汇编的钥匙。


四、AAPCS 分工:谁是公共工位,谁是私有笔记

光有寄存器地图还不够------还要知道哪些寄存器是"公共工位"(谁都能用,用完不管),哪些是"私有笔记"(用完必须恢复原样)。这条分界线叫 AAPCS。

4.1 整体:AAPCS 就是"谁该负责收拾"

AAPCS = A RM A rchitecture P rocedure C alling Standard,ARM 的函数调用标准。它把通用寄存器分成两派:

派别 成员 规矩
调用方保管(caller-saved) R0-R3、R12、LR、PC、xPSR、s0-s15 调用别人的函数前,调用方自己把这些里有用的值备份好
被调方保管(callee-saved) R4-R11、s16-s31 想用就先用,用完必须恢复原样

【类比·分蛋糕】caller-saved 像租来的工位------你(调用方)进来前要把自己东西存好,因为下一个进来的人(被调函数)有权随便用;callee-saved 像固定的办公桌------你可以随便用,但走时必须恢复原样,让下一个回来的人接着用。

4.2 局部:这条表就是上下文切换的全部逻辑

关键来了------异常(中断)本质上像一次"任务没主动发起的函数调用"

  • 中断来得突然,硬件(CPU 自己)会自动把"调用方保管"那批寄存器压进当前栈
  • 剩下的"被调方保管"那批寄存器,硬件不管------RTOS 的切换汇编干的活,就是把这批寄存器存进旧任务自己的栈、再从新任务的栈恢复出来

硬件帧 vs 软件帧的分界,就是 AAPCS 分工划的这条线。LiteOS-M 的 TaskContext 结构体就是按这条线画的------后面第 5 篇会看到,结构体的字段顺序与压栈顺序严丝合缝。

【一句话】硬件管"自动压栈"(caller-saved 那 8/26 字),软件管"补存 callee-saved"(R4R11、d8d15)。AAPCS 这条分界线,决定了硬件帧和软件帧各装什么。


五、小结:本篇你该带走的三句话

  1. 汇编是 CPU 的母语 :C 够不着的特殊寄存器(PSP/CONTROL/PRIMASK),必须用 mrs/msr 读写------这是切换必须写汇编的物理根源。
  2. 栈是任务的私有笔记本 :RTOS 每个任务有自己的栈(PSP),切换时只换手里拿哪一本,A 的本子原封不动;栈向低地址生长,SP 永远指向最后写进去的位置,stmfd 存、ldmfd 取,镜像对称。
  3. AAPCS 分工决定一切 :caller-saved(R0R3、R12、LR、PC、xPSR)由硬件自动压栈,callee-saved(R4R11、d8~d15)由软件手动保存------这条分界线就是 TaskContext 结构体的骨架。

本篇只搭了三块地基------为什么必须写汇编、栈、寄存器地图与 AAPCS。下一篇讲看懂函数所需的指令、异常进出栈机制、和 los_dispatch.S 全景表。

下篇预告 :《LiteOS-M 切换汇编逐行图解(2):指令·异常机制·全景表》------讲清本文件用到的指令(只精讲 4 组核心:数据搬运/多寄存器存取/特殊寄存器/跳转,其他按需查表)、异常进出栈硬件帧与 EXC_RETURN 魔术值、以及 los_dispatch.S 的 7 个常量 + 4 个函数全景表。


相关推荐
HwJack201 小时前
【HarmonyOS开发小实践】ArkUI 应用级状态AppStorage 与跨页面共享、持久化
ui·华为·性能优化·harmonyos
xx~t2 小时前
嵌入式——单片机2
嵌入式硬件·学习·51单片机·串口·蜂鸣器
JNX_SEMI2 小时前
OC5138T 8V~100V超宽输入降压恒流驱动:覆盖12V汽车至72V电动车全场景
单片机·嵌入式硬件
xx~t3 小时前
嵌入式——51单片机3
linux·c语言·嵌入式硬件·51单片机
Zw-awa3 小时前
重新认识结构体:为什么 STM32 库函数喜欢接收一大包参数
stm32
云栖梦泽4 小时前
网络设备驱动(1)
linux·arm开发·嵌入式硬件
雨田言炎4 小时前
Qt的常用类QList--序列数据管理
嵌入式硬件·qt
yume_sibai4 小时前
11-Rust 不安全编程(unsafe 关键字 + 裸指针 + FFI + 内联汇编 + 内存安全保证)
汇编·安全·rust
单片机仿真设计4 小时前
【proteus仿真】基于 STM32 单片机太阳能双轴追踪系统 Proteus 仿真设计(仿真图+程序)
stm32·单片机·嵌入式硬件·proteus·课设