核心:无论是执行第一个新任务的伪造现场,还是旧任务切换到新任务时的切换现场,核心都是在异常处理时设置PSP为新任务的栈,然后从这个栈中弹出值到寄存器中恢复新任务的现场,只不过执行第一个新任务时栈中的值是伪造的切换到新任务时栈中的值是在PendSV异常中保存的。

1. 通过 SVC 异常执行第一个任务

1.1 源码

// 57. 初始化任务栈函数定义,头文件 64
// 在创建任务时,手动模拟一个中断返回时的栈帧结构,以便任务第一次被调度运行时,能像从中断返回一样正确地跳转到任务入口函数
StackType_t * port_pu32InitStack( StackType_t * pxTopOfStack,
                                     TaskFunc_t pxCode,
                                     void * pvParameters ){
    pxTopOfStack--;
    *pxTopOfStack = portINITIAL_XPSR;                                    /* xPSR */
    pxTopOfStack--;
    *pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; /* PC */
    pxTopOfStack--;
    *pxTopOfStack = ( StackType_t ) portTASK_RETURN_ADDRESS;             /* LR */
    pxTopOfStack -= 5;                            /* R12, R3, R2 and R1. */
    *pxTopOfStack = ( StackType_t ) pvParameters; /* R0 */
    pxTopOfStack--;
    *pxTopOfStack = portINITIAL_EXC_RETURN;
    pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4. */
    return pxTopOfStack;
}

// 59. SVC异常处理函数定义,把系统从“启动模式”切换到“任务运行模式”,让第一个任务开始干活
// msr psp, r0 把栈指针切换到任务专用的栈
void port_vSVCHandler( void )
{
    __asm volatile (
        "   ldr r3, pxCurTCBConst2          \n" 
        "   ldr r1, [r3]                    \n" 
        "   ldr r0, [r1]                    \n" 
        "   ldmia r0!, {r4-r11, r14}        \n" 
        "   msr psp, r0                     \n" 
        "   isb                             \n"
        "   mov r0, #0                      \n"
        "   msr basepri, r0                 \n"
        "   bx r14                          \n"
        "                                   \n"
        "   .align 4                        \n"
        "pxCurTCBConst2: .word g_pxCurTCB  \n"
        );
}

// 60. 启动第一个任务函数定义,完成最后的系统初始化,然后按下 SVC 0 按钮,触发之前的 port_vSVCHandler,让第一个任务正式开始运行
static void port_prv_vStartFirstTask( void )
{
    __asm volatile (
        " ldr r0, =0xE000ED08   \n" 
        " ldr r0, [r0]          \n"
        " ldr r0, [r0]          \n"
        " msr msp, r0           \n" 
        " mov r0, #0            \n" 
        " msr control, r0       \n"
        " cpsie i               \n" 
        " cpsie f               \n"
        " dsb                   \n"
        " isb                   \n"
        " svc 0                 \n" 
        " nop                   \n"
        " .ltorg                \n"
        );
}
FLASH (0x08000000 – 0x0801FFFF, 128KB)
┌──────────────────────────────────────┐
│ .isr_vector  (中断向量表)             │ ← 0x08000000
├──────────────────────────────────────┤
│ .text        (代码)                  │
├──────────────────────────────────────┤
│ .rodata      (只读数据)               │
├──────────────────────────────────────┤← _sidata
│ .data 副本   (初始化数据镜像)          │ 
└──────────────────────────────────────┘
 
 
RAM (0x20000000 – 0x20007FFF)
┌──────────────────────────────┐
│ .data (运行时初始化数据)      │ ← 0x20000000 (_sdata)
├──────────────────────────────┤← _sbss
│ .bss (未初始化数据)           │ 
├──────────────────────────────┤← _ebss
│ 未使用区域(安全缓冲)         │
├──────────────────────────────┤
│ 栈 (Stack) 向下/低地址增长    |    
└──────────────────────────────┘ ← 0x20008000 (_estack)

注意:压入栈时向低地址增长,但是异常返回时从低地址向高地址读取数据!

1.2 流程

第一个任务开始执行时:

上电
  ↓
CPU 读 0x0 / 0x4 → 设置 SP 和 PC
  ↓
执行 Reset_Handler → 调用 main()
  ↓
main() 中创建任务 → 分配栈 + 调用 port_pu32InitStack 伪造栈帧
  ↓
启动调度器
  ↓
触发 svc 异常,硬件自动执行:压入现场(当前8个寄存器的值)到MSP栈中,并设置r14(控制异常从PSP返回而不是MSP)
  ↓
svc 异常处理:设置栈指针 PSP 为新任务的栈,
  ↓
异常返回到 PSP:自动从 PSP 指向的新任务的栈顶依次弹出8个值并加载到寄存器(加载新任务的现场)
  ↓
加载新任务伪造的现场的值到PC:程序从PC开始执行,即新任务的任务函数
  ↓
新任务开始执行!

当任务被创建时,它从未运行过,那么异常返回就需要返回到这个从未运行过的任务

Cortex-M 处理器在进入异常时,会自动将 8 个寄存器的值压入当前栈,自动保存当前现场,然后异常返回时

如果不伪造,则异常返回加载8个值时,就与异常返回应该获取的8个寄存器值对应不上,导致 HardFault.

1.3 预处理:伪造栈帧 port_pu32InitStack

// 57. 初始化任务栈函数定义,头文件 64
// 在创建任务时,手动模拟一个中断返回时的栈帧结构,以便任务第一次被调度运行时,能像从中断返回一样正确地跳转到任务入口函数
StackType_t * port_pu32InitStack( StackType_t * pxTopOfStack,
                                     TaskFunc_t pxCode,
                                     void * pvParameters ){
    pxTopOfStack--;
    *pxTopOfStack = portINITIAL_XPSR;                                    /* xPSR */
    pxTopOfStack--;
    *pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; /* PC */
    pxTopOfStack--;
    *pxTopOfStack = ( StackType_t ) portTASK_RETURN_ADDRESS;             /* LR */
    pxTopOfStack -= 5;                            /* R12, R3, R2 and R1. */
    *pxTopOfStack = ( StackType_t ) pvParameters; /* R0 */
    pxTopOfStack--;
    *pxTopOfStack = portINITIAL_EXC_RETURN;
    pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4. */
    return pxTopOfStack;
}

步骤

代码 

作用详解

寄存器与说明

1

pxTopOfStack--;

栈顶指针下移 1(4 字节),为压入 xPSR 腾出空间。

栈是向下增长的(满递减栈)。初始 pxTopOfStack 指向分配内存的最高地址+1,需先减再写。

2

*pxTopOfStack = portINITIAL_XPSR;

写入初始 xPSR(程序状态寄存器)

将 xPSR 写入当前地址

3

pxTopOfStack--;

指针下移,准备写入 PC(任务入口地址)。

4

*pxTopOfStack = ((StackType_t) pxCode) & portSTART_ADDRESS_MASK;

写入任务函数地址

异常恢复后会把这个值(任务函数的地址)加载到 PC

5

pxTopOfStack--;

指针下移,准备写入 LR

6

*pxTopOfStack = (StackType_t) portTASK_RETURN_ADDRESS;

写入“伪返回地址”。
通常为一个无效地址,因为任务永不返回。

异常恢复后会把这个值加载到 LR,若任务意外返回,会跳转到此地址 → 触发 HardFault,便于调试。

7

pxTopOfStack -= 5;

一次性下移 5 个单位(20 字节),跳过 R12, R3, R2, R1, R0 的位置(稍后单独初始化 R0)。

R12, R3, R2, R1 这些寄存器在 AAPCS 中属于 caller-saved,任务启动时可为任意值

8

*pxTopOfStack = (StackType_t) pvParameters;

R0 位置写入任务参数 pvParameters

符合 AAPCS:函数第一个参数通过 R0 传递任务函数将收到此参数。

9

pxTopOfStack--;

*pxTopOfStack = portINITIAL_EXC_RETURN;

指针下移,写入 EXC_RETURN 值

10

pxTopOfStack -= 8;

下移 8 个单位(32 字节),跳过 R4–R11 的位置,R4在高地址

这些是 callee-saved 寄存器,任务启动时设为 0(未显式写入,但栈内存通常已清零)。

11

return pxTopOfStack;

返回更新后的栈顶指针

供调度器恢复上下文时使用。

完整构造了一个符合异常返回的任务栈帧,这样后续异常返回到重新设置的 PSP 后读取这里的栈帧会认为正确返回了

1.4 第一步:触发svc异常 port_prv_vStartFirstTask

// 60. 启动第一个任务函数定义,完成最后的系统初始化,然后按下 SVC 0 按钮,触发之前的 port_vSVCHandler,让第一个任务正式开始运行
static void port_prv_vStartFirstTask( void )
{
    __asm volatile (
        " ldr r0, =0xE000ED08   \n"
        " ldr r0, [r0]          \n"
        " ldr r0, [r0]          \n"
        " msr msp, r0           \n" 
        " mov r0, #0            \n" 
        " msr control, r0       \n"
        " cpsie i               \n" 
        " cpsie f               \n"
        " dsb                   \n"
        " isb                   \n"
        " svc 0                 \n"
        " nop                   \n"
        " .ltorg                \n"
        );
}

步骤

指令

操作说明

0

函数调用前

port_lStartScheduler() 调用此函数,进入启动第一个任务的准备阶段

1

ldr r0, =0xE000ED08 获取向量表基地址寄存器的地址

2

ldr r0, [r0]

从寄存器中获取中断向量表在内存中的实际位置

3

ldr r0, [r0]

读取向量表第 0 项 _estack

4

msr msp, r0

将 r0 的值即 _estack 写入到 msp

5

mov r0, #0
msr control, r0

CONTROL 寄存器清零,强制系统进入“特权级 + 使用主栈指针(MSP)”的状态,并禁用浮点单元(FPU)相关特性

6

cpsie i

使能全局中断

7

cpsie f

使能浮点异常(若支持)

8

dsb

数据同步屏障,等待所有读写操作完成

9

isb

指令同步屏障,清空处理器流水线,并丢弃预取的指令,确保后续指令从内存中重新获取

10

svc 0

触发 SVC 异常

11

nop

占位符:防止编译器优化,实际永不执行

12

SVC 处理开始

硬件跳转到 port_vSVCHandler

  1. 恢复主栈:从向量表读取 MSP 初始值,确保中断有合法栈空间
  2. 强制特权:清零 CONTROL 寄存器,保证后续操作在特权模式下执行
  3. 触发异常:svc 0 是唯一能安全启动任务的途径,利用硬件异常机制完成上下文切换

1.5 第二步:在异常中启动第一个任务 port_vSVCHandler

// 59. SVC异常处理函数定义,把系统从“启动模式”切换到“任务运行模式”,让第一个任务开始干活
// msr psp, r0 把栈指针切换到任务专用的栈
void port_vSVCHandler( void )
{
    __asm volatile (
        "   ldr r3, pxCurTCBConst2          \n" 
        "   ldr r1, [r3]                    \n" 
        "   ldr r0, [r1]                    \n" 
        "   ldmia r0!, {r4-r11, r14}        \n" 
        "   msr psp, r0                     \n" 
        "   isb                             \n"
        "   mov r0, #0                      \n"
        "   msr basepri, r0                 \n"
        "   bx r14                          \n"
        "                                   \n"
        "   .align 4                        \n"
        "pxCurTCBConst2: .word g_pxCurTCB  \n"
        );
}

步骤

指令

作用详解

涉及寄存器

1

ldr r3, pxCurTCBConst2

将符号 pxCurTCBConst2 的值即 g_pxCurTCB 的地址加载到 r3。

r3:临时指针寄存器,用于存放 g_pxCurTCB 的地址。
pxCurTCBConst2 :汇编标签,定义在下方,值为 g_pxCurTCB 的地址。

2

ldr r1, [r3]

从 r3 指向的内存(即 &g_pxCurTCB)中读取值 → 得到当前任务 TCB 的指针值,存入 r1。

r1:保存 &g_pxCurTCB

3

ldr r0, [r1]

从 TCB 结构体首字段(即 pxTopOfStack)读取任务的栈顶指针(已初始化好的上下文栈),存入 r0。

r0:保存任务专用栈的栈顶地址 pxTopOfStack

4

ldmia r0!, {r4-r11, r14}

从任务栈中弹出寄存器
- 加载 r4r11(Callee-saved 寄存器)
- 同时加载 r14(即 LR)
- ! 表示自动更新 r0

r4–r11:官方规定的被调用者保存寄存器,任务上下文的一部分。

ldmia从 r0 开始,连续加载多个寄存器,加载完后 r0 自动增加(指向下一个未加载的位置),加载完成后,r0 会被更新为 r0 + 总字节数

5

msr psp, r0

将更新后的 r0(即任务栈指针)写入 PSP 寄存器

PSP:进程栈指针,任务运行时使用的栈。即 PSP 被设为异常返回栈帧起始地址。

6

isb

指令同步屏障

7

mov r0, #0

将立即数 0 加载到 r0,为清除 BASEPRI 做准备。

8

msr basepri, r0

清除 BASEPRI 寄存器 → 允许所有优先级中断

BASEPRI:可屏蔽中断的阈值寄存器。写 0 表示“不屏蔽任何中断”

9

bx r14

异常返回:跳转到 r14(即 EXC_RETURN 值)。
由于 SVC 是异常,进入时硬件自动设置 LR = 0xFFFF_FFFD(表示:返回线程模式 + 使用 PSP + 返回 Thumb 状态)。

r14 (LR):在异常处理中,LR 被硬件自动设为 EXC_RETURN 码
BX LR 触发异常返回硬件自动从 PSP 弹出 xPSR/PC/LR/R12/R3-R0,并切换到线程模式

10

.align 4

确保 pxCurTCBConst2 地址按 4 字节对齐

11

pxCurTCBConst2: .word g_pxCurTCB

定义一个 32 位字,其值为全局变量 g_pxCurTCB 的地址(链接时确定)。

供第 1 行 LDR 使用

  1. 硬件压栈:svc #0 触发后,硬件自动保存现场 (8 个寄存器的值)到 MSP 栈
  2. 软件恢复:从任务栈恢复 R4-R11 + EXC_RETURN (0xFFFFFFFD) 到 r14
  3. 硬件弹栈:bx r14 触发异常返回,硬件自动从 PSP 弹出 8 个寄存器并切换模式/栈

2. 通过 PendSV 异常切换任务

2.1 源码

// 65. PendSV异常处理函数定义
void port_vPendSVHandler( void )
{
    __asm volatile
    (
        "   mrs r0, psp                         \n"
        "   isb                                 \n"
        "                                       \n"
        "   ldr r3, pxCurTCBConst               \n" 
        "   ldr r2, [r3]                        \n"
        "                                       \n"
        "   tst r14, #0x10                      \n" 
        "   it eq                               \n"
        "   vstmdbeq r0!, {s16-s31}             \n"
        "                                       \n"
        "   stmdb r0!, {r4-r11, r14}            \n" 
        "   str r0, [r2]                        \n" 
        "                                       \n"
        "   stmdb sp!, {r0, r3}                 \n"
        "   mov r0, %0                          \n"
        "   msr basepri, r0                     \n"
        "   dsb                                 \n"
        "   isb                                 \n"
        "   bl task_vSwitchContext              \n"
        "   mov r0, #0                          \n"
        "   msr basepri, r0                     \n"
        "   ldmia sp!, {r0, r3}                 \n"
        "                                       \n"
        "   ldr r1, [r3]                        \n" 
        "   ldr r0, [r1]                        \n"
        "                                       \n"
        "   ldmia r0!, {r4-r11, r14}            \n"
        "                                       \n"
        "   tst r14, #0x10                      \n" 
        "   it eq                               \n"
        "   vldmiaeq r0!, {s16-s31}             \n"
        "                                       \n"
        "   msr psp, r0                         \n"
        "   isb                                 \n"
        "                                       \n"
        "                                       \n"
        "   bx r14                              \n"
        "                                       \n"
        "   .align 4                            \n"
        "pxCurTCBConst: .word g_pxCurTCB        \n"
        ::"i" ( cfgMAX_SYSCALL_INTERRUPT_PRIORITY )
    );
}

2.2 流程

任务运行中(Thread Mode + PSP)
  ↓
触发任务切换(如 taskYIELD / task_vDelay / SysTick 唤醒高优任务)
  ↓
写 ICSR.PENDSVSET 位 → 挂起 PendSV 异常(portYIELD)
  ↓
(等待安全时机:所有高优先级中断执行完毕)
  ↓
PendSV 异常触发,CPU 切换到 Handler Mode
  ↓
硬件自动将当前任务的 8 个寄存器(R0-R3/R12/LR/PC/xPSR)压入 MSP 栈
  ↓
进入自定义的 PendSV 异常处理函数(port_vPendSVHandler)
  ↓
读取当前任务的 PSP(mrs r0, psp)→ 获取当前任务栈顶
  ↓
手动保存 R4-R11 和 EXC_RETURN(0xfffffffd)到当前任务的栈(stmdb r0!, {r4-r11, r14})
  ↓
更新当前任务 TCB 的 pu32TopOfStack 字段(str r0, [g_pxCurTCB])
  ↓
调用 task_vSwitchContext():从就绪队列选择最高优先级任务作为新任务
  ↓
从新任务 TCB 中读取其 pu32TopOfStack → 加载新任务栈顶到 r0
  ↓
从新任务栈中恢复 R4-R11 和 EXC_RETURN(ldmia r0!, {r4-r11, r14})
  ↓
设置 PSP 为新任务的栈指针(msr psp, r0)
  ↓
异常返回(bx r14),因 r14 = 0xfffffffd,硬件从 PSP 弹出 8 个寄存器(R0-R3/PC/xPSR)
  ↓
加载新任务上次被切换出去时保存的现场值到 PC 等寄存器
  ↓
新任务继续执行!

可以看出,这里旧任务的现场(即8个寄存器值)在 PendSV 异常处理中被保存在了旧任务的PSP栈中,那么假如在新任务切换到旧任务时,重新读取的PSP栈就不是一开始伪造的,而是在 PendSV 异常处理中手动压入的原始现场

所以可以得知,无论是执行第一个新任务的伪造现场,还是旧任务切换到新任务时的切换现场,核心都是在异常处理时设置PSP为新任务的栈,然后从这个栈中弹出值到寄存器中恢复新任务的现场,只不过执行第一个新任务时栈中的值是伪造的切换到新任务时栈中的值是在PendSV异常中保存的。

2.3 详细步骤

步骤

指令

作用详解

1

mrs r0, psp

读取当前任务的 进程栈指针(PSP) 到 r0,用于保存/恢复上下文

2

isb

指令同步屏障

3

ldr r3, pxCurTCBConst

加载 g_pxCurTCB 的地址到 r3。

4

ldr r2, [r3]

从 r3 读取 g_pxCurTCB 的值(当前任务 TCB 指针)到 r2。

5

tst r14, #0x10

测试 r14/LR(即 EXC_RETURN 值)的 bit4 是否为 0。
bit4=0 → 表示使用了 FPU(FPCA=1)。

6

it eq

IT(If-Then)指令:若上一条结果为相等(即 bit4=0),则下一条指令条件执行。

7

vstmdbeq r0!, {s16-s31}

若使用 FPU,则将高 16 个浮点寄存器(S16-S31)压栈(满递减,写后更新)。

8

stmdb r0!, {r4-r11, r14}

R4-R11 和 R14 压入当前任务栈。
stmdb = Store Multiple Decrement Before(满递减)。

9

str r0, [r2]

当前任务上下文保存:将更新后的栈顶指针(r0)写回当前任务的 TCB(pxTopOfStack 字段)。

10

stmdb sp!, {r0, r3}

将 r0(新栈顶)、r3(g_pxCurTCB 地址)临时压入 MSP 栈(Handler 模式栈)。

11

mov r0, %0

将立即数 %0(即 cfgMAX_SYSCALL_INTERRUPT_PRIORITY)加载到 r0

12

msr basepri, r0

进入临界区:设置 BASEPRI,屏蔽优先级数值 ≥ 此值的中断(即允许更高优先级中断,禁止低优先级)。

13–14

dsb / isb

数据/指令同步屏障,确保 BASEPRI 生效。

15

bl task_vSwitchContext

调用 C 函数 task_vSwitchContext()
• 选择下一个要运行的任务
• 更新 g_pxCurTCB 指向新任务

16

mov r0, #0

清零 r0

17

msr basepri, r0

退出临界区:清除 BASEPRI(写 0),重新使能所有中断

18

ldmia sp!, {r0, r3}

从 MSP 栈弹出之前保存的 r0 和 r3。

19

ldr r1, [r3]

重新加载 g_pxCurTCB(现在指向新任务)到 r1。

20

ldr r0, [r1]

从新任务 TCB 读取其 栈顶指针(pxTopOfStack) 到 r0。

21

ldmia r0!, {r4-r11, r14}

从新任务栈中弹出 R4-R11 和 R14 并更新 r0

22

tst r14, #0x10

再次测试 EXC_RETURN 的 bit4(判断新任务是否使用 FPU)。

23

it eq

条件执行准备。

24

vldmiaeq r0!, {s16-s31}

若新任务使用 FPU,则从栈中恢复 S16-S31。

25

msr psp, r0

将更新后的 r0(指向新任务的异常帧起始位置)写入 PSP

26

isb

指令同步,确保 PSP 写入完成。

27

bx r14

异常返回:跳转到 r14(即 EXC_RETURN 值,如 0xFFFFFFFD),触发硬件自动从 PSP 弹出 xPSR/PC/LR/R12/R3-R0,切换到线程模式 + PSP,新任务开始运行

28

.align 4

字节对齐

29

pxCurTCBConst: .word g_pxCurTCB

定义文字池项,值为 g_pxCurTCB 的地址,供第 3 行 LDR 使用。

流程图:

3. 总结

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐