Reactos 第 5 章 进程与线程 — 5.4 系统调用 NtCreateThread()
摘要 本章深入解析Windows内核中线程创建的核心系统调用NtCreateThread()。线程作为基本执行单元,其创建过程涉及多个关键子系统协作,包括内存分配、对象管理、调度器集成等。分析框架展示了从用户态API到内核实现的完整调用链,重点剖析了PspCreateThread()的14个核心步骤,涵盖线程对象创建、内核栈/用户栈分配、TEB设置等关键环节。通过理解线程创建机制,可以掌握操作系统
第 5 章 进程与线程 — 5.4 系统调用 NtCreateThread()
概述
线程是操作系统中最基本的执行单元,理解线程的创建过程是深入理解操作系统内核运作机制的关键。本章聚焦于 Windows 内核中线程创建的核心系统调用 NtCreateThread,通过剖析其完整实现流程,揭示线程从概念到实体的转化过程。
本节目标
为什么要学习线程创建?
线程创建是操作系统最频繁执行的操作之一,几乎每个应用程序的运行都依赖于线程的创建和管理。理解线程创建过程有助于:
- 理解操作系统的资源分配机制:线程创建涉及内存分配(内核栈、用户栈、TEB)、对象管理(ETHREAD 对象创建)、调度器集成(KTHREAD 初始化)等多个子系统的协作
- 掌握进程与线程的关系:线程是进程的执行单元,共享进程的地址空间但拥有独立的执行上下文,这种关系在创建过程中得到充分体现
- 深入理解内核态与用户态的交互:线程创建跨越用户态和内核态,涉及系统调用、上下文切换、权限检查等关键机制
- 为后续学习打下基础:线程创建是理解线程调度、线程同步、线程生命周期管理的前提
内容结构
本节采用"理论+实践"的方式组织内容:
- 框架图:直观展示 NtCreateThread 的完整调用链
- 函数签名详解:分析 NtCreateThread 和 NtCreateThreadEx 的参数含义和设计意图
- 核心实现剖析:深入分析 PspCreateThread 的 14 个步骤,每个步骤都对应一个具体的资源分配或状态初始化操作
- 关键概念解读:详细解释内核栈、用户栈、TEB 等核心数据结构的作用和设计原理
- 设计决策分析:探讨线程创建过程中的关键设计决策及其背后的考量
学习路径
建议按照以下顺序阅读本节内容:
- 首先理解框架图,建立整体认知
- 分析函数签名,理解调用接口
- 深入 PspCreateThread 的每个步骤,理解线程创建的具体过程
- 对比内核栈与用户栈的差异,理解双栈设计的必要性
- 分析 TEB 的结构和用途,理解线程本地存储机制
- 思考关键设计决策,加深对设计哲学的理解
通过本节学习,读者将获得对 Windows 线程创建机制的完整认知,为深入研究操作系统内核奠定坚实基础。
5.4.0 框架图
┌──────────────────────────────────────────────────────────────────────────────────┐
│ NtCreateThread 完整调用链 │
│ │
│ 用户态 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ kernel32!CreateThread / CreateRemoteThread │ │
│ │ │ │ │
│ │ └─► ntdll!NtCreateThread │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ syscall │
│ 内核态 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ntoskrnl!NtCreateThread │ │
│ │ │ │ │
│ │ └─► PspCreateThread (核心创建逻辑) │ │
│ │ │ │ │
│ │ ├─► 步骤 1: 参数校验 │ │
│ │ ├─► 步骤 2: 解析进程句柄 │ │
│ │ ├─► 步骤 3: 创建 ETHREAD 对象 │ │
│ │ ├─► 步骤 4: 初始化 ETHREAD 字段 │ │
│ │ ├─► 步骤 5: 创建内核栈 │ │
│ │ ├─► 步骤 6: 初始化 KTHREAD │ │
│ │ ├─► 步骤 7: 创建用户栈 │ │
│ │ ├─► 步骤 8: 设置线程上下文 │ │
│ │ ├─► 步骤 9: 分配 TID │ │
│ │ ├─► 步骤 10: 插入线程链表 │ │
│ │ ├─► 步骤 11: 设置调度参数 │ │
│ │ ├─► 步骤 12: 创建 TEB │ │
│ │ ├─► 步骤 13: 插入对象目录 │ │
│ │ └─► 步骤 14: 返回线程句柄 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────────┘
5.4.0.2 内部函数调用流程图
┌──────────────────────────────────────────────────────────────────────────────────┐
│ NtCreateThread 内部函数调用流程图 │
│ │
│ 用户态入口 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ kernel32!CreateThread / CreateRemoteThread │ │
│ │ │ │ │
│ │ └─► ntdll!NtCreateThread (syscall) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ syscall (INT 2E / SYSCALL) │
│ 内核态入口 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ntoskrnl!NtCreateThread │ │
│ │ │ │ │
│ │ │ 参数验证 & 模式检查 │ │
│ │ └─► PspCreateThread │ │
│ │ │ │ │
│ │ ├─► ObReferenceObjectByHandle() │ │
│ │ │ └─► 验证 ProcessHandle 有效性 │ │
│ │ │ │ │
│ │ ├─► ObCreateObject() │ │
│ │ │ └─► 创建 ETHREAD 对象 │ │
│ │ │ │ │
│ │ ├─► RtlZeroMemory() │ │
│ │ │ └─► 清零 ETHREAD 结构 │ │
│ │ │ │ │
│ │ ├─► KeAllocateStack() │ │
│ │ │ └─► 分配内核栈 (连续物理内存) │ │
│ │ │ │ │
│ │ ├─► KeInitializeThread() │ │
│ │ │ └─► 初始化 KTHREAD 调度结构 │ │
│ │ │ │ │
│ │ ├─► MmCreateThreadStack() │ │
│ │ │ └─► 分配用户栈 (虚拟内存) │ │
│ │ │ │ │
│ │ ├─► KeContextToKframes() │ │
│ │ │ └─► 设置线程初始上下文 │ │
│ │ │ │ │
│ │ ├─► ExCreateHandle() │ │
│ │ │ └─► 在 PspCidTable 中分配 TID │ │
│ │ │ │ │
│ │ ├─► InsertTailList() │ │
│ │ │ └─► 插入进程线程链表 │ │
│ │ │ │ │
│ │ ├─► KeSetPriorityThread() │ │
│ │ │ └─► 设置线程调度优先级 │ │
│ │ │ │ │
│ │ ├─► KeSetQuantumThread() │ │
│ │ │ └─► 设置线程时间片 │ │
│ │ │ │ │
│ │ ├─► MmCreateTeb() │ │
│ │ │ └─► 在用户态创建 TEB │ │
│ │ │ │ │
│ │ └─► ObInsertObject() │ │
│ │ └─► 将线程对象插入对象目录 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ return STATUS_SUCCESS │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 涉及的内核子系统: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ • Object Manager (OB): 对象创建与句柄管理 │ │
│ │ • Memory Manager (MM): 栈分配与 TEB 创建 │ │
│ │ • Process Manager (PS): 线程创建核心逻辑 │ │
│ │ • Kernel (KE): 线程初始化与调度参数设置 │ │
│ │ • Executive (EX): 句柄表管理 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────────┘
函数调用层次说明:
| 层次 | 函数 | 所属子系统 | 职责 |
|---|---|---|---|
| 1 | NtCreateThread |
PS | 系统调用入口,参数验证 |
| 2 | PspCreateThread |
PS | 线程创建核心逻辑 |
| 3 | ObReferenceObjectByHandle |
OB | 解析进程句柄 |
| 3 | ObCreateObject |
OB | 创建 ETHREAD 对象 |
| 3 | KeAllocateStack |
KE | 分配内核栈 |
| 3 | KeInitializeThread |
KE | 初始化 KTHREAD |
| 3 | MmCreateThreadStack |
MM | 分配用户栈 |
| 3 | KeContextToKframes |
KE | 设置线程上下文 |
| 3 | ExCreateHandle |
EX | 分配 TID |
| 3 | MmCreateTeb |
MM | 创建 TEB |
| 3 | ObInsertObject |
OB | 插入对象目录 |
调用流程总结:
- 用户态到内核态:
CreateThread→NtCreateThread→ syscall →NtCreateThread(内核) - 参数验证:检查参数合法性和调用者权限
- 对象创建:创建 ETHREAD 对象并初始化基本字段
- 栈分配:分配内核栈和用户栈
- 调度初始化:初始化 KTHREAD,设置优先级和时间片
- 上下文设置:设置线程初始寄存器状态
- ID 分配:分配唯一的 TID
- 集成到系统:插入线程链表、创建 TEB、插入对象目录
- 返回句柄:将线程句柄返回给调用者
源码位置:[ntoskrnl/ps/thread.c](file:///d:/reactos/ntoskrnl/ps/thread.c)
5.4.0.1 设计意图
核心问题
如何在内核态创建一个完整的线程对象?线程与进程的关系是什么?线程创建的关键步骤有哪些?
设计哲学:「线程是进程的执行单元」
想象一下,进程是一个公司,那么线程就是公司里的员工:
- 进程(公司):提供资源和环境(办公室、设备、资金)
- 线程(员工):执行具体的工作(编写代码、处理客户、管理事务)
- 内核栈:员工的工作空间,存放当前工作状态
- 用户栈:员工的办公桌,存放日常工作资料
- TEB:员工的个人档案,记录个人信息和工作状态
一个公司(进程)可以有多个员工(线程),他们共享公司的资源(内存、文件句柄),但每个人有自己的工作空间(栈)和个人档案(TEB)。
本节定位
本节深入分析 NtCreateThread 系统调用的完整实现,特别是核心函数 PspCreateThread。读完本节后,读者应当能够:
- 理解线程创建的完整流程(14 个步骤)
- 掌握线程与进程的关系
- 理解内核栈和用户栈的作用
- 理解 TEB 的结构和用途
- 理解线程调度的基本概念
5.4.1 NtCreateThread 函数签名
NtCreateThread:线程创建的"招聘专员"
NtCreateThread 是创建线程的核心系统调用。如果把创建线程比作招聘新员工,那么 NtCreateThread 就是招聘专员,负责为公司(进程)招聘新员工(线程)。
NTSTATUS NTAPI NtCreateThread(
OUT PHANDLE ThreadHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN HANDLE ProcessHandle,
OUT PCLIENT_ID ClientId,
IN PCONTEXT ThreadContext,
IN PETHREAD_INITIAL_TEB InitialTeb,
IN BOOLEAN CreateSuspended);
参数详解
| 参数 | 类型 | 说明 | 比喻 |
|---|---|---|---|
ThreadHandle |
OUT PHANDLE | 返回创建的线程句柄 | 员工工牌(拿到后可以管理员工) |
DesiredAccess |
IN ACCESS_MASK | 请求的访问权限 | 对员工的管理权限(如解雇、调岗) |
ObjectAttributes |
IN POBJECT_ATTRIBUTES | 对象属性(名称等) | 员工的职位名称、部门等信息 |
ProcessHandle |
IN HANDLE | 进程句柄 | 公司(线程属于哪个公司) |
ClientId |
OUT PCLIENT_ID | 返回线程的客户端 ID | 员工编号(TID)和所属公司(PID) |
ThreadContext |
IN PCONTEXT | 线程初始上下文 | 员工的初始工作状态(寄存器值) |
InitialTeb |
IN PETHREAD_INITIAL_TEB | 初始 TEB 数据 | 员工的个人档案初始数据 |
CreateSuspended |
IN BOOLEAN | 是否挂起创建 | 是否先让员工待命(不立即开始工作) |
参数深入分析
ThreadHandle 参数是一个输出参数,用于返回新创建线程的句柄。这个句柄是用户态代码与内核线程对象交互的唯一方式,通过它可以执行线程挂起、恢复、终止等操作。句柄的有效性依赖于引用计数机制——每次创建句柄时,线程对象的引用计数增加,关闭句柄时引用计数减少,当引用计数归零时线程对象才会被真正释放。
DesiredAccess 参数指定了调用者希望获得的线程访问权限。这些权限定义了调用者可以对线程执行哪些操作,例如 THREAD_TERMINATE 允许终止线程,THREAD_SUSPEND_RESUME 允许挂起和恢复线程,THREAD_GET_CONTEXT 允许读取线程的寄存器上下文。权限检查在对象创建时进行,确保只有具有足够权限的进程才能执行敏感操作。
ProcessHandle 参数指定了新线程所属的进程。线程必须属于某个进程,就像员工必须属于某个公司一样。这个句柄需要具有 PROCESS_CREATE_THREAD 权限,否则创建操作会失败。通过这个参数,内核可以验证调用者是否有权限在目标进程中创建线程,这是一种重要的安全机制。
ThreadContext 参数是线程创建中最关键的参数之一。它包含了线程开始执行时的寄存器状态,其中最重要的是指令指针(EIP/RIP),它指向线程函数的入口地址。这个参数允许调用者完全控制线程的初始执行状态,包括栈指针、通用寄存器值等。
返回值
| 返回值 | 说明 | 比喻 |
|---|---|---|
STATUS_SUCCESS |
创建成功 | 招聘成功,员工到岗 |
STATUS_INVALID_HANDLE |
无效句柄 | 公司不存在 |
STATUS_ACCESS_DENIED |
访问被拒绝 | 没有权限在该公司招聘 |
STATUS_INSUFFICIENT_RESOURCES |
资源不足 | 公司没有足够的办公空间 |
参数之间的关系
-
ProcessHandle 与线程归属:线程必须属于某个进程,就像员工必须属于某个公司。内核通过
ObReferenceObjectByHandle将句柄转换为进程对象指针,并在创建过程中验证该进程是否有权限接受新线程。 -
ThreadContext 与线程入口:
ThreadContext指定线程开始执行时的寄存器状态,特别是 EIP(指令指针)指向线程函数的入口。用户态线程通常从RtlUserThreadStart开始,这是一个 RTL 函数,负责初始化 TLS 和调用用户提供的线程函数。 -
CreateSuspended 与线程启动:如果为 TRUE,线程创建后处于挂起状态,需要调用
NtResumeThread才能开始执行。这种机制允许调用者在线程开始执行前完成额外的初始化工作,例如设置断点、修改线程上下文等。
调用上下文
NtCreateThread 通常由用户态的 kernel32!CreateThread 或 kernel32!CreateRemoteThread 调用。CreateThread 创建同一进程内的线程,而 CreateRemoteThread 可以在其他进程中创建线程,后者通常用于调试或注入代码。两种方式最终都通过系统调用进入内核态的 NtCreateThread 实现。
5.4.2 NtCreateThreadEx(新接口)
NtCreateThreadEx:升级版的"招聘系统"
随着 Windows 系统的发展,线程创建的需求越来越复杂。NtCreateThreadEx 就像是升级版的招聘系统,提供了更多的选项和更清晰的参数结构。
NTSTATUS NTAPI NtCreateThreadEx(
OUT PHANDLE ThreadHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN HANDLE ProcessHandle,
IN PVOID StartRoutine,
IN PVOID Argument,
IN ULONG CreateFlags,
IN SIZE_T ZeroBits,
IN SIZE_T StackSize,
IN SIZE_T MaximumStackSize,
IN PVOID AttributeList OPTIONAL);
新增参数
| 参数 | 说明 | 比喻 |
|---|---|---|
StartRoutine |
线程入口函数 | 员工的岗位职责描述 |
Argument |
传递给线程的参数 | 员工入职时收到的任务说明 |
CreateFlags |
创建标志位 | 招聘时的特殊要求 |
ZeroBits |
栈地址的零位数 | 办公位的位置要求 |
StackSize |
初始栈大小 | 办公桌的尺寸 |
MaximumStackSize |
最大栈大小 | 办公桌的最大扩展空间 |
AttributeList |
扩展属性列表 | 员工的额外资质要求 |
标志位(THREAD_CREATE_FLAGS_*)
| 标志 | 说明 | 比喻 |
|---|---|---|
THREAD_CREATE_FLAGS_CREATE_SUSPENDED |
创建时挂起 | 员工先待命,等待通知 |
THREAD_CREATE_FLAGS_SKIP_THREAD_ATTACH |
跳过线程附加通知 | 员工入职不需要通知其他部门 |
THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER |
对调试器隐藏 | 保密岗位,不接受常规检查 |
与 NtCreateThread 的关系
NtCreateThread 是 NtCreateThreadEx 的简化包装,就像快速招聘通道和完整招聘流程的关系。
5.4.3 PspCreateThread 深入分析
PspCreateThread:线程创建的"人事主管"
PspCreateThread 是线程创建的核心实现函数。如果把创建线程比作招聘新员工,那么 PspCreateThread 就是负责具体招聘流程的人事主管。
整个招聘过程分为 14 个步骤,每个步骤都有明确的目的和依赖关系。
步骤详解
┌──────────────────────────────────────────────────────────────────────────────────┐
│ PspCreateThread 执行流程 │
│ │
│ 步骤 1: 参数校验 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ if (CreateFlags & ~THREAD_CREATE_FLAGS_LEGAL_MASK) │ │
│ │ return STATUS_INVALID_PARAMETER; │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【招聘前检查】检查招聘申请是否符合规定。 │
│ │ │
│ ▼ │
│ 步骤 2: 解析进程句柄 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Status = ObReferenceObjectByHandle(ProcessHandle, │ │
│ │ PROCESS_CREATE_THREAD, │ │
│ │ PsProcessType, │ │
│ │ &Process); │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【确认公司】确认公司存在且有权招聘新员工。 │
│ │ │
│ ▼ │
│ 步骤 3: 创建 ETHREAD 对象 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Status = ObCreateObject(KernelMode, │ │
│ │ PsThreadType, │ │
│ │ ObjectAttributes, │ │
│ │ KernelMode, │ │
│ │ NULL, │ │
│ │ sizeof(ETHREAD), │ │
│ │ 0, │ │
│ │ 0, │ │
│ │ &Thread); │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【创建员工档案】为新员工创建 ETHREAD 对象。 │
│ │ │
│ ▼ │
│ 步骤 4: 初始化 ETHREAD 字段 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ RtlZeroMemory(Thread, sizeof(ETHREAD)); │ │
│ │ Thread->ThreadsProcess = Process; │ │
│ │ InitializeListHead(&Thread->ThreadListEntry); │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【填写基本信息】设置员工所属公司、初始化线程链表等。 │
│ │ │
│ ▼ │
│ 步骤 5: 创建内核栈 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Status = KeAllocateStack(&Thread->Tcb.KernelStack, │ │
│ │ KernelStackSize, │ │
│ │ ZeroBits, │ │
│ │ &Thread->Tcb.StackLimit); │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【分配工作空间】为员工分配内核栈,用于执行内核态代码。 │
│ │ │
│ ▼ │
│ 步骤 6: 初始化 KTHREAD │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ KeInitializeThread(&Thread->Tcb, │ │
│ │ &Thread->Tcb.KernelStack, │ │
│ │ ThreadStart, │ │
│ │ Context); │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【设置工作状态】初始化线程调度相关字段,设置线程入口。 │
│ │ │
│ ▼ │
│ 步骤 7: 创建用户栈 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ if (PreviousMode == UserMode) { │ │
│ │ Status = MmCreateThreadStack(Thread, │ │
│ │ StackSize, │ │
│ │ MaximumStackSize); │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【分配办公桌】为用户态线程分配用户栈。 │
│ │ │
│ ▼ │
│ 步骤 8: 设置线程上下文 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ KeContextToKframes(&Thread->Tcb, Context); │ │
│ │ Thread->Tcb.ContextSwitchState.ContextRecord = Context; │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【设置初始状态】将用户提供的上下文复制到线程结构中。 │
│ │ │
│ ▼ │
│ 步骤 9: 分配 TID │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Thread->Cid.UniqueThread = ExCreateHandle(PspCidTable, │ │
│ │ &CidEntry); │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【分配员工编号】通过 ExCreateHandle 在 PspCidTable 中分配 TID。 │
│ │ │
│ ▼ │
│ 步骤 10: 插入线程链表 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ InsertTailList(&Process->ThreadListHead, │ │
│ │ &Thread->ThreadListEntry); │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【加入公司名册】将线程插入所属进程的线程链表。 │
│ │ │
│ ▼ │
│ 步骤 11: 设置调度参数 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ KeSetPriorityThread(&Thread->Tcb, │ │
│ │ Process->Pcb.BasePriority); │ │
│ │ KeSetQuantumThread(&Thread->Tcb, │ │
│ │ KeCalculateQuantum(&Thread->Tcb)); │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【设置工作优先级】设置线程的调度优先级和时间片。 │
│ │ │
│ ▼ │
│ 步骤 12: 创建 TEB │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Status = MmCreateTeb(Thread, InitialTeb); │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【创建个人档案】在用户态地址空间中创建 TEB。 │
│ │ │
│ ▼ │
│ 步骤 13: 插入对象目录 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Status = ObInsertObject((PVOID)Thread, │ │
│ │ NULL, │ │
│ │ DesiredAccess, │ │
│ │ 0, │ │
│ │ NULL, │ │
│ │ ThreadHandle); │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【发放工牌】将线程对象插入对象目录,创建句柄返回给调用者。 │
│ │ │
│ ▼ │
│ 步骤 14: 返回线程句柄 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ return STATUS_SUCCESS; │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【招聘完成】所有步骤成功完成,返回 STATUS_SUCCESS。 │
└──────────────────────────────────────────────────────────────────────────────────┘
5.4.4 内核栈与用户栈
内核栈:员工的工作空间
内核栈是线程在内核态执行时使用的栈空间。就像员工的工作空间,存放当前正在处理的任务信息。
内核栈的特点:
| 特性 | 说明 | 比喻 |
|---|---|---|
| 固定大小 | 通常为 12KB 或 24KB | 工作空间的固定大小 |
| 连续物理内存 | 需要连续的物理页面 | 工作空间必须是连续的区域 |
| 线程私有 | 每个线程有独立的内核栈 | 每个员工有独立的工作空间 |
| 高地址 | 位于内核地址空间 | 工作空间在公司内部的核心区域 |
内核栈的用途:
- 保存寄存器:中断或系统调用时保存当前寄存器状态
- 传递参数:内核函数调用时传递参数
- 局部变量:内核函数的局部变量存储
- 调用栈:函数调用链的追踪
用户栈:员工的办公桌
用户栈是线程在用户态执行时使用的栈空间。就像员工的办公桌,存放日常工作资料。
用户栈的特点:
| 特性 | 说明 | 比喻 |
|---|---|---|
| 可增长 | 可以动态扩展 | 办公桌可以临时扩展 |
| 虚拟内存 | 使用虚拟地址空间 | 办公桌在虚拟办公区 |
| 线程私有 | 每个线程有独立的用户栈 | 每个员工有独立的办公桌 |
| 低地址 | 位于用户态地址空间 | 办公桌在公司的普通区域 |
用户栈的用途:
- 函数调用:用户态函数调用的栈帧
- 局部变量:用户态函数的局部变量
- 参数传递:函数参数的传递
- 返回地址:函数返回地址的存储
栈切换:
┌──────────────────────────────────────────────────────────────────────────────────┐
│ 线程栈切换示意图 │
│ │
│ 用户态执行: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 用户栈 (低地址) │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ 局部变量 │ │ │
│ │ │ 函数参数 │ │ │
│ │ │ 返回地址 │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ syscall │
│ 内核态执行: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 内核栈 (高地址) │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ 保存的用户态寄存器 │ │ │
│ │ │ 内核函数参数 │ │ │
│ │ │ 内核函数局部变量 │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────────┘
双栈设计的意义:
为什么线程需要两个独立的栈?这是操作系统安全模型的核心设计:
- 隔离性:内核栈和用户栈完全隔离,防止用户态代码直接访问内核栈,提高系统安全性。
- 权限保护:内核栈位于内核地址空间,只能在内核态访问;用户栈位于用户态地址空间,可以在用户态访问。
- 性能优化:内核栈较小且固定,便于缓存;用户栈较大且可扩展,适合用户态程序的需求。
- 上下文切换:系统调用时自动切换到内核栈,返回时自动切换回用户栈,确保执行环境的正确性。
栈溢出的风险:
无论是内核栈还是用户栈,栈溢出都是严重的问题:
- 内核栈溢出:会导致系统崩溃(BSOD),因为内核栈溢出会破坏内核数据结构。
- 用户栈溢出:可能导致程序崩溃,或者被利用进行缓冲区溢出攻击。
现代操作系统通过多种机制来防止栈溢出:
- 栈金丝雀(Stack Canary):在栈帧之间插入随机值,检测栈溢出。
- 地址空间布局随机化(ASLR):随机化栈的起始地址,增加攻击难度。
- 栈保护(Stack Protection):编译器生成额外的代码来检测栈溢出。
5.4.5 TEB (Thread Environment Block)
TEB:员工的个人档案
TEB 是线程环境块,存储线程的用户态信息。就像员工的个人档案,记录个人信息和工作状态。
TEB 的结构:
typedef struct _TEB {
PVOID Reserved1[12];
PPEB ProcessEnvironmentBlock; // 所属进程的 PEB
PVOID Reserved2[399];
BYTE Reserved3[1952];
PVOID TlsSlots[64]; // TLS 槽
BYTE Reserved4[8];
PVOID Reserved5[26];
PVOID ReservedForOle; // OLE 相关
PVOID Reserved6[4];
PVOID TlsExpansionSlots; // TLS 扩展槽
} TEB;
TEB 的关键字段:
| 字段 | 说明 | 比喻 |
|---|---|---|
ProcessEnvironmentBlock |
指向所属进程的 PEB | 员工所属公司的信息看板 |
TlsSlots |
线程本地存储槽 | 员工的个人储物柜 |
ReservedForOle |
OLE 自动化使用 | 员工的特殊技能证书 |
TEB 的用途:
- 线程本地存储(TLS):每个线程有独立的 TLS 空间。TLS 允许每个线程拥有自己的全局变量副本,这在多线程编程中非常有用。
- 异常处理:存储异常处理链。Windows 的结构化异常处理(SEH)通过 TEB 来维护异常处理帧链。
- 结构化异常处理(SEH):存储 SEH 帧。当异常发生时,系统会遍历 TEB 中的 SEH 链来找到异常处理程序。
- COM/OLE:支持 COM 对象的线程关联。OLE 自动化需要知道线程的状态。
TEB 的位置:
- 在 x86 系统上,TEB 位于 FS:[0]
- 在 x64 系统上,TEB 位于 GS:[0]
- 用户态代码可以通过段寄存器快速访问 TEB
TEB 的访问方式:
用户态代码可以通过多种方式访问 TEB:
- 段寄存器:通过 FS:[0] 或 GS:[0] 直接访问
- NtQueryInformationThread:通过系统调用获取 TEB 地址
- TlsGetValue:通过 TLS 索引访问特定的 TLS 槽
TEB 的创建过程:
TEB 在线程创建时由 MmCreateTeb 函数创建。创建过程包括:
- 在用户态地址空间分配内存
- 初始化 TEB 的各个字段
- 将 TEB 地址写入 ETHREAD 的 Teb 字段
- 设置线程的段寄存器指向 TEB
TEB 与线程安全:
TEB 是线程私有的,每个线程有自己的 TEB 副本。这意味着:
- 线程可以安全地修改自己的 TEB,不会影响其他线程
- TLS 槽是线程私有的,每个线程可以有不同的值
- 异常处理链是线程私有的,一个线程的异常不会影响其他线程
5.4.6 线程状态转换
线程的生命周期
线程从创建到终止会经历多个状态转换:
┌──────────────────────────────────────────────────────────────────────────────────┐
│ 线程状态转换图 │
│ │
│ 创建 │
│ │ │
│ ▼ │
│ 就绪 ──────────────────────────────────────┐ │
│ │ │ │
│ │ 调度器选择执行 │ │
│ ▼ │ │
│ 运行 ◄─────────────────────────────────────┤ │
│ │ │ │
│ │ 时间片用完 │ │
│ ├─────────────────────────────────────────┤ │
│ │ │ │
│ │ 等待资源 │ │
│ ▼ │ │
│ 等待 ──────────────────────────────────────┤ │
│ │ │ │
│ │ 资源可用 │ │
│ └─────────────────────────────────────────┘ │
│ │
│ 终止 │
└──────────────────────────────────────────────────────────────────────────────────┘
主要状态说明:
| 状态 | 说明 | 比喻 |
|---|---|---|
| 就绪 | 线程已准备好执行,等待调度器选择 | 员工在工位等待工作任务 |
| 运行 | 线程正在 CPU 上执行 | 员工正在处理工作任务 |
| 等待 | 线程等待某个事件(如 I/O、互斥锁) | 员工等待材料或同事配合 |
| 挂起 | 线程被显式挂起,需要 Resume 才能继续 | 员工被安排待命 |
5.4.7 关键设计决策
5.4.7.1 为什么每个线程需要独立的内核栈?
答案:隔离和安全性。
- 隔离性:每个线程有独立的执行上下文,需要独立的栈空间
- 安全性:防止一个线程的栈溢出影响其他线程
- 并发支持:多线程可以同时在内核态执行
5.4.7.2 为什么内核栈需要连续物理内存?
答案:性能和硬件要求。
- 性能:连续物理内存可以减少 TLB 刷新
- 硬件要求:某些硬件特性需要连续的物理地址
- 栈溢出检测:连续内存便于实现栈溢出检测
5.4.7.3 TEB 为什么在用户态地址空间?
答案:用户态代码需要访问。
- TLS 访问:用户态代码需要快速访问 TLS
- 异常处理:SEH 链需要在用户态可访问
- 性能:避免系统调用来访问线程信息
5.4.8 小结
5.4.8.1 关键知识点
| 主题 | 关键点 |
|---|---|
| NtCreateThread | 创建线程的系统调用 |
| NtCreateThreadEx | 扩展版本,支持更多选项 |
| PspCreateThread | 核心创建函数,14 个步骤 |
| 内核栈 | 线程在内核态使用的栈 |
| 用户栈 | 线程在用户态使用的栈 |
| TEB | 线程环境块,存储用户态信息 |
| TID | 线程标识符,通过 PspCidTable 分配 |
5.4.8.2 设计原则
- 线程是进程的执行单元:线程共享进程的资源,但有独立的执行上下文
- 栈隔离:内核栈和用户栈分离,提高安全性
- 分层设计:用户态 API → 系统调用 → 内部函数
- 事务性创建:失败时回滚所有已分配资源
5.4.8.3 常见陷阱
- 栈溢出:用户栈或内核栈溢出会导致崩溃
- 线程泄漏:创建线程后忘记关闭句柄
- 同步问题:多线程访问共享资源需要同步
- 死锁:线程相互等待对方释放资源
5.4.8.4 后续学习路径
- 线程调度算法
- 线程同步机制(互斥锁、事件、信号量)
- 线程本地存储(TLS)
- 线程池
源码位置:[ntoskrnl/ps/thread.c](file:///d:/reactos/ntoskrnl/ps/thread.c)
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)