ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Threadx源码解析——任务栈的构建

2026/8/10 8:43:38 拓冰建站 浏览量
Threadx源码解析——任务栈的构建 _tx_thread_stack_build任务栈结构分析一、函数概述1.1 功能描述在线程创建时为线程构建一个伪造的中断栈帧。当线程首次被调度运行时调度器的中断返回路径会从这个栈帧中恢复上下文使线程从指定的入口函数开始执行。1.2 调用者_tx_thread_create()- 创建线程服务1.3 输入参数参数寄存器说明thread_ptrr0线程控制块指针function_ptrr1线程入口函数指针1.4 核心设计思想“伪造一个中断栈帧”—— 让调度器的中断返回路径LDMIA sp!, {r0-r12, lr, pc}^以为这个线程是被中断打断的从而自动完成首次启动。二、栈帧布局2.1 官方文档中的栈帧结构栈顶低地址 --------------------------- | 1 ← 栈帧类型 | ← sp 指向这里 --------------------------- | CPSR ← 初始 CPSR | --------------------------- | r0 (a1) ← 初始值 0 | --------------------------- | r1 (a2) ← 初始值 0 | --------------------------- | r2 (a3) ← 初始值 0 | --------------------------- | r3 (a4) ← 初始值 0 | --------------------------- | r4 (v1) ← 初始值 0 | --------------------------- | r5 (v2) ← 初始值 0 | --------------------------- | r6 (v3) ← 初始值 0 | --------------------------- | r7 (v4) ← 初始值 0 | --------------------------- | r8 (v5) ← 初始值 0 | --------------------------- | r9 (sb) ← 初始值 0 | --------------------------- | r10 (sl) ← 栈起始地址 | --------------------------- | r11 (fp) ← 初始值 0 | --------------------------- | r12 (ip) ← 初始值 0 | --------------------------- | lr (r14) ← _tx_thread_schedule | --------------------------- | pc (r15) ← function_ptr | --------------------------- | 0 ← GDB 回溯用 | --------------------------- 栈底高地址2.2 偏移量表偏移字节寄存器存储内容说明0x00—0x00000001栈帧类型标记非零 中断帧0x04CPSRSVC_MODE | (CPSR ~0x1F)初始 CPSRSVC 模式中断使能0x08r00x00000000参数寄存器 10x0Cr10x00000000参数寄存器 20x10r20x00000000参数寄存器 30x14r30x00000000参数寄存器 40x18r40x00000000被调用者保存寄存器0x1Cr50x00000000被调用者保存寄存器0x20r60x00000000被调用者保存寄存器0x24r70x00000000被调用者保存寄存器0x28r80x00000000被调用者保存寄存器0x2Cr90x00000000被调用者保存寄存器0x30r10栈起始地址被调用者保存寄存器0x34r110x00000000被调用者保存寄存器0x38r120x00000000临时寄存器0x3Clr_tx_thread_schedule链接寄存器返回地址0x40pcfunction_ptr线程入口函数地址0x44—0x00000000GDB 回溯用标记总栈帧大小 0x4872 字节 4 字节对齐 76 字节0x4C三、代码逐段解析3.1 步骤 1计算栈帧位置LDR r2, [r0, #16] r2 thread_ptr-tx_thread_stack_end BIC r2, r2, #7 8 字节对齐清除低 3 位 SUB r2, r2, #76 分配 76 字节栈帧空间TCB 偏移 0x10tx_thread_stack_end栈结束地址即最高地址线程栈布局创建后 高地址 ← tx_thread_stack_end --------------------------- │ 76 字节 │ ← 栈帧空间伪造中断帧 │ 栈帧区域 │ --------------------------- 低地址 ← r2新的栈顶→ 保存到 TCB8为什么要 8 字节对齐ARM AAPCS 规范要求栈保持 8 字节对齐特别是当使用 VFP 时LDRD/STRD 指令要求 8 字节对齐3.2 步骤 2设置栈帧类型标记MOV r3, #1 栈帧类型 1中断帧 STR r3, [r2, #0] 存储到偏移 0x00为什么是 1 而不是其他值调度器的中断返回路径中LDMIA sp!, {r4, r5}弹出第一个值到r4CMP r4, #0判断1 ! 0→ 走中断返回路径这会让调度器执行LDMIA sp!, {r0-r12, lr, pc}^值含义路径0主动挂起帧_tx_solicited_return非零中断帧含新建线程中断返回路径3.3 步骤 3设置通用寄存器初始值MOV r3, #0 r3 0 STR r3, [r2, #8] r0 0 STR r3, [r2, #12] r1 0 STR r3, [r2, #16] r2 0 STR r3, [r2, #20] r3 0 STR r3, [r2, #24] r4 0 STR r3, [r2, #28] r5 0 STR r3, [r2, #32] r6 0 STR r3, [r2, #36] r7 0 STR r3, [r2, #40] r8 0 STR r3, [r2, #44] r9 0所有通用寄存器初始化为 0除了r10特殊处理lr特殊处理3.4 步骤 4设置特殊寄存器LDR r3, [r0, #12] r3 thread_ptr-tx_thread_stack_start STR r3, [r2, #48] r10 (sl) 栈起始地址r10的特殊用途在某些 GDB 调试配置中r10用作栈限制或静态基址设置为栈起始地址可以帮助调试器识别栈边界LDR r3, _tx_thread_schedule r3 _tx_thread_schedule STR r3, [r2, #60] lr _tx_thread_schedulelr的特殊用途当线程执行完毕返回时lr指向调度器如果线程入口函数返回会跳转到_tx_thread_schedule重新进入调度循环STR r1, [r2, #64] pc function_ptr线程入口函数这是最关键的一步pc被设置为线程的入口函数地址中断返回时CPU 会跳转到这个地址执行MOV r3, #0 STR r3, [r2, #68] 0 for back-traceGDB 回溯标记GDB 回溯支持调试器在回溯栈帧时遇到 0 值会停止这是栈帧结束的标记3.5 步骤 5设置初始 CPSRMRS r1, CPSR 读取当前 CPSR BIC r1, r1, #CPSR_MASK 清除模式位低 5 位 ORR r3, r1, #SVC_MODE 设置 SVC 模式 STR r3, [r2, #4] 存储到偏移 0x04CPSR 位置CPSR 构建逻辑原始 CPSR [N][Z][C][V][Q]...[I][F][T][M4-M0] ↓ BIC 清除低 5 位模式位 ↓ ORR 设置 SVC_MODE (0x13) ↓ 最终 CPSR (原始高 27 位) | 0x13SVC_MODE 0x13Supervisor 模式CPSR_MASK 定义CPSR_MASK 0x9F 清除位I(7), F(6), T(5), M4-M0(4:0) 或 CPSR_MASK 0xDF FIQ 启用时结果线程以SVC 模式运行IRQ 中断使能因为 CPSR 的 I 位未被设置FIQ 中断根据配置决定3.6 步骤 6保存栈指针到 TCBSTR r2, [r0, #8] thread_ptr-tx_thread_stack_ptr r2TCB 偏移 0x08tx_thread_stack_ptr调度器在切换线程时通过LDR sp, [r0, #8]读取这个值。四、完整栈帧数据结构图4.1 内存布局详细版内存地址低地址 → 高地址 偏移 0x00 ------------------------------------------- | 0x00000001 ← 栈帧类型标记 (中断帧) | 偏移 0x04 ------------------------------------------- | CPSR 0x00000013 ← SVC 模式IRQ 使能 | 偏移 0x08 ------------------------------------------- | r0 0x00000000 ← a1 | 偏移 0x0C ------------------------------------------- | r1 0x00000000 ← a2 | 偏移 0x10 ------------------------------------------- | r2 0x00000000 ← a3 | 偏移 0x14 ------------------------------------------- | r3 0x00000000 ← a4 | 偏移 0x18 ------------------------------------------- | r4 0x00000000 ← v1 | 偏移 0x1C ------------------------------------------- | r5 0x00000000 ← v2 | 偏移 0x20 ------------------------------------------- | r6 0x00000000 ← v3 | 偏移 0x24 ------------------------------------------- | r7 0x00000000 ← v4 | 偏移 0x28 ------------------------------------------- | r8 0x00000000 ← v5 | 偏移 0x2C ------------------------------------------- | r9 0x00000000 ← sb | 偏移 0x30 ------------------------------------------- | r10 栈起始地址 ← sl (栈限制/静态基址) | 偏移 0x34 ------------------------------------------- | r11 0x00000000 ← fp (帧指针) | 偏移 0x38 ------------------------------------------- | r12 0x00000000 ← ip | 偏移 0x3C ------------------------------------------- | lr _tx_thread_schedule ← 返回地址 | 偏移 0x40 ------------------------------------------- | pc function_ptr ← ★ 线程入口函数 ★ | 偏移 0x44 ------------------------------------------- | 0x00000000 ← GDB 回溯结束标记 | 偏移 0x48 ------------------------------------------- | (填充) ← 76 字节对齐填充 | ------------------------------------------- ↑ sp 指向这里 (TCB8) 高地址 ← tx_thread_stack_end (原始值)4.2 与调度器恢复的对应关系当调度器首次选择该线程执行时 1. 切换到任务栈 LDR sp, [r0, #8] sp 指向栈顶偏移 0x00 2. 弹出栈帧类型和 CPSR LDMIA sp!, {r4, r5} r4 0x00000001非零 r5 CPSR CMP r4, #0 非零 → 走中断返回路径 BEQ _tx_solicited_return 不跳转 3. 恢复 SPSR MSR SPSR_cxsf, r5 SPSR CPSR 4. 恢复所有寄存器并返回 LDMIA sp!, {r0-r12, lr, pc}^ 从偏移 0x08 开始恢复恢复后r0-r12 0除 r10 为栈起始地址lr_tx_thread_schedulepcfunction_ptr线程入口函数开始执行CPSR SVC 模式中断使能五、关键设计要点5.1 为什么使用伪造中断帧而不是主动挂起帧对比项中断帧本函数使用主动挂起帧恢复方式LDMIA ... {pc}^硬件返回手动恢复 CPSR BX lr寄存器恢复完整的r0-r12, lr, pc只恢复r4-r11, lr首次启动✅ 直接从入口函数执行❌ 无法启动新线程使用中断帧的好处恢复路径简单无需手动设置 CPSR硬件自动完成 CPSR 恢复与真正的中断返回路径一致代码复用5.2 为什么lr设置为_tx_thread_schedule线程入口函数执行 ↓ 线程执行完毕函数返回BX lr ↓ lr 指向 _tx_thread_schedule ↓ 返回调度器调度器选择下一个线程这实现了线程的尾调用结束当线程函数返回时自动回到调度器不需要显式调用tx_thread_terminate()5.3 栈帧大小 76 字节的计算寄存器数量 × 4 字节 对齐填充 18 个字段 × 4 字节 4 字节对齐 72 4 76 字节字段数量说明1栈帧类型 (0x00)1CPSR (0x04)13r0-r12 (0x08-0x38)1lr (0x3C)1pc (0x40)1回溯标记 (0x44)18总字段数5.4 GDB 回溯支持GDB 回溯栈帧时 从当前 sp 开始遇到 0x00000000 停止 ↓ 这使得 GDB 不会无限回溯到无效地址六、与 TCB 的关联6.1 使用的 TCB 字段偏移字段用途0x08tx_thread_stack_ptr保存构建后的栈顶指针0x0Ctx_thread_stack_start用作 r10 的初始值0x10tx_thread_stack_end计算栈帧位置6.2 与调度器的配合_tx_thread_create() ↓ _tx_thread_stack_build() ↓ 构建伪造中断帧到线程栈 ↓ 线程被加入就绪队列 ↓ 调度器选择该线程 ↓ _tx_thread_schedule() 执行中断返回路径 ↓ 线程首次运行从 function_ptr 开始执行七、总结项目说明核心功能为新建线程构建伪造中断栈帧栈帧大小76 字节18 个字段栈帧类型0x00000001中断帧初始模式SVC 模式中断使能初始 pc线程入口函数地址初始 lr_tx_thread_schedule恢复路径中断返回路径^后缀设计思想复用调度器的中断返回机制实现线程首次启动