线程:操作系统调度CPU的最小单位一个进程可以有多个线程,共享进程资源,每个线程有独立的执行流、独立栈、独立寄存器上下文。
进程:拥有独立的地址空间,全局变量、堆。开销大。
工程中一个tcb线程控制块即一个任务,线程的详细数据结构:
typedef struct tcb { //sp 放第一个 = 汇编永远用 [r1] 零偏移 void *sp; /* 当前栈指针 (切换时存/取) */ void *entry; /* 线程入口函数 */ void *parameter; /* 入口函数参数 */ void *stack_addr; /* 栈起始地址 (低地址) */ uint32_t stack_size; /* 栈大小 (字节) */ uint8_t priority; /* 优先级: 0 最高, 越大越低 */ uint8_t base_priority; /* 基础优先级 = 创建时的静态优先级 (PI 只临时改 priority, 不动它) */ uint8_t state; //线程状态 const char *name; /* 线程名 (栈检查报警 / 将来 ps 命令用) */ list_t list; /* 链表节点: 同一时刻只在 就绪队列 或 睡眠队列 之一 */ uint32_t wake_tick;/* 唤醒时刻 (绝对 tick, 替代旧 remaining_ticks) */ list_t tick_node; /* 定时器链表节点 (独立于 list, 阻塞时同时挂两条队) */ list_t all_node; /* 全局线程链表节点: 栈检查 / ps 遍历所有线程 */ list_t taken_list; /* 持有锁链表: 挂各 mutex_t.taken_node (PI 重算时遍历) */ uint8_t timeout; /* 本次唤醒是否因超时: 1=超时, 0=正常唤醒 */ int block_result; /* 阻塞 API 返回值: 0=成功, -ETIMEDOUT=超时 */ uint8_t dynamic; /* 1=TCB+栈堆分配 (idle 回收时可 free); 0=静态分配, 只摘链不释放 */ } tcb_t;其中:
- sp:当前栈指针,必须放第一个成员,方便在汇编中获取sp指针时不需要计算结构体偏移。
- entry:线程的回调函数入口。
- parameter:回调函数参数。
- stack_addr:线程栈起始地址。
- stack_size:线程栈大小。
- priority:线程优先级大小,0最高。
- base_priority:基础优先级,用于PI处理。
- state:线程状态。
- name:线程名。
- list:链表节点。
- wake_tick:唤醒时间。
- tick_node:定时器链表。
- all_node:全局链表节点。
- taken_list:持锁链表。
- timeout:超时唤醒标志。
- block_result:阻塞的API返回值
- dynamic:动态创建线程的标志
线程状态如下:
typedef enum { THREAD_INIT = 0, /* 已创建, 未 thread_start (不在任何队列) */ THREAD_READY, /* 在就绪队列 */ THREAD_BLOCKED, /* 阻塞: 在某 IPC 等待队列 (sem/mutex/mq) */ THREAD_SLEEP, /* 睡眠: 在睡眠队列 */ THREAD_SUSPENDED, /* 被挂起: 在挂起队列 (被 thread_suspend) */ THREAD_DEFUNCT, /* 僵尸: 已退出, 在 g_defunct_list 等 idle 回收 TCB/栈*/ } thread_state_t;THREAD_INIT:初始化状态,TCB与线程栈已经创建完毕,但是没有调用thread_start(),该状态下不会在任何队列,调度器目前看不到这个线程。
THREAD_READY:就绪状态,执行thread_start()后将该线程添加到就绪队列,随时可以被调度器调度到CPU执行,也可以通过其他方式进入就绪状态,比如唤醒、定时器超时等。
THREAD_BLOCKED:阻塞状态,线程主动等待某个IPC资源,比如信号量、互斥锁、消息队列等唤醒后转为READY状态,加入就绪队列。
THREAD_SLEEP:睡眠状态,线程主动休息一段时间,等待时间到了才可以继续运行,时间到转为就绪队列。
THREAD_SUSPENDED:挂起状态,内核调用thread_suspend()挂起,不参与调度,通过thread_resume()才能唤醒。
THREAD_DEFUNCT:僵尸状态,线程函数已经执行完毕,已经退出,不再运行,但是TCB任务块以及线程栈还不能立即释放,放到僵尸链表,由idle线程统一回收。
状态机如下图所示:
伪造栈帧的本质:新线程从没运行过,没有“被打断的现场”可保存,但PendSV不区分首次与后续切换,只是机械地从sp取16个寄存器恢复,伪造栈帧就是造一套假的寄存器快照放在栈里,让PendSV第一次恢复无法分辨真假,新线程就像“被打断过一样”自然执行。
伪造栈帧需要填充寄存器,Cortex-M内核存在16个寄存器R0-R15、xPSR,具体功能如下表:
| 寄存器 | 名称 | 用途 | 是否硬件自动压栈 |
| R0 | R0 | 函数参数1 | 是 |
| R1 | R1 | 函数参数2 | 是 |
| R2 | R2 | 函数参数3 | 是 |
| R3 | R3 | 函数参数4 | 是 |
| R4 | R4 | 通用变量 | 否 |
| R5 | R5 | 通用变量 | 否 |
| R6 | R6 | 通用变量 | 否 |
| R7 | R7 | 通用变量 | 否 |
| R8 | R8 | 通用变量 | 否 |
| R9 | R9 | 通用变量 | 否 |
| R10 | R10 | 通用变量 | 否 |
| R11 | R11 | 通用变量 | 否 |
| R12 | R12 | 是 | |
| R13 | SP栈指针 | 栈指针 | 否 |
| R14 | LR | 链接返回地址 | 是 |
| R15 | PC | 程序计数器 | 是 |
注:xPSR是状态寄存器,硬件中断时一起压入栈帧,伪造栈帧必须置Thumb位
R0-R3、R12、LR、PC、xPSR由硬件自动压栈,R4-R11由PendSV手动压栈。SP不会压入栈。上下文切换的核心就是把当前SP存放到tcb->sp,切换时候再从tcb->sp取出。
SP指针分为MSP与PSP:
- MSP:主栈指针,复位默认使用,中断服务函数默认使用MSP。
- PSP:进程栈指针,一般用于任务线程栈。
LR链接寄存器:正常调用函数bl func相当于LR=下一条指令地址,函数return跳LR,进入异常时,硬件自动保存一个特殊的EXC_RETURN值到LR,用于异常返回。
PC程序计数器:PC保存当前正在取指的指令地址,CPU没取一条指令PC自动加1.
上下文切换逻辑:
- 进入PendSV异常,硬件自动压栈R0 R1 R2 R3 LR PC xPSR
- PendSV汇编手动压栈R4-R11
- 将SP存入tcb->sp
- 调度器选择新的tcb任务,取出新tcb的sp
- PendSV弹出R4-R11
- 异常返回,硬件自动弹出R0 R1 R2 R3 LR PC xPSR跳转,线程恢复运行
压栈完成如下所示:
高地址 (栈底)
xPSR = 0x01000000 ┐
pc = entry │
lr = thread_exit │
r12 = 0 │ 硬件异常栈帧
r3 = 0 │ (8 个, bx lr 时硬件自动弹)
r2 = 0 │
r1 = 0 │
r0 = parameter ┘
─────────────────────
r11 = 0xdeadbeef ┐
r10 = 0xdeadbeef │
r9 = 0xdeadbeef │ 软件保存区
r8 = 0xdeadbeef │ (8 个, PendSV 用 ldmia 弹)
r7 = 0xdeadbeef │
r6 = 0xdeadbeef │
r5 = 0xdeadbeef │
r4 = 0xdeadbeef ┘
低地址 (栈顶)
↑ TCB.sp 指向这里 (指向 r4 的位置)
栈向下生长,栈的上面是高地址下面是低地址,硬件压栈在高地址,软件压栈在低地址。
压栈:硬件自动压栈顺序为xPSR -> PC -> LR -> R12 -> R3 -> R2 -> R1 -> R0
软件压栈顺序为 R11 -> R10 -> R9 -> R8 -> R7 -> R6 -> R5 -> R4
出栈:软件先弹,顺序为 R4 -> R5 -> R6 -> R7 -> R8 -> R9 -> R10 -> R11。SP随之到R0位置
硬件在出弹,顺序为 R0 -> R1 -> R2 -> R3 -> R12 -> LR -> PC -> xPSR。PC被填成entry任务从入口开始跑。
伪造栈帧过程如下:
(1)首先填充整个栈为0xDE
(2)计算栈顶,并设置八字节对齐,这里通过& ~7UL设置对齐,通过&低三位0让其向下对齐到八字节,如下
stk = (uint32_t *)((uint32_t)stack_top & ~7UL);(3)R0填充回调函数的参数,R1-R3、R12设置为0,LR填充退出回调函数,PC填充需要执行的回调函数,Thumb置1,如下:
frame->exception.r0 = (uint32_t)parameter; /* entry 的第一个参数 */ frame->exception.r1 = 0; frame->exception.r2 = 0; frame->exception.r3 = 0; frame->exception.r12 = 0; frame->exception.lr = (uint32_t)exit; /* entry return 后跳这里 */ frame->exception.pc = (uint32_t)entry; /* 启动时跳到这里 */ frame->exception.psr = 0x01000000UL; /* Thumb 位 = 1, 必须 */对tcb线程控制块的变量进行初始化后,线程初始化完成。
上下文切换是由PendSV触发的,PendSV具体实现代码如下:
(1)获取PSP指针
mrs r0, psp
(2)获取当前正在执行的tcb的指针并获取里面内容
ldr r1, =g_current_tcb
ldr r1, [r1]
(3)将R4-R11压栈
stmdb r0!, {r4-r11}
(4)将正在运行任务的tcb指针保存到tcb->sp中
str r0, [r1]
(5)取下一个任务的tcb
ldr r1, =g_next_tcb
ldr r1, [r1]
ldr r0, [r1]
(6)将当前任务的指针指向下一个任务
ldr r2, =g_current_tcb
str r1, [r2]
(7)弹出R4-R11
ldmia r0!, {r4-r11}
(8)设置新的任务栈地址,切换栈帧
msr psp, r0
(9)EXC_RETURN 触发硬件出栈,弹出硬件自动压栈寄存器,返回线程模式
0xFFFFFFFD:返回线程模式,使用 PSP0xFFFFFFF9:返回线程模式,使用 MSP0xFFFFFFF1:返回异常模式
ldr lr, =EXC_RETURN_PSP
bx lr