核心: 无论是执行第一个新任务的伪造现场,还是旧任务切换到新任务时的切换现场,核心都是在异常处理时设置PSP为新任务的栈 ,然后从这个栈中弹出值到寄存器中恢复新任务的现场 ,只不过执行第一个新任务时栈中的值是伪造的 ,切换到新任务时栈中的值是在PendSV异常中保存的。
1. 通过 SVC 异常执行第一个任务
1.1 源码
cpp
// 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"
);
}
cpp
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 流程
第一个任务开始执行时:
Dart
上电
↓
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
cpp
// 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
cpp
// 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 |
|---|
- 恢复主栈:从向量表读取 MSP 初始值,确保中断有合法栈空间
- 强制特权:清零 CONTROL 寄存器,保证后续操作在特权模式下执行
- 触发异常:svc 0 是唯一能安全启动任务的途径,利用硬件异常机制完成上下文切换
1.5 第二步:在异常中启动第一个任务 port_vSVCHandler
cpp
// 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} | 从任务栈中弹出寄存器 : - 加载 r4 到 r11 (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 使用 |
|---|
- 硬件压栈:svc #0 触发后,硬件自动保存现场 (8 个寄存器的值)到 MSP 栈
- 软件恢复:从任务栈恢复 R4-R11 + EXC_RETURN (0xFFFFFFFD) 到 r14
- 硬件弹栈:bx r14 触发异常返回,硬件自动从 PSP 弹出 8 个寄存器并切换模式/栈
2. 通过 PendSV 异常切换任务
2.1 源码
cpp
// 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 流程
Dart
任务运行中(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. 总结
