第 5 章 进程与线程 — 5.4 系统调用 NtCreateThread()

概述

线程是操作系统中最基本的执行单元,理解线程的创建过程是深入理解操作系统内核运作机制的关键。本章聚焦于 Windows 内核中线程创建的核心系统调用 NtCreateThread,通过剖析其完整实现流程,揭示线程从概念到实体的转化过程。

本节目标

为什么要学习线程创建?

线程创建是操作系统最频繁执行的操作之一,几乎每个应用程序的运行都依赖于线程的创建和管理。理解线程创建过程有助于:

  1. 理解操作系统的资源分配机制:线程创建涉及内存分配(内核栈、用户栈、TEB)、对象管理(ETHREAD 对象创建)、调度器集成(KTHREAD 初始化)等多个子系统的协作
  2. 掌握进程与线程的关系:线程是进程的执行单元,共享进程的地址空间但拥有独立的执行上下文,这种关系在创建过程中得到充分体现
  3. 深入理解内核态与用户态的交互:线程创建跨越用户态和内核态,涉及系统调用、上下文切换、权限检查等关键机制
  4. 为后续学习打下基础:线程创建是理解线程调度、线程同步、线程生命周期管理的前提

内容结构

本节采用"理论+实践"的方式组织内容:

  1. 框架图:直观展示 NtCreateThread 的完整调用链
  2. 函数签名详解:分析 NtCreateThread 和 NtCreateThreadEx 的参数含义和设计意图
  3. 核心实现剖析:深入分析 PspCreateThread 的 14 个步骤,每个步骤都对应一个具体的资源分配或状态初始化操作
  4. 关键概念解读:详细解释内核栈、用户栈、TEB 等核心数据结构的作用和设计原理
  5. 设计决策分析:探讨线程创建过程中的关键设计决策及其背后的考量

学习路径

建议按照以下顺序阅读本节内容:

  1. 首先理解框架图,建立整体认知
  2. 分析函数签名,理解调用接口
  3. 深入 PspCreateThread 的每个步骤,理解线程创建的具体过程
  4. 对比内核栈与用户栈的差异,理解双栈设计的必要性
  5. 分析 TEB 的结构和用途,理解线程本地存储机制
  6. 思考关键设计决策,加深对设计哲学的理解

通过本节学习,读者将获得对 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 插入对象目录

调用流程总结

  1. 用户态到内核态CreateThreadNtCreateThread → syscall → NtCreateThread(内核)
  2. 参数验证:检查参数合法性和调用者权限
  3. 对象创建:创建 ETHREAD 对象并初始化基本字段
  4. 栈分配:分配内核栈和用户栈
  5. 调度初始化:初始化 KTHREAD,设置优先级和时间片
  6. 上下文设置:设置线程初始寄存器状态
  7. ID 分配:分配唯一的 TID
  8. 集成到系统:插入线程链表、创建 TEB、插入对象目录
  9. 返回句柄:将线程句柄返回给调用者

源码位置:[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!CreateThreadkernel32!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 工作空间的固定大小
连续物理内存 需要连续的物理页面 工作空间必须是连续的区域
线程私有 每个线程有独立的内核栈 每个员工有独立的工作空间
高地址 位于内核地址空间 工作空间在公司内部的核心区域

内核栈的用途

  1. 保存寄存器:中断或系统调用时保存当前寄存器状态
  2. 传递参数:内核函数调用时传递参数
  3. 局部变量:内核函数的局部变量存储
  4. 调用栈:函数调用链的追踪

用户栈:员工的办公桌

用户栈是线程在用户态执行时使用的栈空间。就像员工的办公桌,存放日常工作资料。

用户栈的特点

特性 说明 比喻
可增长 可以动态扩展 办公桌可以临时扩展
虚拟内存 使用虚拟地址空间 办公桌在虚拟办公区
线程私有 每个线程有独立的用户栈 每个员工有独立的办公桌
低地址 位于用户态地址空间 办公桌在公司的普通区域

用户栈的用途

  1. 函数调用:用户态函数调用的栈帧
  2. 局部变量:用户态函数的局部变量
  3. 参数传递:函数参数的传递
  4. 返回地址:函数返回地址的存储

栈切换

┌──────────────────────────────────────────────────────────────────────────────────┐
│                        线程栈切换示意图                                      │
│                                                                                  │
│   用户态执行:                                                                    │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ 用户栈 (低地址)                                                     │       │
│   │  ┌─────────────────────────────────────────────────────┐   │       │
│   │  │ 局部变量                                                     │   │       │
│   │  │ 函数参数                                                     │   │       │
│   │  │ 返回地址                                                     │   │       │
│   │  └─────────────────────────────────────────────────────┘   │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
│                                    │                                           │
│                                    ▼ syscall                                    │
│   内核态执行:                                                                    │
│   ┌─────────────────────────────────────────────────────────────────────┐       │
│   │ 内核栈 (高地址)                                                     │       │
│   │  ┌─────────────────────────────────────────────────────┐   │       │
│   │  │ 保存的用户态寄存器                                            │   │       │
│   │  │ 内核函数参数                                                  │   │       │
│   │  │ 内核函数局部变量                                              │   │       │
│   │  └─────────────────────────────────────────────────────┘   │       │
│   └─────────────────────────────────────────────────────────────────────┘       │
└──────────────────────────────────────────────────────────────────────────────────┘

双栈设计的意义

为什么线程需要两个独立的栈?这是操作系统安全模型的核心设计:

  1. 隔离性:内核栈和用户栈完全隔离,防止用户态代码直接访问内核栈,提高系统安全性。
  2. 权限保护:内核栈位于内核地址空间,只能在内核态访问;用户栈位于用户态地址空间,可以在用户态访问。
  3. 性能优化:内核栈较小且固定,便于缓存;用户栈较大且可扩展,适合用户态程序的需求。
  4. 上下文切换:系统调用时自动切换到内核栈,返回时自动切换回用户栈,确保执行环境的正确性。

栈溢出的风险

无论是内核栈还是用户栈,栈溢出都是严重的问题:

  • 内核栈溢出:会导致系统崩溃(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 的用途

  1. 线程本地存储(TLS):每个线程有独立的 TLS 空间。TLS 允许每个线程拥有自己的全局变量副本,这在多线程编程中非常有用。
  2. 异常处理:存储异常处理链。Windows 的结构化异常处理(SEH)通过 TEB 来维护异常处理帧链。
  3. 结构化异常处理(SEH):存储 SEH 帧。当异常发生时,系统会遍历 TEB 中的 SEH 链来找到异常处理程序。
  4. COM/OLE:支持 COM 对象的线程关联。OLE 自动化需要知道线程的状态。

TEB 的位置

  • 在 x86 系统上,TEB 位于 FS:[0]
  • 在 x64 系统上,TEB 位于 GS:[0]
  • 用户态代码可以通过段寄存器快速访问 TEB

TEB 的访问方式

用户态代码可以通过多种方式访问 TEB:

  1. 段寄存器:通过 FS:[0] 或 GS:[0] 直接访问
  2. NtQueryInformationThread:通过系统调用获取 TEB 地址
  3. TlsGetValue:通过 TLS 索引访问特定的 TLS 槽

TEB 的创建过程

TEB 在线程创建时由 MmCreateTeb 函数创建。创建过程包括:

  1. 在用户态地址空间分配内存
  2. 初始化 TEB 的各个字段
  3. 将 TEB 地址写入 ETHREAD 的 Teb 字段
  4. 设置线程的段寄存器指向 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 设计原则

  1. 线程是进程的执行单元:线程共享进程的资源,但有独立的执行上下文
  2. 栈隔离:内核栈和用户栈分离,提高安全性
  3. 分层设计:用户态 API → 系统调用 → 内部函数
  4. 事务性创建:失败时回滚所有已分配资源

5.4.8.3 常见陷阱

  1. 栈溢出:用户栈或内核栈溢出会导致崩溃
  2. 线程泄漏:创建线程后忘记关闭句柄
  3. 同步问题:多线程访问共享资源需要同步
  4. 死锁:线程相互等待对方释放资源

5.4.8.4 后续学习路径

  • 线程调度算法
  • 线程同步机制(互斥锁、事件、信号量)
  • 线程本地存储(TLS)
  • 线程池

源码位置:[ntoskrnl/ps/thread.c](file:///d:/reactos/ntoskrnl/ps/thread.c)

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐