Skip to content
Published at:

第3章:进程

进程基础(Process Fundamentals)

进程(Process)是 Windows 中最基本的管理和容纳对象(the basic management and containment object)。所有代码的执行都发生在某个进程的上下文中。

Windows 支持多种类型的进程:

  • 受保护进程(Protected Processes):Windows Vista 引入,用于 DRM 保护。即使管理员级别的进程也无法以侵入式方式访问其地址空间内存。
  • UWP 进程:Windows 8 起可用,承载 Windows 运行时(Windows Runtime),通常发布到 Microsoft Store。在 AppContainer 沙箱中执行。
  • PPL 进程(Protected Processes Light):Windows 8.1 起可用,以多个级别扩展保护。即使管理员进程也无法侵入式访问或终止它们。
  • 最小化进程(Minimal Processes):Windows 10 版本 1607 起可用。其地址空间缺少正常的映像和数据结构——"没有可执行文件被映射,也没有 DLL 存在"。
  • Pico 进程:建立在最小化进程之上,配有一个 Pico 提供者内核驱动(Pico provider kernel driver),用于拦截 Linux 系统调用并转换为等效的 Windows 系统调用。WSL 即基于此。

任务管理器的详细信息选项卡

任务管理器的"详细信息"选项卡(Details tab)展示了进程的关键属性:

名称(Name):通常为可执行文件名。没有可执行文件名的特殊进程包括:

  • System(PID 4):从内核角度看是最小化进程,代表所有内核空间活动
  • Secure System:仅在开启基于虚拟化的安全性(Virtualization-Based Security)时存在
  • Registry:自 Win10 1803(RS4)起存在,是一个最小化进程,作为注册表管理的"工作区域"
  • Memory Compression:自 Win10 1607 起存在,在地址空间中保存压缩内存,服务器上不可用
  • System Idle Process(PID 0):不是真正的进程
  • System Interrupts:不是真正的进程,度量中断和 DPC 时间

PID(Process ID,进程 ID):唯一的进程标识符,从 4 开始,为 4 的倍数。PID 在进程终止后会被重用。要获得真正唯一性,需将 PID 与进程启动时间组合。PID(以及线程 ID)来自一个特殊句柄表(handle table)中的句柄值。

状态(Status):有三种可能值:

进程类型运行中(Running)已挂起(Suspended)无响应(Not Responding)
非 UWP GUIGUI 线程响应消息所有线程被挂起GUI 线程 >= 5 秒未检查消息队列
非 UWP CUI至少一个线程未挂起所有线程被挂起永远不会显示
UWP处于前台处于后台GUI 线程 >= 5 秒未检查消息队列

GUI 进程需要至少一个线程通过 GetMessagePeekMessage 泵送消息。如果 5 秒以上没有调用,状态变为"无响应",窗口变灰,标题栏出现"(Not Responding)"。可能原因:线程被挂起、等待 I/O 超过 5 秒、或 CPU 密集工作超过 5 秒。

UWP 进程进入后台(如最小化)时会自动挂起。非 UWP 的纯控制台进程除非所有线程被挂起,否则始终显示"运行中"。

Windows API 没有直接的进程挂起函数——只有线程挂起函数(SuspendThread / ResumeThread)。原生 API 中存在 NtSuspendProcess(位于 NtDll.dll),对应的恢复函数为 NtResumeProcess

用户名(User Name):进程运行所用的用户,基于附加到进程的主令牌(primary token)。特殊内置用户包括 Local System(显示为"System")、Network Service 和 Local Service。

会话 ID(Session ID):系统进程/服务在会话 0(Session 0);交互式登录在会话 1 及以上。

CPU:仅以整数百分比显示。如需更高精度,使用 Process Explorer。

内存(Memory):默认列显示"内存(活动专用工作集)"(Win10 1903),或"内存(专用工作集)"(更早版本)。工作集(Working Set)指 RAM。专用工作集(Private Working Set)是仅该进程使用的 RAM。活动专用工作集在 UWP 进程挂起时归零。推荐列——提交大小(Commit Size)——默认不显示。Process Explorer 中对应的列是"Private Bytes"。

基本优先级(Base Priority / Priority Class,优先级类):六种可能值:

优先级类基本优先级值
Idle(显示为"Low")4
Below Normal6
Normal8
Above Normal10
High13
Realtime24

默认值为 Normal(8)。

句柄(Handles):已打开的内核对象句柄数。

线程(Threads):通常至少 1 个。Secure System 不显示线程(调度由普通内核完成)。System Idle Process 的线程数等于逻辑处理器数。

Process Explorer 中的进程

Process Explorer 可被视为"超级任务管理器"。它提供颜色编码的进程显示。

表 3-2:Process Explorer 颜色含义:

名称(默认颜色)含义
New Objects(绿色)新创建的对象
Deleted Objects(红色)已销毁的对象
Own Processes(蓝色调)当前用户运行的进程
Services(粉色)承载 Windows 服务的进程
Suspended Processes(灰色)已挂起的进程
Packed Images(紫色)使用压缩的可执行文件/DLL(可能是恶意软件标志)
Relocated DLLs(浅黄色)仅模块视图,已重定位的 DLL
Jobs(棕色)属于某个作业(Job)的进程
.NET Processes(浅黄色)承载 .NET CLR 的进程
Immersive Processes(青色)通常为非挂起的 UWP 进程
Protected Processes(紫红色)受保护/PPL 进程

新建/已删除对象的颜色默认显示一秒钟。

Process Explorer 支持进程树视图(process tree view),通过点击"Process"列三次切换。子节点是其父进程的子进程。一些左对齐的进程(如 Explorer.exe)没有父进程或其父进程已退出。Explorer.exe 由 UserInit.exe 创建,而 UserInit.exe 在启动 Shell 后退出。重要概念:"如果进程 A 创建了进程 B,当进程 A 终止时,进程 B 不受影响。"进程之间更像兄弟关系,而非父子依赖。

进程创建(Process Creation)

进程创建流程如下:

  1. 内核打开可执行文件的映像(image),验证 PE 格式(文件扩展名不重要)
  2. 验证通过后,创建新的进程内核对象(process kernel object)和线程内核对象(thread kernel object)
  3. 内核将映像映射到新进程的地址空间,同时映射 NtDll.dll(最小化/Pico 进程除外)
  4. 内核通知 Windows 子系统进程(Csrss.exe)关于新进程和线程的创建
  5. 从内核角度看,进程已成功创建
  6. 第二阶段初始化在新进程上下文中由新创建的线程完成

NtDll 的角色:创建进程环境块 PEB(Process Environment Block)和线程环境块 TEB(Thread Environment Block)。这些结构在 <winternl.h> 中有部分文档。可通过 NtCurrentTeb() 获取当前线程的 TEB,通过 NtCurrentTeb()->ProcessEnvironmentBlock 获取当前进程的 PEB。

其他初始化工作包括:创建默认进程堆(default process heap)、默认进程线程池(default process thread pool)等。

加载器(Loader):通过检查可执行文件的导入节(import section)来加载所需的 DLL。通常包括 kernel32.dll、user32.dll、gdi32.dll、advapi32.dll。

DLL 搜索顺序

  1. 如果 DLL 名称是 Known DLL(注册表指定),首先搜索系统目录
  2. 可执行文件所在目录
  3. 进程的当前目录
  4. 系统目录(GetSystemDirectory,如 c:\windows\system32
  5. Windows 目录(GetWindowsDirectory,如 c:\Windows
  6. PATH 环境变量中的目录

已知 DLL(Known DLLs):注册表路径 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs 中列出的 DLL 始终从系统目录加载,以防止 DLL 劫持(DLL hijacking)。

API 集(API Sets)

API 集(例如 api-ms-win-core-libraryloader-l1-2-0.dll)提供从合约到实际实现 DLL 的间接层。自 Windows 7 起存在。映射关系存储在每个进程的 PEB 中。可使用 ApiSetMap.exe 工具查看映射关系。

main 函数(Main Functions)

C/C++ 应用程序的入口点因应用程序类型和字符集而异。

表 3-4:main 函数与 C/C++ 启动函数:

开发者编写的 mainC/C++ 启动函数场景
mainmainCRTStartup控制台应用,ASCII
wmainwmainCRTStartup控制台应用,Unicode
WinMainWinMainCRTStartupGUI 应用,ASCII
wWinMainwWinMainCRTStartupGUI 应用,Unicode

通过链接器的 /SUBSYSTEM 开关或 Visual Studio 项目属性设置。

main / wmain 参数

  • argc(>=1,第一个为可执行文件路径)
  • argv(已解析,以空格分隔的指针数组)

WinMain / wWinMain 参数

  • hInstance:进程地址空间中的可执行模块自身。HINSTANCE 和 HMODULE 在今天完全相同(在 16 位 Windows 中有所区别)。
  • hPrevInstance:始终为 NULL,未使用。源于与 16 位 Windows 的兼容性。
  • commandLine:可执行文件路径之后的所有内容,未解析。使用 <ShellApi.h> 中的 CommandLineToArgvW 进行解析。释放内存需使用 LocalFree
  • showCmd:建议主窗口的显示模式。默认值为 SW_SHOWDEFAULT(10)。可以忽略。

无论使用何种入口点,任何时候都可以通过 GetCommandLine() 获取命令行。

进程环境变量(Process Environment Variables)

环境变量(Environment Variables)是名称/值对,存储在注册表中:

  • 用户变量:HKEY_CURRENT_USER\Environment
  • 系统变量:HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment

进程从其父进程继承环境变量(系统变量 + 用户变量)。

控制台应用程序可以通过 main/wmain 的第三个参数 env[] 访问环境变量——这是一个以 null 结尾的 name=value 字符串数组。

GUI 应用程序使用 GetEnvironmentStrings,该函数返回一块内存,格式为:

name1=value1\0
name2=value2\0
\0

使用后必须通过 FreeEnvironmentStrings 释放。

关键函数

cpp
BOOL SetEnvironmentVariable(LPCTSTR lpName, LPCTSTR lpValue);
DWORD GetEnvironmentVariable(LPCTSTR lpName, LPTSTR lpBuffer, DWORD nSize);

GetEnvironmentVariable 返回值:返回拷贝的字符数;如果缓冲区太小,返回所需的变量长度;失败返回 0(变量不存在)。

cpp
DWORD ExpandEnvironmentStrings(LPCTSTR lpSrc, LPTSTR lpDst, DWORD nSize);

ExpandEnvironmentStrings%VariableName% 模式展开为实际值。

CreateProcess(进程创建)

CreateProcess 要求提供实际的可执行文件路径,而非文档路径。该函数使用 10 个参数:

cpp
BOOL CreateProcess(
    PCTSTR pApplicationName,
    PTSTR pCommandLine,
    PSECURITY_ATTRIBUTES pProcessAttributes,
    PSECURITY_ATTRIBUTES pThreadAttributes,
    BOOL bInheritHandles,
    DWORD dwCreationFlags,
    PVOID pEnvironment,
    PCTSTR pCurrentDirectory,
    PSTARTUPINFO pStartupInfo,
    PPROCESS_INFORMATION lpProcessInformation);

pApplicationName 和 pCommandLine:通常将第一个参数设为 NULL,所有内容放在第二个参数中。第二个参数在缺失 .EXE 扩展名时自动补全,并按照 DLL 搜索目录进行查找。注意 pCommandLine 的类型是 PTSTR(非常量)——CreateProcess 会写入此缓冲区。使用字符串字面量(静态常量)会导致访问违规。应使用可修改的内存:

cpp
WCHAR name[] = L"Notepad";
CreateProcess(nullptr, name, ...);

使用 CreateProcessA 不会出现此问题,但不应使用 ANSI 版本。

pProcessAttributes 和 pThreadAttributes:指向 SECURITY_ATTRIBUTES 的指针。除非需要返回的句柄可继承,否则传递 NULL。

bInheritHandles:全局开关。FALSE = 子进程不继承任何句柄。TRUE = 所有可继承的句柄都会被继承。

dwCreationFlags(表 3-5)

标志描述
CREATE_BREAKAWAY_FROM_JOB子进程不属于父进程的作业
CREATE_SUSPENDED线程创建后挂起;父进程可调用 ResumeThread 恢复
DEBUG_PROCESS父进程成为调试器,子进程及其子进程成为被调试者
DEBUG_ONLY_THIS_PROCESS仅直接子进程是被调试者
CREATE_NEW_CONSOLE新进程获得自己的控制台
CREATE_NO_WINDOWCUI 应用创建时不带控制台
DETACHED_PROCESS无控制台;稍后可调用 AllocConsole
CREATE_PROTECTED_PROCESS以受保护模式运行
CREATE_UNICODE_ENVIRONMENT环境块为 Unicode
INHERIT_PARENT_AFFINITY(Win7+)子进程继承父进程的组亲和性
EXTENDED_STARTUPINFO_PRESENT使用扩展的 STARTUPINFOEX 结构
CREATE_DEFAULT_ERROR_MODE使用系统默认错误模式

优先级类标志(表 3-6)

标志基本优先级
IDLE_PRIORITY_CLASS4
BELOW_NORMAL_PRIORITY_CLASS6
NORMAL_PRIORITY_CLASS8
ABOVE_NORMAL_PRIORITY_CLASS10
HIGH_PRIORITY_CLASS13
REALTIME_PRIORITY_CLASS24

默认值为 Normal(8),除非创建者的优先级类为 Below Normal 或 Idle(此时子进程继承)。Realtime 需要管理员权限,否则退级到 High。

pEnvironment:可选的环境块指针。NULL = 复制父进程的环境。

pCurrentDirectory:设置子进程的当前目录。NULL = 子进程获得父进程的当前目录。

cpp
BOOL SetCurrentDirectory(PCTSTR pPathName);
DWORD GetCurrentDirectory(DWORD nBufferLength, LPTSTR lpBuffer);

pStartupInfo:指向 STARTUPINFOSTARTUPINFOEX。最小用法:

cpp
STARTUPINFO si = { sizeof(si) };
CreateProcess(..., &si, ...);

PROCESS_INFORMATION:成功创建后填充:

cpp
typedef struct _PROCESS_INFORMATION {
    HANDLE hProcess;
    HANDLE hThread;
    DWORD dwProcessId;
    DWORD dwThreadId;
} PROCESS_INFORMATION;

提供进程和线程的 ID 以及打开的句柄(具有所有可能的访问权限,除非是受保护进程)。使用完毕后应关闭这些句柄。

STARTUPINFO 结构(简化):

cpp
typedef struct _STARTUPINFO {
    DWORD cb;
    PTSTR lpDesktop;
    PTSTR lpTitle;
    DWORD dwX, dwY;
    DWORD dwXSize, dwYSize;
    DWORD dwXCountChars, dwYCountChars;
    DWORD dwFillAttribute;
    DWORD dwFlags;
    WORD wShowWindow;
    HANDLE hStdInput;
    HANDLE hStdOutput;
    HANDLE hStdError;
} STARTUPINFO;

dwFlags 值(表 3-7)STARTF_USESHOWWINDOWSTARTF_USESIZESTARTF_USEPOSITIONSTARTF_USECOUNTCHARSSTARTF_USEFILLATTRIBUTESTARTF_RUNFULLSCREENSTARTF_FORCEONFEEDBACKSTARTF_FORCEOFFFEEDBACKSTARTF_USESTDHANDLESSTARTF_USEHOTKEYSTARTF_TITLEISLINKNAMESTARTF_TITLEISAPPIDSTARTF_PREVENTPINNINGSTARTF_UNTRUSTEDSOURCE

lpDesktop:指定备选的窗口站(Window Station)和桌面(Desktop)。NULL = 使用父进程的。格式:windowstation\desktop(如 winsta0\mydesktop)。

窗口站和桌面:窗口站是内核对象,包含剪贴板(clipboard)、原子表(atom table)和桌面。交互式窗口站是 WinSta0。默认登录会话有"Default"桌面和"Winlogon"桌面(在 Ctrl+Alt+Del 时使用)。可通过 CreateDesktop/OpenDesktop 函数管理桌面。

dwFillAttribute(表 3-8)

常量文本/背景
FOREGROUND_BLUE0x01文本色
FOREGROUND_GREEN0x02文本色
FOREGROUND_RED0x04文本色
FOREGROUND_INTENSITY0x08文本色(增强)
BACKGROUND_BLUE0x10背景色
BACKGROUND_GREEN0x20背景色
BACKGROUND_RED0x40背景色
BACKGROUND_INTENSITY0x80背景色(增强)

wShowWindow(配合 STARTF_USESHOWWINDOW 使用):值以 SW_ 为前缀,作为 WinMain 的最后一个参数传入。Shell 快捷方式可以设置此项(常规、最小化、最大化)。

hStdInput、hStdOutput、hStdError(配合 STARTF_USESTDHANDLES 使用):标准 I/O 句柄。默认:输入来自键盘,输出到控制台。

等待进程终止

cpp
::WaitForSingleObject(pi.hProcess, INFINITE);  // 永久等待
// 或使用超时:
DWORD rv = ::WaitForSingleObject(pi.hProcess, 10000);

进程可以通过 GetStartupInfo 检索自身的 STARTUPINFO

句柄继承(Handle Inheritance)

当创建子进程时,父进程中可继承的句柄(inheritable handles)会被复制到子进程中,并具有相同的句柄值。子进程需要知道这些值(通常通过命令行传递)。

使句柄可继承的三种方式

  1. 在创建对象时通过 SECURITY_ATTRIBUTES 设置:
cpp
SECURITY_ATTRIBUTES sa = { sizeof(sa), TRUE };
HANDLE h = ::CreateEvent(&sa, FALSE, FALSE, nullptr);
  1. 对已有句柄使用 SetHandleInformation
cpp
::SetHandleInformation(h, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
  1. 大多数 Open 函数(如 OpenEvent)的第二个参数可直接设为 TRUE。

InheritSharing 示例应用演示了此机制:一个共享内存句柄被设为可继承,通过命令行传递给同一可执行文件的子进程实例。子进程通过 GetCommandLine()CommandLineToArgvW 提取句柄值。

使用 Visual Studio 调试子进程:安装"Microsoft Child Process Debugging Power Tool"扩展。通过调试(Debug)-> 其他调试目标(Other Debug Targets)-> 子进程调试设置(Child Process Debugging Settings)启用。

进程驱动器目录(Process Drive Directory)

每个进程为每个驱动器维护各自的当前目录(Per-drive Current Directory),存储在环境变量中,形如 =C:=C:\Dev\Win10SysProg。使用 GetFullPathName 获取:

cpp
WCHAR path[MAX_PATH];
::GetFullPathName(L"c:", MAX_PATH, path, nullptr);

注意:不要在盘符冒号后追加反斜杠。将文件名追加到 "c:" 可获得组合后的路径。此函数不检查文件是否存在。

进程(和线程)属性(Process and Thread Attributes)

STARTUPINFOEX 扩展了 STARTUPINFO

cpp
typedef struct _STARTUPINFOEX {
    STARTUPINFO StartupInfo;
    PPROC_THREAD_ATTRIBUTE_LIST pAttributeList;
} STARTUPINFOEX;

属性列表使用步骤

  1. 使用 InitializeProcThreadAttributeList 分配和初始化(调用两次:第一次获取大小,第二次实际初始化)
  2. 使用 UpdateProcThreadAttribute 添加属性
  3. 设置 STARTUPINFOEX.pAttributeList
  4. 调用 CreateProcess 并传入 EXTENDED_STARTUPINFO_PRESENT
  5. 使用 DeleteProcThreadAttributeList 清理 + 释放内存

表 3-9:已文档化的进程和线程属性:

属性适用范围最低版本描述
PARENT_PROCESSProcessVista设置不同的父进程
HANDLE_LISTProcessVista子进程可继承的句柄列表
GROUP_AFFINITYThreadWin7默认 CPU 组亲和性
PREFERRED_NODEProcessWin7首选 NUMA 节点
IDEAL_PROCESSORThreadWin7理想 CPU
UMS_THREADThreadWin7用户模式调度上下文
MITIGATION_POLICYProcessWin7安全缓解策略
SECURITY_CAPABILITIESProcessWin8AppContainer 安全能力
PROTECTION_LEVELProcessWin8与创建者相同的保护级别
CHILD_PROCESS_POLICYProcessWin10新进程是否可以创建子进程
DESKTOP_APP_POLICYProcessWin10 1703Desktop Bridge UWP 转换

设置不同父进程的示例

cpp
HANDLE hParent = ::OpenProcess(PROCESS_CREATE_PROCESS, FALSE, parentPid);
// ... 构建属性列表 ...
::UpdateProcThreadAttribute(attlist, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
    &hParent, sizeof(hParent), nullptr, nullptr);
STARTUPINFOEX si = { sizeof(si) };
si.lpAttributeList = attlist;
::CreateProcess(nullptr, name, ...,
    EXTENDED_STARTUPINFO_PRESENT, ..., (STARTUPINFO*)&si, &pi);

对父进程句柄需要 PROCESS_CREATE_PROCESS 访问掩码(access mask)。

缓解策略示例PROCESS_CREATION_MITIGATION_POLICY_WIN32K_SYSTEM_CALL_DISABLE_ALWAYS_ON 阻止对 Win32k.sys 的系统调用,导致 Notepad 初始化失败。

受保护进程和 PPL 进程(Protected Processes and PPL)

受保护进程(Protected Processes)(Vista+):为 DRM 保护而设计。即使是管理员用户也只能获得有限的访问权限:PROCESS_QUERY_LIMITED_INFORMATIONPROCESS_SET_LIMITED_INFORMATIONPROCESS_SUSPEND_RESUMEPROCESS_TERMINATE。只有由 Microsoft 签名且具有特定 EKU(Enhanced Key Usage,增强密钥用法)的可执行文件才能作为受保护进程运行。

PPL(Protected Processes Light,轻量级受保护进程)(8.1+):具有多个保护级别。更高级别可以完全访问更低级别,但反过来不行。第三方反恶意软件在获得适当签名后可以运行。

表 3-10:PPL 签名者(Signers):

PPL 签名者级别描述
WinSystem7系统和基础进程
WinTcb6关键 Windows 组件,拒绝 PROCESS_TERMINATE
Windows5处理敏感数据的重要 Windows 组件
Lsa4Lsass.exe(如果配置为受保护模式)
Antimalware3反恶意软件服务进程(包括第三方),拒绝 PROCESS_TERMINATE
CodeGen2.NET Native 代码生成
Authenticode1承载 DRM 内容
None0无效(无保护)

受保护/PPL 进程无法加载任意 DLL——所有 DLL 必须经过适当签名。创建时使用 CREATE_PROTECTED_PROCESS 标志(要求可执行文件具有适当签名)。

UWP 进程(UWP Processes)

UWP 进程具有独特的属性:

  • 在 AppContainer 沙箱中运行
  • 由 Explorer.exe 中的进程生命期管理器 PLM(Process Lifetime Manager)管理
  • 应用包声明所需的能力(Capabilities,如相机、位置等)
  • 默认单实例(Win10 1803 起支持多实例)

无法使用标准 CreateProcess 创建 UWP 进程。需要通过未文档化的进程属性来设置包标识和完整包名。

MetroManager 示例应用演示了使用 CppWinRT 库(NuGet 包)进行 UWP 进程枚举和启动。

枚举 UWP 包(使用 CppWinRT):

cpp
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Management.Deployment.h>
#include <winrt/Windows.ApplicationModel.h>

auto packages = PackageManager().FindPackagesForUser(L"");
for (auto package : packages) {
    // 访问 package.Id().FullName(), package.InstalledLocation(), 等等
}

启动 UWP 应用(通过 COM 接口 IApplicationActivationManager):

cpp
CComPtr<IApplicationActivationManager> mgr;
mgr.CoCreateInstance(CLSID_ApplicationActivationManager);
mgr->ActivateApplication(appUserModelId, nullptr, AO_NOERRORUI, &pid);

UWP 进程在任务管理器中显示 Svchost.exe(DCOM Launch 服务)为其父进程。

最小化进程和 Pico 进程(Minimal Processes and Pico Processes)

**最小化进程(Minimal Processes)**仅包含一个用户模式地址空间(无映像文件和数据结构的映射)。例如:Memory Compression、Registry。只能由内核创建。

Pico 进程在最小化进程的基础上添加了 Pico 提供者内核驱动(Pico provider kernel driver)。该驱动拦截 Linux 系统调用并将其翻译为 Windows 系统调用。Pico 进程是 WSL(Windows Subsystem for Linux,Windows 的 Linux 子系统) 的基础,自 Win10 1607 / WinServer 2016 起可用。

进程终止(Process Termination)

无论采用何种终止方式,内核都会确保清理:释放所有私有内存(private memory),关闭进程句柄表(process handle table)中的所有句柄。

四种终止条件

  1. 进程中的所有线程退出或被终止
  2. 任何线程调用 ExitProcess
  3. 外部调用 TerminateProcess(包括由未处理异常触发)
  4. main 函数返回(触发 C/C++ 运行时调用 ExitProcess
cpp
void ExitProcess(UINT exitCode);
BOOL GetExitCodeProcess(HANDLE hProcess, LPDWORD lpExitCode);
BOOL TerminateProcess(HANDLE hProcess, UINT uExitCode);

STILL_ACTIVE(0x103)在进程仍在运行时返回。为可靠检测进程是否已终止,使用 WaitForSingleObject(hProcess, 0)

ExitProcess 的有序关闭流程

  1. 终止进程中的所有其他线程
  2. DLL_PROCESS_DETACH 调用所有 DLL 的 DllMain
  3. 终止调用线程(永远不会返回)

TerminateProcess 立即终止进程。DLL 的 DllMain 函数不会被调用——存在数据丢失的风险。应作为最后手段使用。

任务管理器"详细信息"选项卡的"结束任务"(End Task)调用 TerminateProcess。"进程"选项卡的"结束任务"则首先向主窗口发送 WM_CLOSE 消息。

枚举进程(Enumerating Processes)

EnumProcesses(PSAPI)

来自 <psapi.h>,仅提供进程 ID:

cpp
BOOL EnumProcesses(DWORD *pProcessIds, DWORD cb, DWORD *pBytesReturned);

调用者分配缓冲区。如果 pBytesReturned == 缓冲区大小,缓冲区可能太小——使用更大的缓冲区重试。然后对每个 PID 使用 OpenProcess 打开进程,使用 PROCESS_QUERY_LIMITED_INFORMATION 获取基本信息。

获取进程信息的关键函数

cpp
BOOL GetProcessTimes(HANDLE hProcess, LPFILETIME lpCreationTime,
    LPFILETIME lpExitTime, LPFILETIME lpKernelTime, LPFILETIME lpUserTime);

BOOL QueryFullProcessImageName(HANDLE hProcess, DWORD dwFlags,
    LPTSTR lpExeName, PDWORD lpdwSize);

FILETIME 是一个 64 位值(分为两个 32 位),以 100 纳秒为单位,自 1601 年 1 月 1 日 UTC 起算。使用 FileTimeToSystemTime 进行转换。

QueryFullProcessImageNamedwFlags 参数:0 表示普通路径,PROCESS_NAME_NATIVE(1)表示设备格式路径(\Device\HarddiskVolume3\...)。

访问限制:标准用户对许多进程会收到错误 5(ACCESS_DENIED)。具有调试权限(debug privilege)的管理员可获得更好的结果。启用调试权限:

cpp
bool EnableDebugPrivilege() {
    wil::unique_handle hToken;
    OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, ...);
    // LookupPrivilegeValue + AdjustTokenPrivileges with SE_DEBUG_NAME
}

即使拥有调试权限,特殊进程(System PID 4、Secure System、Registry)也返回"Unknown"映像名称——它们没有常规的可执行文件名。

Toolhelp 函数

来自 <tlhelp32.h>,无需提权即可在标准用户级别工作。

cpp
HANDLE hSnapshot = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
// 失败返回 INVALID_HANDLE_VALUE

PROCESSENTRY32 结构:

cpp
typedef struct tagPROCESSENTRY32 {
    DWORD dwSize;
    DWORD cntUsage;             // 未使用
    DWORD th32ProcessID;        // PID
    ULONG_PTR th32DefaultHeapID; // 未使用
    DWORD th32ModuleID;         // 未使用
    DWORD cntThreads;           // 线程数
    DWORD th32ParentProcessID;  // PPID(父进程 ID)
    LONG pcPriClassBase;        // 优先级类基本值
    DWORD dwFlags;              // 未使用
    TCHAR szExeFile[MAX_PATH];  // 可执行文件路径
} PROCESSENTRY32;

用法:

cpp
PROCESSENTRY32 pe = { sizeof(pe) };
Process32First(hSnapshot, &pe);
do {
    /* 处理 pe */
} while (Process32Next(hSnapshot, &pe));
CloseHandle(hSnapshot);

输出包括特殊进程名称,如 PID 0 显示为 [System Process],PID 4 显示为 System,PID 88 显示为 Secure System,PID 152 显示为 Registry

WTS 函数(Windows Terminal Services)

来自 <wtsapi32.h>,需链接 wtsapi32.lib

cpp
BOOL WTSEnumerateProcesses(HANDLE hServer, DWORD Reserved, DWORD Version,
    PWTS_PROCESS_INFO *ppProcessInfo, DWORD *pCount);

对于本地机器使用 WTS_CURRENT_SERVER_HANDLEVersion 必须为 1。函数分配内存;调用者需通过 WTSFreeMemory 释放。

WTS_PROCESS_INFO 提供:SessionId、ProcessId、pProcessName、pUserSid。在标准用户级别,许多 SID 为 NULL。管理员提权可获取更完整的结果。

WTSEnumerateProcessesEx(Win7+):

cpp
BOOL WTSEnumerateProcessesEx(HANDLE hServer, DWORD *pLevel,
    DWORD SessionID, PTSTR *pProcessInfo, DWORD *pCount);

pLevel 为 0 或 1。Level 1 返回 WTS_PROCESS_INFO_EX,包含:SessionId、ProcessId、pProcessName、pUserSid、NumberOfThreads、HandleCount、PagefileUsage、PeakPagefileUsage、WorkingSetSize、PeakWorkingSetSize、UserTime、KernelTime。

使用 WTS_ANY_SESSION 枚举所有会话。通过 WTSFreeMemoryEx(WTSTypeProcessInfoLevel1, info, count) 释放内存。

注意:内存相关字段是 DWORD(32 位),因此即使 64 位进程中,超过 4GB 的值也会报告不正确。

原生 API(Native API)

位于 NtDll.dll 中的函数,大多未文档化或仅有部分文档。在 <winternl.h> 中有定义。

NtQuerySystemInformation 配合 SystemProcessInformation(值为 5)返回 SYSTEM_PROCESS_INFORMATION 结构。任务管理器和 Process Explorer 正是使用此函数。

官方文档有限。Process Hacker 作者维护的 phnt 项目(https://github.com/processhacker/phnt)提供了最完整的定义。

文档化结构中有许多 "reserved" 字段。完整(未文档化)结构包括:NextEntryOffset、NumberOfThreads、WorkingSetPrivateSize、HardFaultCount、CreateTime、UserTime、KernelTime、ImageName、BasePriority、UniqueProcessId、InheritedFromUniqueProcessId、HandleCount、SessionId、VirtualSize、WorkingSetSize、PagefileUsage、PrivatePageCount,以及 I/O 计数器和线程信息。

建议:"如果有官方 API 可用,使用它比依赖未文档化的原生 API 更安全。"

练习(Exercises)

  1. 编写 MiniProcExp(GUI 或控制台版本)——小型 Process Explorer。使用 Toolhelp、WTS 或原生 API。对于无法直接获取的信息,使用适当的函数打开进程句柄以获取。
  2. 扩展功能:添加进程操作,如终止进程、更改优先级类。
  3. 根据自己的需要继续扩展。

总结(Summary)

进程是 "Windows 中最基本的构建块"(the most fundamental building block in Windows)。进程包含资源(如地址空间等),使线程能够执行代码。下一章将介绍作业(Job),用于将进程作为单元进行管理。