第 5 章 进程与线程 --- 5.6 Windows 的进程创建和映像装入
概述
进程创建是操作系统最复杂的操作之一,它涉及内核多个子系统的协同工作。本节深入剖析 Windows 从用户态调用 CreateProcess 到新进程开始执行的完整流程,揭示进程创建和映像装入的核心机制。
本节内容概览
本节将系统地介绍以下内容:
1. 进程创建的完整流程
- 用户态入口:
CreateProcess的参数解析和准备工作 - 系统调用过渡:从
kernel32到ntdll再到内核态的转换 - 内核态核心:
NtCreateProcess和PspCreateProcess的执行逻辑 - 主线程创建:进程创建后如何创建并启动第一个线程
2. 关键数据结构的创建与初始化
EPROCESS对象的创建和初始化- 地址空间的建立(页表创建、内存映射)
PEB(进程环境块)的初始化ETHREAD和TEB的创建
3. 映像装入机制
- PE 文件的解析和验证
- Section 对象的创建和映射
- 重定位的处理
- 导入表的解析和 DLL 加载
4. 用户态初始化
LdrInitializeThunk的作用- DLL 的加载顺序和依赖解析
- TLS(线程本地存储)的初始化
DLL_PROCESS_ATTACH的调用
学习目标
读完本节后,您将能够:
- 理解从双击可执行文件到程序开始执行的完整过程
- 掌握进程创建涉及的核心数据结构(EPROCESS、PEB、ETHREAD、TEB)
- 理解 PE 映像如何被加载到内存并执行
- 了解 DLL 加载和初始化的机制
- 理解用户态和内核态在进程创建中的协作
涉及的内核子系统
进程创建是一个复杂的系统工程,涉及多个内核子系统的协作:
| 子系统 | 职责 |
|---|---|
| Process Manager (PS) | 进程/线程对象的创建和管理 |
| Memory Manager (MM) | 地址空间创建、内存映射、PEB/TEB 分配 |
| Object Manager (OB) | 对象创建、句柄管理、对象目录 |
| Security (SE) | 安全令牌分配和权限检查 |
| I/O Manager (IO) | 文件打开、Section 对象创建 |
| LDR (Loader) | 用户态 DLL 加载和初始化 |
关键函数调用链
用户态
│
├─► CreateProcessInternalW() [kernel32]
│ │
│ ├─► CreateFile() // 打开可执行文件
│ ├─► NtCreateSection() // 创建 Section 对象
│ └─► NtCreateProcess() // 进入内核
│
▼ syscall
│
内核态
│
├─► NtCreateProcess() [ntoskrnl]
│ │
│ └─► PspCreateProcess() // 核心创建逻辑
│ │
│ ├─► ObCreateObject() // 创建 EPROCESS
│ ├─► MmCreateProcessAddressSpace() // 创建地址空间
│ ├─► SeAssignSecurity() // 设置安全令牌
│ └─► PspCreateThread() // 创建主线程
│ │
│ └─► KeInitializeThread()
│
▼ 线程调度
│
新进程用户态
│
├─► LdrInitializeThunk() [ntdll]
│ │
│ ├─► LdrpInitializeProcess()
│ ├─► LdrLoadDll() // 加载依赖 DLL
│ ├─► LdrpRunInitializeRoutines() // TLS 初始化
│ └─► LdrpCallInitRoutines() // DLL_PROCESS_ATTACH
│
▼
└─► 用户程序入口点 (WinMain/main)
通过本节的学习,您将深入理解 Windows 进程创建的完整机制,为后续学习线程调度、内存管理等高级主题打下坚实基础。
5.6.0 框架图
┌──────────────────────────────────────────────────────────────────────────────────┐
│ Windows 进程创建与映像装入完整流程 │
│ │
│ 用户态 (Application) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 用户调用 CreateProcess("notepad.exe", ...) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ kernel32!CreateProcessInternalW │ │
│ │ │ │ │
│ │ ├─► 解析命令行参数 │ │
│ │ ├─► 打开可执行文件 │ │
│ │ ├─► 创建 Section 对象 │ │
│ │ ├─► 初始化 RTL_USER_PROCESS_PARAMETERS │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ntdll!NtCreateProcess │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ syscall │
│ │
│ 内核态 (ntoskrnl) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ NtCreateProcess → PspCreateProcess │ │
│ │ │ │ │
│ │ ├─► 创建 EPROCESS 对象 │ │
│ │ ├─► 创建地址空间 │ │
│ │ ├─► 初始化 KPROCESS │ │
│ │ ├─► 设置安全令牌 │ │
│ │ ├─► 初始化句柄表 │ │
│ │ ├─► 映射 Section 到地址空间 │ │
│ │ ├─► 分配 PID │ │
│ │ ├─► 创建 PEB │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ PspCreateThread → 创建主线程 │ │
│ │ │ │ │
│ │ ├─► 创建 ETHREAD 对象 │ │
│ │ ├─► 创建内核栈/用户栈 │ │
│ │ ├─► 设置线程上下文 (EIP = 入口点) │ │
│ │ ├─► 创建 TEB │ │
│ │ ├─► 分配 TID │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 线程加入就绪队列 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ 调度器选择执行 │
│ │
│ 用户态 (新进程) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 线程开始执行 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ntdll!LdrInitializeThunk │ │
│ │ │ │ │
│ │ ├─► 初始化 PEB_LDR_DATA │ │
│ │ ├─► 加载 NTDLL.dll │ │
│ │ ├─► 加载其他依赖 DLL │ │
│ │ ├─► 处理 TLS 初始化 │ │
│ │ ├─► 调用 DLL_PROCESS_ATTACH │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 用户程序入口点 (WinMain/main) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────────┘
5.6.0.1 设计意图
核心问题
Windows 如何将一个可执行文件变成一个运行中的进程?整个过程涉及哪些组件?
设计哲学 :「分层的进程创建与加载」
想象一下,启动一个程序就像开一家新公司:
- 申请营业执照(创建进程对象):向系统申请创建进程,获得进程句柄
- 租用办公场地(创建地址空间):为进程分配独立的虚拟地址空间
- 装修办公场地(映射可执行文件):将可执行文件映射到地址空间
- 招聘员工(创建主线程):创建第一个线程作为进程的执行入口
- 配备办公用品(创建栈和TEB):为线程分配栈空间和环境块
- 员工到岗(线程开始执行):线程开始运行,执行程序代码
- 正式营业(调用入口点):程序开始执行用户代码
本节定位
本节综合前面几节的内容,详细描述从用户调用 CreateProcess 到程序入口点执行的完整流程。读完本节后,读者应当能够:
- 理解完整的进程创建流程
- 掌握 PE 文件如何被加载到内存
- 理解 DLL 加载顺序
- 理解进程初始化的各个阶段
5.6.1 用户态的准备工作
CreateProcess:一站式进程创建服务
CreateProcess 是用户态创建进程的主要 API,它提供了一站式的进程创建服务。
CreateProcess 的完整签名:
c
BOOL WINAPI CreateProcess(
LPCWSTR lpApplicationName,
LPWSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCWSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation);
参数详解:
| 参数 | 说明 | 比喻 |
|---|---|---|
lpApplicationName |
可执行文件名 | 公司名称 |
lpCommandLine |
命令行参数 | 公司业务范围 |
lpProcessAttributes |
进程安全属性 | 公司注册信息 |
lpThreadAttributes |
线程安全属性 | 员工入职信息 |
bInheritHandles |
是否继承句柄 | 是否继承母公司资源 |
dwCreationFlags |
创建标志 | 特殊要求(如挂起创建) |
lpEnvironment |
环境变量 | 公司运营环境 |
lpCurrentDirectory |
当前目录 | 公司办公地址 |
lpStartupInfo |
启动信息(窗口位置等) | 公司初始配置 |
lpProcessInformation |
返回进程/线程信息 | 公司注册成功后的信息 |
CreateProcess 的内部流程:
┌──────────────────────────────────────────────────────────────────────────────────┐
│ CreateProcess 内部流程 │
│ │
│ 阶段 1: 参数解析 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 1. 解析 lpApplicationName 和 lpCommandLine │ │
│ │ 2. 确定可执行文件路径 │ │
│ │ 3. 解析环境变量 │ │
│ │ 4. 验证参数合法性 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 阶段 2: 文件操作 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 1. CreateFile 打开可执行文件 │ │
│ │ 2. CreateSection 创建 Section 对象 │ │
│ │ 3. CloseHandle 关闭文件句柄 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 阶段 3: 参数准备 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 1. 初始化 RTL_USER_PROCESS_PARAMETERS │ │
│ │ - CommandLine │ │
│ │ - Environment │ │
│ │ - CurrentDirectory │ │
│ │ - ImagePathName │ │
│ │ 2. 初始化 STARTUPINFO 相关数据 │ │
│ │ 3. 准备线程上下文 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 阶段 4: 系统调用 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 1. NtCreateProcess 创建进程 │ │
│ │ 2. NtCreateThread 创建主线程 │ │
│ │ 3. NtResumeThread 恢复线程(如果不是挂起创建) │ │
│ │ 4. 返回 PROCESS_INFORMATION │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────────┘
RTL_USER_PROCESS_PARAMETERS:
c
typedef struct _RTL_USER_PROCESS_PARAMETERS {
BYTE Reserved1[16];
PVOID Reserved2[10];
UNICODE_STRING ImagePathName; // 可执行文件路径
UNICODE_STRING CommandLine; // 命令行参数
PWSTR CurrentDirectory; // 当前目录
UNICODE_STRING DllPath; // DLL 搜索路径
PWSTR Environment; // 环境变量块
// ... 其他字段 ...
} RTL_USER_PROCESS_PARAMETERS;
5.6.2 内核态的进程创建
PspCreateProcess:进程创建的核心
PspCreateProcess 是内核态创建进程的核心函数,负责完成进程对象的创建和初始化。
完整流程回顾:
┌──────────────────────────────────────────────────────────────────────────────────┐
│ PspCreateProcess 完整流程 │
│ │
│ 步骤 1-4: 参数校验与对象解析 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 1. 校验 CreateFlags │ │
│ │ 2. 解析父进程句柄 → 获取 EPROCESS │ │
│ │ 3. 解析 SectionHandle → 获取 SECTION 对象 │ │
│ │ 4. 解析 DebugPort/ExceptionPort │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 步骤 5-7: 进程结构创建 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 5. ObCreateObject 创建 EPROCESS 对象 │ │
│ │ 6. 初始化 EPROCESS 字段 │ │
│ │ 7. MmCreateProcessAddressSpace 创建地址空间 │ │
│ │ - 分配页目录 │ │
│ │ - 设置 DirectoryTableBase │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 步骤 8-11: 进程初始化 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 8. KeInitializeProcess 初始化 KPROCESS │ │
│ │ 9. PspInitializeProcessSecurity 复制安全令牌 │ │
│ │ 10. ObInitializeObjectAttributes 初始化句柄表 │ │
│ │ 11. MmInitializeProcessAddressSpace 映射 Section │ │
│ │ - 解析 PE Header │ │
│ │ - 映射各个节区到内存 │ │
│ │ - 设置 SectionBaseAddress │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 步骤 12-15: 进程注册 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 12. ExCreateHandle 在 PspCidTable 分配 PID │ │
│ │ 13. InsertTailList 插入 PsActiveProcessHead │ │
│ │ 14. 设置调度参数 (Priority/Quantum) │ │
│ │ 15. MmCreatePeb 创建用户态 PEB │ │
│ │ - 初始化 PEB 字段 │ │
│ │ - 设置 ProcessParameters │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 步骤 16-18: 完成创建 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 16. ObInsertObject 插入对象目录 │ │
│ │ 17. 访问检查 │ │
│ │ 18. 返回进程句柄 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────────┘
地址空间初始化的细节:
c
NTSTATUS MmInitializeProcessAddressSpace(PEPROCESS Process, PSECTION Section) {
// 1. 获取 PE Header
PIMAGE_NT_HEADERS NtHeaders = MmGetImageHeader(Section);
// 2. 计算映像大小和基地址
PVOID ImageBase = NtHeaders->OptionalHeader.ImageBase;
SIZE_T ImageSize = NtHeaders->OptionalHeader.SizeOfImage;
// 3. 检查基地址是否可用
if (!MmIsAddressAvailable(Process, ImageBase, ImageSize)) {
// 如果基地址被占用,需要重定位
ImageBase = MmFindAvailableAddress(Process, ImageSize);
Process->Flags |= PROCEXP_RELOCATED;
}
// 4. 映射各个节区
PIMAGE_SECTION_HEADER SectionHeader = IMAGE_FIRST_SECTION(NtHeaders);
for (int i = 0; i < NtHeaders->FileHeader.NumberOfSections; i++) {
PVOID VirtualAddress = (PVOID)((ULONG_PTR)ImageBase + SectionHeader->VirtualAddress);
SIZE_T Size = max(SectionHeader->SizeOfRawData, SectionHeader->Misc.VirtualSize);
// 创建内存区域并映射文件内容
MmCreateVirtualMemory(Process, &VirtualAddress, Size,
PAGE_READWRITE, SEC_COMMIT | SEC_IMAGE);
// 设置正确的保护属性
ULONG Protect = MmSectionFlagsToProtection(SectionHeader->Characteristics);
MmProtectVirtualMemory(Process, &VirtualAddress, &Size, Protect);
SectionHeader++;
}
// 5. 设置进程的映像基地址
Process->SectionBaseAddress = ImageBase;
return STATUS_SUCCESS;
}
5.6.3 主线程的创建
PspCreateThread:线程创建的核心
创建完进程后,需要创建主线程才能让进程真正开始执行。
线程创建流程:
┌──────────────────────────────────────────────────────────────────────────────────┐
│ PspCreateThread 完整流程 │
│ │
│ 步骤 1-4: 线程结构创建 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 1. 参数校验 │ │
│ │ 2. 解析进程句柄 → 获取 EPROCESS │ │
│ │ 3. ObCreateObject 创建 ETHREAD 对象 │ │
│ │ 4. 初始化 ETHREAD 字段 │ │
│ │ - ThreadsProcess = Process │ │
│ │ - 初始化线程链表 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 步骤 5-8: 栈和上下文初始化 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 5. KeAllocateStack 创建内核栈 │ │
│ │ 6. KeInitializeThread 初始化 KTHREAD │ │
│ │ 7. MmCreateThreadStack 创建用户栈 │ │
│ │ 8. 设置线程上下文 │ │
│ │ - EIP = ImageBase + AddressOfEntryPoint │ │
│ │ - ESP = 用户栈顶 │ │
│ │ - 设置其他寄存器 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 步骤 9-14: 线程注册与完成 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 9. ExCreateHandle 在 PspCidTable 分配 TID │ │
│ │ 10. InsertTailList 插入 Process->ThreadListHead │ │
│ │ 11. 设置调度参数 (Priority/Quantum) │ │
│ │ 12. MmCreateTeb 创建用户态 TEB │ │
│ │ 13. ObInsertObject 插入对象目录 │ │
│ │ 14. 返回线程句柄 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 线程加入就绪队列 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ KeInsertQueueApc (如果有初始化 APC) │ │
│ │ KeReadyThread 将线程加入就绪队列 │ │
│ │ 调度器可以选择该线程执行 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────────┘
线程上下文的设置:
c
VOID KeContextToKframes(PKTHREAD Thread, PCONTEXT Context) {
// 将用户态上下文复制到内核栈
PKEXCEPTION_FRAME ExceptionFrame = (PKEXCEPTION_FRAME)(Thread->InitialStack - sizeof(KEXCEPTION_FRAME));
PKTRAP_FRAME TrapFrame = (PKTRAP_FRAME)(ExceptionFrame - sizeof(KTRAP_FRAME));
// 复制通用寄存器
TrapFrame->Eax = Context->Eax;
TrapFrame->Ecx = Context->Ecx;
TrapFrame->Edx = Context->Edx;
TrapFrame->Ebx = Context->Ebx;
// 设置栈指针和指令指针
TrapFrame->Esp = Context->Esp;
TrapFrame->Eip = Context->Eip;
// 设置标志寄存器
TrapFrame->Eflags = Context->EFlags;
// 设置段寄存器
TrapFrame->SegCs = KGDT_R0_CODE;
TrapFrame->SegDs = KGDT_R3_DATA;
TrapFrame->SegEs = KGDT_R3_DATA;
TrapFrame->SegFs = KGDT_R3_TEB;
// 保存指向帧的指针
Thread->InitialStack = (PVOID)TrapFrame;
Thread->TrapFrame = TrapFrame;
}
5.6.4 PE 映像的加载与重定位
核心流程概述:从磁盘读取 EXE 到内存执行的完整过程
当操作系统加载一个可执行文件时,需要完成以下关键步骤:读取文件头、验证格式、映射节区到内存、处理重定位、解析导入表、加载依赖 DLL。本节详细描述这个过程。
5.6.4.1 PE 文件头的解析过程
第一步:读取 DOS Header
加载器首先读取文件开头的 IMAGE_DOS_HEADER:
c
NTSTATUS ReadDosHeader(HANDLE FileHandle, PIMAGE_DOS_HEADER *OutDosHeader) {
// 1. 读取前 64 字节(DOS Header 的最小大小)
*OutDosHeader = AllocatePool(NonPagedPool, sizeof(IMAGE_DOS_HEADER));
DWORD BytesRead;
// 2. 从文件偏移 0 开始读取
if (!ReadFile(FileHandle, *OutDosHeader, sizeof(IMAGE_DOS_HEADER), &BytesRead, NULL)) {
return STATUS_INVALID_IMAGE_FORMAT;
}
// 3. 验证 DOS 签名 "MZ"
if ((*OutDosHeader)->e_magic != IMAGE_DOS_SIGNATURE) {
return STATUS_INVALID_IMAGE_FORMAT;
}
return STATUS_SUCCESS;
}
第二步:定位 PE Header
通过 DOS Header 的 e_lfanew 字段找到 PE Header 的位置:
c
NTSTATUS ReadNtHeaders(HANDLE FileHandle, PIMAGE_DOS_HEADER DosHeader, PIMAGE_NT_HEADERS *OutNtHeaders) {
// 1. e_lfanew 指向 PE Header 在文件中的偏移
DWORD NtHeadersOffset = DosHeader->e_lfanew;
// 2. 移动文件指针到 PE Header 位置
SetFilePointer(FileHandle, NtHeadersOffset, NULL, FILE_BEGIN);
// 3. 读取 PE Header
*OutNtHeaders = AllocatePool(NonPagedPool, sizeof(IMAGE_NT_HEADERS));
DWORD BytesRead;
if (!ReadFile(FileHandle, *OutNtHeaders, sizeof(IMAGE_NT_HEADERS), &BytesRead, NULL)) {
return STATUS_INVALID_IMAGE_FORMAT;
}
// 4. 验证 PE 签名 "PE\0\0"
if ((*OutNtHeaders)->Signature != IMAGE_NT_SIGNATURE) {
return STATUS_INVALID_IMAGE_FORMAT;
}
return STATUS_SUCCESS;
}
第三步:解析 Section Headers
读取所有节区头信息:
c
NTSTATUS ReadSectionHeaders(HANDLE FileHandle, PIMAGE_NT_HEADERS NtHeaders, PIMAGE_SECTION_HEADER *OutSections) {
// 1. Section Headers 紧跟在 Optional Header 之后
DWORD SectionsOffset = NtHeaders->e_lfanew +
sizeof(IMAGE_FILE_HEADER) +
NtHeaders->FileHeader.SizeOfOptionalHeader;
// 2. 计算 Section Headers 的总大小
DWORD NumSections = NtHeaders->FileHeader.NumberOfSections;
DWORD SectionsSize = NumSections * sizeof(IMAGE_SECTION_HEADER);
// 3. 读取所有 Section Headers
*OutSections = AllocatePool(NonPagedPool, SectionsSize);
SetFilePointer(FileHandle, SectionsOffset, NULL, FILE_BEGIN);
DWORD BytesRead;
if (!ReadFile(FileHandle, *OutSections, SectionsSize, &BytesRead, NULL)) {
return STATUS_INVALID_IMAGE_FORMAT;
}
return STATUS_SUCCESS;
}
5.6.4.2 内存映射与节区加载
地址空间预留
加载器首先在进程地址空间中预留一块区域:
c
NTSTATUS ReserveImageSpace(PEPROCESS Process, PIMAGE_NT_HEADERS NtHeaders, PVOID *OutImageBase) {
// 1. 获取期望的基地址和映像大小
PVOID DesiredBase = (PVOID)NtHeaders->OptionalHeader.ImageBase;
SIZE_T ImageSize = NtHeaders->OptionalHeader.SizeOfImage;
// 2. 尝试在期望地址预留空间
NTSTATUS Status = MmCreateVirtualMemory(
Process,
&DesiredBase,
ImageSize,
MEM_RESERVE,
PAGE_NOACCESS
);
if (!NT_SUCCESS(Status)) {
// 3. 如果期望地址不可用,找一个可用地址
*OutImageBase = MmFindAvailableAddress(Process, ImageSize);
Status = MmCreateVirtualMemory(
Process,
OutImageBase,
ImageSize,
MEM_RESERVE,
PAGE_NOACCESS
);
} else {
*OutImageBase = DesiredBase;
}
return Status;
}
节区映射
逐节区将文件内容映射到内存:
c
NTSTATUS MapSections(HANDLE FileHandle, PEPROCESS Process, PIMAGE_NT_HEADERS NtHeaders,
PIMAGE_SECTION_HEADER Sections, PVOID ImageBase) {
DWORD NumSections = NtHeaders->FileHeader.NumberOfSections;
for (DWORD i = 0; i < NumSections; i++) {
PIMAGE_SECTION_HEADER Section = &Sections[i];
// 1. 计算节区在内存中的地址
PVOID SectionBase = (PVOID)((ULONG_PTR)ImageBase + Section->VirtualAddress);
// 2. 计算需要提交的大小(取 VirtualSize 和 SizeOfRawData 的较大值)
SIZE_T CommitSize = max(Section->Misc.VirtualSize, Section->SizeOfRawData);
// 3. 提交物理内存
NTSTATUS Status = MmCreateVirtualMemory(
Process,
&SectionBase,
CommitSize,
MEM_COMMIT,
PAGE_READWRITE
);
if (!NT_SUCCESS(Status)) return Status;
// 4. 如果节区在文件中有数据,读取并复制到内存
if (Section->SizeOfRawData > 0) {
DWORD BytesRead;
SetFilePointer(FileHandle, Section->PointerToRawData, NULL, FILE_BEGIN);
ReadFile(FileHandle, SectionBase, Section->SizeOfRawData, &BytesRead, NULL);
}
// 5. 根据节区属性设置内存保护
ULONG Protection = SectionCharacteristicsToProtection(Section->Characteristics);
MmProtectVirtualMemory(Process, &SectionBase, &CommitSize, Protection);
}
return STATUS_SUCCESS;
}
节区属性转换
c
ULONG SectionCharacteristicsToProtection(ULONG Characteristics) {
ULONG Protection = 0;
if (Characteristics & IMAGE_SCN_CNT_CODE) {
Protection |= PAGE_EXECUTE;
}
if (Characteristics & IMAGE_SCN_MEM_READ) {
Protection |= PAGE_READONLY;
}
if (Characteristics & IMAGE_SCN_MEM_WRITE) {
Protection &= ~PAGE_READONLY;
Protection |= PAGE_READWRITE;
}
if (Characteristics & IMAGE_SCN_MEM_EXECUTE) {
Protection |= PAGE_EXECUTE;
}
return Protection;
}
5.6.4.3 重定位处理
为什么需要重定位
当 PE 文件无法加载到期望的基地址时,需要调整所有硬编码的地址引用:
c
NTSTATUS ApplyRelocations(PEPROCESS Process, PIMAGE_NT_HEADERS NtHeaders, PVOID ImageBase) {
// 1. 检查是否需要重定位
if (NtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size == 0) {
// 没有重定位信息,直接返回
return STATUS_SUCCESS;
}
// 2. 获取重定位表的位置和大小
PIMAGE_BASE_RELOCATION RelocTable = (PIMAGE_BASE_RELOCATION)(
(ULONG_PTR)ImageBase +
NtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress
);
SIZE_T RelocSize = NtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size;
// 3. 计算地址偏移量(Delta)
ULONG_PTR Delta = (ULONG_PTR)ImageBase - NtHeaders->OptionalHeader.ImageBase;
// 4. 遍历重定位块
while (RelocTable->VirtualAddress != 0) {
// 计算该重定位块的大小
DWORD BlockSize = RelocTable->SizeOfBlock;
DWORD NumEntries = (BlockSize - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);
// 获取重定位条目数组
PWORD RelocEntries = (PWORD)(RelocTable + 1);
// 处理每个重定位条目
for (DWORD i = 0; i < NumEntries; i++) {
WORD Entry = RelocEntries[i];
// 低 12 位是偏移量,高 4 位是类型
WORD Type = Entry >> 12;
WORD Offset = Entry & 0x0FFF;
// 计算需要修改的地址
PULONG_PTR AddressToFix = (PULONG_PTR)(
(ULONG_PTR)ImageBase + RelocTable->VirtualAddress + Offset
);
// 根据类型进行修复
switch (Type) {
case IMAGE_REL_BASED_ABSOLUTE:
// 不需要修改
break;
case IMAGE_REL_BASED_HIGHLOW:
// 完整的 32 位地址
*AddressToFix += Delta;
break;
case IMAGE_REL_BASED_DIR64:
// 64 位地址(仅 PE32+)
*(PULONG64)AddressToFix += (ULONG64)Delta;
break;
// 其他类型省略...
}
}
// 移动到下一个重定位块
RelocTable = (PIMAGE_BASE_RELOCATION)((ULONG_PTR)RelocTable + BlockSize);
}
return STATUS_SUCCESS;
}
5.6.4.4 导入表解析与 DLL 加载
导入表结构
c
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 表示结束
DWORD OriginalFirstThunk; // 指向原始名称表(INT)
};
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name; // DLL 名称的 RVA
DWORD FirstThunk; // 指向导入地址表(IAT)
} IMAGE_IMPORT_DESCRIPTOR;
导入表解析流程
c
NTSTATUS ProcessImportTable(PEPROCESS Process, PIMAGE_NT_HEADERS NtHeaders, PVOID ImageBase) {
// 1. 获取导入表的位置
PIMAGE_IMPORT_DESCRIPTOR ImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)(
(ULONG_PTR)ImageBase +
NtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress
);
// 2. 遍历每个导入描述符
while (ImportDesc->Name != 0) {
// 获取 DLL 名称
PCHAR DllName = (PCHAR)((ULONG_PTR)ImageBase + ImportDesc->Name);
// 加载 DLL
HMODULE hModule = LdrLoadDll(NULL, NULL, (PWSTR)DllName);
if (hModule == NULL) {
return STATUS_DLL_NOT_FOUND;
}
// 填充 IAT(导入地址表)
PIMAGE_THUNK_DATA OriginalFirstThunk = (PIMAGE_THUNK_DATA)(
(ULONG_PTR)ImageBase + ImportDesc->OriginalFirstThunk
);
PIMAGE_THUNK_DATA FirstThunk = (PIMAGE_THUNK_DATA)(
(ULONG_PTR)ImageBase + ImportDesc->FirstThunk
);
// 遍历导入函数
while (OriginalFirstThunk->u1.AddressOfData != 0) {
PVOID FunctionAddress;
if (IMAGE_SNAP_BY_ORDINAL(OriginalFirstThunk->u1.Ordinal)) {
// 按序号导入
WORD Ordinal = IMAGE_ORDINAL(OriginalFirstThunk->u1.Ordinal);
FunctionAddress = GetProcAddress(hModule, (LPCSTR)Ordinal);
} else {
// 按名称导入
PIMAGE_IMPORT_BY_NAME ImportByName = (PIMAGE_IMPORT_BY_NAME)(
(ULONG_PTR)ImageBase + OriginalFirstThunk->u1.AddressOfData
);
FunctionAddress = GetProcAddress(hModule, (LPCSTR)ImportByName->Name);
}
// 将函数地址写入 IAT
*FirstThunk = (IMAGE_THUNK_DATA)FunctionAddress;
OriginalFirstThunk++;
FirstThunk++;
}
ImportDesc++;
}
return STATUS_SUCCESS;
}
5.6.4.5 执行跳转与入口点调用
设置线程上下文
线程创建时,内核设置初始上下文,将入口点地址放入 EIP:
c
VOID SetThreadEntryPoint(PETHREAD Thread, PIMAGE_NT_HEADERS NtHeaders, PVOID ImageBase) {
PCONTEXT Context = &Thread->Tcb.Context;
// 设置指令指针为入口点
Context->Eip = (ULONG_PTR)ImageBase + NtHeaders->OptionalHeader.AddressOfEntryPoint;
// 设置用户栈指针
Context->Esp = (ULONG_PTR)Thread->Teb.Reserved1[1]; // TEB 中的栈顶指针
// 设置标志寄存器(启用中断)
Context->EFlags = 0x202; // IF = 1
// 设置段寄存器
Context->SegCs = KGDT_R3_CODE;
Context->SegDs = KGDT_R3_DATA;
Context->SegEs = KGDT_R3_DATA;
Context->SegFs = KGDT_R3_TEB;
// 将上下文复制到内核栈
KeContextToKframes(Thread, Context);
}
执行流程
┌──────────────────────────────────────────────────────────────────────────┐
│ EXE 加载与执行流程 │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 打开文件 │
│ │ │
│ ▼ │
│ 2. 读取 DOS Header (offset 0) │
│ │ - 验证 "MZ" 签名 │
│ │ - 获取 e_lfanew │
│ ▼ │
│ 3. 读取 PE Header (offset = e_lfanew) │
│ │ - 验证 "PE\0\0" 签名 │
│ │ - 获取 FileHeader / OptionalHeader │
│ ▼ │
│ 4. 读取 Section Headers │
│ │ - 遍历每个节区 │
│ │ - 获取 VirtualAddress, SizeOfRawData, Characteristics │
│ ▼ │
│ 5. 预留地址空间 │
│ │ - 尝试 ImageBase │
│ │ - 如果失败,寻找可用地址 │
│ ▼ │
│ 6. 映射节区到内存 │
│ │ - 提交物理页面 │
│ │ - 读取文件内容 │
│ │ - 设置保护属性 │
│ ▼ │
│ 7. 应用重定位(如果需要) │
│ │ - 计算 Delta = 实际基地址 - 期望基地址 │
│ │ - 遍历重定位表 │
│ │ - 修改所有绝对地址引用 │
│ ▼ │
│ 8. 解析导入表 │
│ │ - 遍历每个 IMAGE_IMPORT_DESCRIPTOR │
│ │ - 加载依赖 DLL │
│ │ - 填充 IAT(导入地址表) │
│ ▼ │
│ 9. 设置线程上下文 │
│ │ - EIP = ImageBase + AddressOfEntryPoint │
│ │ - ESP = 用户栈顶 │
│ ▼ │
│ 10. 线程调度执行 │
│ │ - 调度器选择线程 │
│ │ - 执行从 EIP 开始 │
│ ▼ │
│ 11. 用户态初始化 (LdrInitializeThunk) │
│ │ - 加载剩余 DLL │
│ │ - 调用 DLL_PROCESS_ATTACH │
│ │ - 初始化 TLS │
│ ▼ │
│ 12. 调用入口点 │
│ │ - WinMainCRTStartup / mainCRTStartup │
│ │ - C 运行时初始化 │
│ │ - 调用 WinMain / main │
│ ▼ │
│ 13. 程序执行结束 │
│ │ - 返回值传递给 ExitProcess │
│ │ - 清理资源 │
│ ▼ │
│ 14. 进程退出 │
│ │
└──────────────────────────────────────────────────────────────────────────┘
5.6.4.6 DLL 调用与返回机制
函数调用过程
c
// 用户代码调用 DLL 函数
int result = MessageBoxA(hwnd, "Hello", "Title", MB_OK);
// 实际执行流程:
// 1. 代码中的调用指令
// call dword ptr [IAT_MessageBoxA]
//
// 2. IAT 中存储的是实际函数地址
// IAT_MessageBoxA = 0x77D50000 (MessageBoxA 在 user32.dll 中的地址)
//
// 3. 执行 call 指令,跳转到 MessageBoxA
// - 将返回地址压栈
// - 跳转到 user32.dll 中的 MessageBoxA 入口
//
// 4. MessageBoxA 执行完毕后
// - 清理栈帧
// - 执行 ret 指令,弹出返回地址
// - 返回到用户代码中 call 指令的下一条指令
调用约定
| 调用约定 | 参数传递 | 栈清理 | 返回值 |
|---|---|---|---|
| __cdecl | 栈(从右到左) | 调用者清理 | EAX |
| __stdcall | 栈(从右到左) | 被调用者清理 | EAX |
| __fastcall | ECX/EDX + 栈 | 被调用者清理 | EAX |
| __thiscall | ECX(this) + 栈 | 被调用者清理 | EAX |
返回机制
c
// 典型的函数返回流程
NTSTATUS SomeFunction(...) {
// 函数执行...
// 返回值放入 EAX
return STATUS_SUCCESS;
// 编译器生成:mov eax, 0
// retn X (X = 参数大小,由被调用者清理)
}
// ret 指令执行过程:
// 1. 从栈顶弹出返回地址
// 2. 将指令指针设置为返回地址
// 3. 如果是 retn X,还会调整 ESP(跳过参数)
// 4. 继续执行调用者的代码
源码位置:
- ntimage.h(file:///d:/reactos/sdk/include/ddk/ntimage.h) --- PE 结构定义
- mm/section.c(file:///d:/reactos/ntoskrnl/mm/section.c) --- Section 对象创建
- mm/memory.c(file:///d:/reactos/ntoskrnl/mm/memory.c) --- 内存映射
- ldr/ldrinit.c(file:///d:/reactos/dll/ntdll/ldr/ldrinit.c) --- 用户态加载器初始化
5.6.5 用户态的初始化
LdrInitializeThunk:用户态初始化入口
当线程第一次在用户态执行时,会从 LdrInitializeThunk 开始,完成进程的用户态初始化。
用户态初始化流程:
┌──────────────────────────────────────────────────────────────────────────────────┐
│ 用户态初始化流程 │
│ │
│ 阶段 1: LDR 初始化 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 1. LdrInitializeThunk │ │
│ │ - 设置 PEB->Ldr 指向 PEB_LDR_DATA │ │
│ │ - 初始化 LDR 链表 │ │
│ │ 2. LdrpInitialize │ │
│ │ - 初始化 NTDLL 的加载信息 │ │
│ │ - 设置加载顺序 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 阶段 2: DLL 加载 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 1. LdrLoadDll 加载依赖 DLL │ │
│ │ - 遍历导入表 │ │
│ │ - 对每个 DLL 调用 LoadLibrary │ │
│ │ - 解析导入函数地址 │ │
│ │ 2. LdrpMapDllIntoMemory │ │
│ │ - 映射 DLL 到地址空间 │ │
│ │ - 处理重定位 │ │
│ │ - 填充 IAT │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 阶段 3: 初始化回调 │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 1. LdrpRunInitializeRoutines │ │ │
│ │ - 调用每个 DLL 的 DLL_PROCESS_ATTACH 回调 │ │ │
│ │ 2. LdrpInitializeTls │ │ │
│ │ - 初始化 TLS 槽 │ │ │
│ │ - 调用 TLS 初始化回调 │ │ │
│ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │
│ ▼ │
│ 阶段 4: 入口点执行 │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 1. LdrpCallInitRoutine │ │ │
│ │ - 调用程序的入口点 │ │ │
│ │ - WinMainCRTStartup / mainCRTStartup │ │ │
│ │ 2. C 运行时初始化 │ │ │
│ │ - 初始化堆 │ │ │
│ │ - 初始化全局变量 │ │ │
│ │ - 设置命令行参数 │ │ │
│ │ 3. 调用用户的 WinMain / main │ │ │
│ └─────────────────────────────────────────────────────────────┘ │ │
└──────────────────────────────────────────────────────────────────────────────────┘
DLL 加载顺序:
| 顺序 | DLL | 说明 |
|---|---|---|
| 1 | ntdll.dll | 内核 API 的包装层,最先加载 |
| 2 | kernel32.dll | 基础 Win32 API |
| 3 | user32.dll | 用户界面 API(如果是 GUI 程序) |
| 4 | gdi32.dll | 图形设备接口(如果需要) |
| 5 | 其他依赖 DLL | 按依赖关系加载 |
PEB_LDR_DATA 结构:
c
typedef struct _PEB_LDR_DATA {
ULONG Length;
BOOLEAN Initialized;
PVOID SsHandle;
LIST_ENTRY InLoadOrderModuleList; // 按加载顺序排列
LIST_ENTRY InMemoryOrderModuleList; // 按内存位置排列
LIST_ENTRY InInitializationOrderModuleList; // 按初始化顺序排列
} PEB_LDR_DATA;
5.6.5 进程创建的完整时序图
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 用户程序 │ │ kernel32 │ │ ntdll │ │ ntoskrnl │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │ │
│ CreateProcess() │ │ │
│────────────────────>│ │ │
│ │ │ │
│ │ 解析参数/打开文件 │ │
│ │ │ │
│ │ NtCreateProcess() │ │
│ │────────────────────>│ │
│ │ │ syscall │
│ │ │────────────────────>│
│ │ │ │
│ │ │ │ PspCreateProcess
│ │ │ │ 创建EPROCESS
│ │ │ │ 创建地址空间
│ │ │ │ 映射Section
│ │ │ │ 创建PEB
│ │ │ STATUS_SUCCESS │
│ │ │<────────────────────│
│ │ 返回进程句柄 │ │
│ │<────────────────────│ │
│ │ │ │
│ │ NtCreateThread() │ │
│ │────────────────────>│ │
│ │ │ syscall │
│ │ │────────────────────>│
│ │ │ │
│ │ │ │ PspCreateThread
│ │ │ │ 创建ETHREAD
│ │ │ │ 创建栈
│ │ │ │ 设置上下文
│ │ │ │ 创建TEB
│ │ │ │ 加入就绪队列
│ │ │ STATUS_SUCCESS │
│ │ │<────────────────────│
│ │ 返回线程句柄 │ │
│ │<────────────────────│ │
│ │ │ │
│ │ NtResumeThread() │ │
│ │────────────────────>│ │
│ │ │ │
│ PROCESS_INFORMATION│ │ │
│<────────────────────│ │ │
│ │ │ │
│ │ │ │ 线程调度执行
│ │ │ │
│ │ │ LdrInitializeThunk│
│ │ │<────────────────────│
│ │ │ │
│ │ │ 加载DLL/初始化 │
│ │ │ │
│ │ │ 调用入口点 │
│ │ │────────────────────>│
│ │ │ │ 用户程序开始执行
5.6.6 关键设计决策
5.6.6.1 为什么需要两个系统调用(NtCreateProcess + NtCreateThread)?
答案:分离关注点和灵活性。
- 分离关注点:进程创建和线程创建是两个独立的操作
- 灵活性:允许创建没有线程的进程(如空壳进程)
- 扩展性:可以在一个进程中创建多个线程
- 安全性:分开权限检查,进程创建和线程创建需要不同的权限
5.6.6.2 为什么用户态需要 LDR 初始化?
答案:内核不了解用户态的 DLL 依赖关系。
- 内核的职责:创建进程和线程,映射可执行文件
- 用户态的职责:解析导入表,加载依赖 DLL
- 模块化:将复杂的用户态初始化放在用户态执行
- 灵活性:允许用户态自定义加载行为(如延迟加载)
5.6.6.3 为什么入口点不是直接的 WinMain?
答案:需要 C 运行时初始化。
- C 运行时初始化:需要初始化堆、全局变量等
- 参数处理:解析命令行参数
- 异常处理:设置结构化异常处理链
- 国际化:处理 Unicode/ANSI 转换
5.6.7 小结
5.6.7.1 关键知识点
| 主题 | 关键点 |
|---|---|
| CreateProcess | 用户态创建进程的 API |
| PspCreateProcess | 内核态创建进程的核心函数 |
| PspCreateThread | 内核态创建线程的核心函数 |
| LdrInitializeThunk | 用户态初始化入口 |
| PEB_LDR_DATA | 模块加载信息 |
| DLL_PROCESS_ATTACH | DLL 初始化回调 |
5.6.7.2 设计原则
- 分层设计:用户态准备 → 内核态创建 → 用户态初始化
- 分离关注点:进程创建和线程创建分开
- 模块化:每个组件负责自己的部分
- 灵活性:支持各种创建选项和标志
5.6.7.3 常见陷阱
- DLL 依赖缺失:程序无法启动,提示找不到 DLL
- 基地址冲突:DLL 无法加载到首选基地址
- 权限不足:创建进程或线程需要足够的权限
- 栈溢出:线程栈大小不足
5.6.7.4 后续学习路径
- 进程终止流程
- DLL 延迟加载
- 进程调试机制
- 进程间通信
5.6.8 用户双击程序的完整调用流程
当用户在资源管理器中双击一个可执行文件(如 notepad.exe)时,系统会经历以下完整的调用流程:
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ 用户双击 EXE 文件的完整调用流程图 │
├─────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ 用户操作 │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Explorer.exe│ [资源管理器检测到双击事件] │
│ │ (Shell32) │ │
│ └──────┬───────┘ │
│ │ │
│ │ 解析文件路径,确认是可执行文件 │
│ ▼ │
│ ┌──────────────┐ │
│ │ ShellExecute │ [shell32.dll] │
│ │ ExW() │ - 检查文件类型关联 │
│ └──────┬───────┘ - 获取可执行文件路径 │
│ │ │
│ │ 调用 CreateProcess 系列 API │
│ ▼ │
│ ┌──────────────┐ │
│ │CreateProcess │ [kernel32.dll] │
│ │ InternalW() │ │
│ └──────┬───────┘ │
│ │ │
│ ├─► CreateFile() // 打开可执行文件 │
│ │ │ │
│ │ ▼ │
│ │ ┌──────────────┐ │
│ │ │ NtCreateFile │ [ntdll.dll] → syscall → [ntoskrnl] │
│ │ └──────────────┘ │
│ │ │
│ ├─► NtCreateSection() // 创建 Section 对象 │
│ │ │ │
│ │ ▼ │
│ │ ┌──────────────┐ │
│ │ │ NtCreateSection│ [ntdll.dll] → syscall → [ntoskrnl/ob] │
│ │ └──────────────┘ │
│ │ │
│ └─► NtCreateProcess() // 创建进程 │
│ │ │
│ ▼ syscall │
│ ┌──────────────┐ │
│ │ NtCreateProc │ [ntoskrnl/ps] │
│ │ ess() │ │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ PspCreatePr │ 内核态进程创建核心 │
│ │ ocess() │ - 创建 EPROCESS 对象 │
│ └──────┬───────┘ - 创建地址空间 │
│ │ - 映射 PE 映像 │
│ ▼ - 创建 PEB │
│ ┌──────────────┐ │
│ │ NtCreateThr │ [ntoskrnl/ps] │
│ │ ead() │ │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ PspCreateTh │ 内核态线程创建核心 │
│ │ read() │ - 创建 ETHREAD 对象 │
│ └──────┬───────┘ - 分配内核栈/用户栈 │
│ │ - 设置线程上下文 (EIP=入口点) │
│ │ - 创建 TEB │
│ ▼ - 加入就绪队列 │
│ ┌──────────────┐ │
│ │ KeReadyThre │ [ntoskrnl/ke] │
│ │ ad() │ - 线程加入调度队列 │
│ └──────┬───────┘ │
│ │ │
│ ▼ 线程调度 │
│ ┌──────────────┐ │
│ │ LdrInitializ │ [ntdll.dll] 用户态初始化 │
│ │ eThunk() │ - 初始化 LDR │
│ └──────┬───────┘ - 加载依赖 DLL │
│ │ - 调用 DLL_PROCESS_ATTACH │
│ ▼ - 初始化 TLS │
│ ┌──────────────┐ │
│ │ WinMainCRTS │ [msvcrt.dll] C 运行时初始化 │
│ │ tartup() │ - 初始化堆 │
│ └──────┬───────┘ - 初始化全局变量 │
│ │ - 设置命令行参数 │
│ ▼ │
│ ┌──────────────┐ │
│ │ WinMain() │ 用户程序入口点 │
│ │ / main() │ - 程序开始执行 │
│ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────┘
5.6.8.1 详细阶段说明
阶段 1:用户交互层
- 用户双击文件图标
- Explorer.exe 检测到鼠标事件
- Shell32.dll 的 ShellExecuteExW 处理文件关联
阶段 2:用户态准备层
- kernel32.dll 的 CreateProcessInternalW
- 打开可执行文件(CreateFile)
- 创建 Section 对象(NtCreateSection)
阶段 3:内核态创建层
- NtCreateProcess → PspCreateProcess
- 创建 EPROCESS 对象
- 创建进程地址空间
- 映射 PE 映像到内存
- 创建 PEB
- NtCreateThread → PspCreateThread
- 创建 ETHREAD 对象
- 分配内核栈和用户栈
- 设置线程上下文
- 创建 TEB
- 将线程加入就绪队列
阶段 4:用户态初始化层
- LdrInitializeThunk(ntdll.dll)
- 初始化 LDR 数据结构
- 解析导入表,加载依赖 DLL
- 调用各 DLL 的 DLL_PROCESS_ATTACH
- 初始化 TLS 槽
阶段 5:程序执行层
- WinMainCRTStartup / mainCRTStartup(C 运行时)
- 初始化堆(HeapInitialize)
- 初始化全局变量
- 设置命令行参数
- 用户程序入口点(WinMain / main)
- 程序正式开始执行
5.6.8.2 关键数据结构传递
| 阶段 | 关键数据结构 | 说明 |
|---|---|---|
| 文件打开 | HANDLE | 可执行文件句柄 |
| Section 创建 | SECTION_OBJECT | 内存映射对象 |
| 进程创建 | EPROCESS | 进程内核对象 |
| 地址空间 | PML4/EPT | 页表结构 |
| PEB 创建 | PEB | 进程环境块 |
| 线程创建 | ETHREAD | 线程内核对象 |
| TEB 创建 | TEB | 线程环境块 |
| 上下文设置 | CONTEXT | 线程初始寄存器状态 |
源码位置:dll/kernel32/client/proc.c(file:///d:/reactos/dll/kernel32/client/proc.c)、ntoskrnl/ps/process.c(file:///d:/reactos/ntoskrnl/ps/process.c)