操作系统开发:(10) 线程创建与调度的底层原理:从硬件行为解释线程

核心: 无论是执行第一个新任务的伪造现场,还是旧任务切换到新任务时的切换现场,核心都是在异常处理时设置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
  1. 恢复主栈:从向量表读取 MSP 初始值,确保中断有合法栈空间
  2. 强制特权:清零 CONTROL 寄存器,保证后续操作在特权模式下执行
  3. 触发异常: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} | 从任务栈中弹出寄存器 : - 加载 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 源码

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. 总结

相关推荐
爱编码的小八嘎2 小时前
第2章 认识CPU-2.3 32位微处理器(2)
c语言
czhc11400756632 小时前
通信217
服务器·网络·tcp/ip
深信达沙箱2 小时前
终端沙箱数据防泄密方案
网络·安全
键盘鼓手苏苏2 小时前
Flutter for OpenHarmony:dart_ping 网络诊断的瑞士军刀(支持 ICMP Ping) 深度解析与鸿蒙适配指南
开发语言·网络·flutter·华为·rust·harmonyos
独行soc2 小时前
2026年渗透测试面试题总结-26(题目+回答)
android·网络·安全·web安全·渗透测试·安全狮
拍客圈2 小时前
Discuz搜索报错
服务器·网络·安全
枫叶丹43 小时前
【Qt开发】Qt界面优化(四)-> Qt样式表(QSS) 选择器概况
c语言·开发语言·c++·qt
郝学胜-神的一滴3 小时前
深入理解TCP连接的优雅关闭:半关闭状态与四次挥手的艺术
linux·服务器·开发语言·网络·tcp/ip·程序人生
hoududubaba8 小时前
ORAN压缩之块浮点压缩
网络·网络协议