第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 GUI | GUI 线程响应消息 | 所有线程被挂起 | GUI 线程 >= 5 秒未检查消息队列 |
| 非 UWP CUI | 至少一个线程未挂起 | 所有线程被挂起 | 永远不会显示 |
| UWP | 处于前台 | 处于后台 | GUI 线程 >= 5 秒未检查消息队列 |
GUI 进程需要至少一个线程通过 GetMessage 或 PeekMessage 泵送消息。如果 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 Normal | 6 |
| Normal | 8 |
| Above Normal | 10 |
| High | 13 |
| Realtime | 24 |
默认值为 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)
进程创建流程如下:
- 内核打开可执行文件的映像(image),验证 PE 格式(文件扩展名不重要)
- 验证通过后,创建新的进程内核对象(process kernel object)和线程内核对象(thread kernel object)
- 内核将映像映射到新进程的地址空间,同时映射 NtDll.dll(最小化/Pico 进程除外)
- 内核通知 Windows 子系统进程(Csrss.exe)关于新进程和线程的创建
- 从内核角度看,进程已成功创建
- 第二阶段初始化在新进程上下文中由新创建的线程完成
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 搜索顺序
- 如果 DLL 名称是 Known DLL(注册表指定),首先搜索系统目录
- 可执行文件所在目录
- 进程的当前目录
- 系统目录(
GetSystemDirectory,如c:\windows\system32) - Windows 目录(
GetWindowsDirectory,如c:\Windows) - 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++ 启动函数:
| 开发者编写的 main | C/C++ 启动函数 | 场景 |
|---|---|---|
main | mainCRTStartup | 控制台应用,ASCII |
wmain | wmainCRTStartup | 控制台应用,Unicode |
WinMain | WinMainCRTStartup | GUI 应用,ASCII |
wWinMain | wWinMainCRTStartup | GUI 应用,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 释放。
关键函数:
BOOL SetEnvironmentVariable(LPCTSTR lpName, LPCTSTR lpValue);
DWORD GetEnvironmentVariable(LPCTSTR lpName, LPTSTR lpBuffer, DWORD nSize);GetEnvironmentVariable 返回值:返回拷贝的字符数;如果缓冲区太小,返回所需的变量长度;失败返回 0(变量不存在)。
DWORD ExpandEnvironmentStrings(LPCTSTR lpSrc, LPTSTR lpDst, DWORD nSize);ExpandEnvironmentStrings 将 %VariableName% 模式展开为实际值。
CreateProcess(进程创建)
CreateProcess 要求提供实际的可执行文件路径,而非文档路径。该函数使用 10 个参数:
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 会写入此缓冲区。使用字符串字面量(静态常量)会导致访问违规。应使用可修改的内存:
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_WINDOW | CUI 应用创建时不带控制台 |
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_CLASS | 4 |
BELOW_NORMAL_PRIORITY_CLASS | 6 |
NORMAL_PRIORITY_CLASS | 8 |
ABOVE_NORMAL_PRIORITY_CLASS | 10 |
HIGH_PRIORITY_CLASS | 13 |
REALTIME_PRIORITY_CLASS | 24 |
默认值为 Normal(8),除非创建者的优先级类为 Below Normal 或 Idle(此时子进程继承)。Realtime 需要管理员权限,否则退级到 High。
pEnvironment:可选的环境块指针。NULL = 复制父进程的环境。
pCurrentDirectory:设置子进程的当前目录。NULL = 子进程获得父进程的当前目录。
BOOL SetCurrentDirectory(PCTSTR pPathName);
DWORD GetCurrentDirectory(DWORD nBufferLength, LPTSTR lpBuffer);pStartupInfo:指向 STARTUPINFO 或 STARTUPINFOEX。最小用法:
STARTUPINFO si = { sizeof(si) };
CreateProcess(..., &si, ...);PROCESS_INFORMATION:成功创建后填充:
typedef struct _PROCESS_INFORMATION {
HANDLE hProcess;
HANDLE hThread;
DWORD dwProcessId;
DWORD dwThreadId;
} PROCESS_INFORMATION;提供进程和线程的 ID 以及打开的句柄(具有所有可能的访问权限,除非是受保护进程)。使用完毕后应关闭这些句柄。
STARTUPINFO 结构(简化):
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_USESHOWWINDOW、STARTF_USESIZE、STARTF_USEPOSITION、STARTF_USECOUNTCHARS、STARTF_USEFILLATTRIBUTE、STARTF_RUNFULLSCREEN、STARTF_FORCEONFEEDBACK、STARTF_FORCEOFFFEEDBACK、STARTF_USESTDHANDLES、STARTF_USEHOTKEY、STARTF_TITLEISLINKNAME、STARTF_TITLEISAPPID、STARTF_PREVENTPINNING、STARTF_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_BLUE | 0x01 | 文本色 |
FOREGROUND_GREEN | 0x02 | 文本色 |
FOREGROUND_RED | 0x04 | 文本色 |
FOREGROUND_INTENSITY | 0x08 | 文本色(增强) |
BACKGROUND_BLUE | 0x10 | 背景色 |
BACKGROUND_GREEN | 0x20 | 背景色 |
BACKGROUND_RED | 0x40 | 背景色 |
BACKGROUND_INTENSITY | 0x80 | 背景色(增强) |
wShowWindow(配合 STARTF_USESHOWWINDOW 使用):值以 SW_ 为前缀,作为 WinMain 的最后一个参数传入。Shell 快捷方式可以设置此项(常规、最小化、最大化)。
hStdInput、hStdOutput、hStdError(配合 STARTF_USESTDHANDLES 使用):标准 I/O 句柄。默认:输入来自键盘,输出到控制台。
等待进程终止:
::WaitForSingleObject(pi.hProcess, INFINITE); // 永久等待
// 或使用超时:
DWORD rv = ::WaitForSingleObject(pi.hProcess, 10000);进程可以通过 GetStartupInfo 检索自身的 STARTUPINFO。
句柄继承(Handle Inheritance)
当创建子进程时,父进程中可继承的句柄(inheritable handles)会被复制到子进程中,并具有相同的句柄值。子进程需要知道这些值(通常通过命令行传递)。
使句柄可继承的三种方式:
- 在创建对象时通过
SECURITY_ATTRIBUTES设置:
SECURITY_ATTRIBUTES sa = { sizeof(sa), TRUE };
HANDLE h = ::CreateEvent(&sa, FALSE, FALSE, nullptr);- 对已有句柄使用
SetHandleInformation:
::SetHandleInformation(h, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);- 大多数
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 获取:
WCHAR path[MAX_PATH];
::GetFullPathName(L"c:", MAX_PATH, path, nullptr);注意:不要在盘符冒号后追加反斜杠。将文件名追加到 "c:" 可获得组合后的路径。此函数不检查文件是否存在。
进程(和线程)属性(Process and Thread Attributes)
STARTUPINFOEX 扩展了 STARTUPINFO:
typedef struct _STARTUPINFOEX {
STARTUPINFO StartupInfo;
PPROC_THREAD_ATTRIBUTE_LIST pAttributeList;
} STARTUPINFOEX;属性列表使用步骤:
- 使用
InitializeProcThreadAttributeList分配和初始化(调用两次:第一次获取大小,第二次实际初始化) - 使用
UpdateProcThreadAttribute添加属性 - 设置
STARTUPINFOEX.pAttributeList - 调用
CreateProcess并传入EXTENDED_STARTUPINFO_PRESENT - 使用
DeleteProcThreadAttributeList清理 + 释放内存
表 3-9:已文档化的进程和线程属性:
| 属性 | 适用范围 | 最低版本 | 描述 |
|---|---|---|---|
PARENT_PROCESS | Process | Vista | 设置不同的父进程 |
HANDLE_LIST | Process | Vista | 子进程可继承的句柄列表 |
GROUP_AFFINITY | Thread | Win7 | 默认 CPU 组亲和性 |
PREFERRED_NODE | Process | Win7 | 首选 NUMA 节点 |
IDEAL_PROCESSOR | Thread | Win7 | 理想 CPU |
UMS_THREAD | Thread | Win7 | 用户模式调度上下文 |
MITIGATION_POLICY | Process | Win7 | 安全缓解策略 |
SECURITY_CAPABILITIES | Process | Win8 | AppContainer 安全能力 |
PROTECTION_LEVEL | Process | Win8 | 与创建者相同的保护级别 |
CHILD_PROCESS_POLICY | Process | Win10 | 新进程是否可以创建子进程 |
DESKTOP_APP_POLICY | Process | Win10 1703 | Desktop Bridge UWP 转换 |
设置不同父进程的示例:
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_INFORMATION、PROCESS_SET_LIMITED_INFORMATION、PROCESS_SUSPEND_RESUME、PROCESS_TERMINATE。只有由 Microsoft 签名且具有特定 EKU(Enhanced Key Usage,增强密钥用法)的可执行文件才能作为受保护进程运行。
PPL(Protected Processes Light,轻量级受保护进程)(8.1+):具有多个保护级别。更高级别可以完全访问更低级别,但反过来不行。第三方反恶意软件在获得适当签名后可以运行。
表 3-10:PPL 签名者(Signers):
| PPL 签名者 | 级别 | 描述 |
|---|---|---|
| WinSystem | 7 | 系统和基础进程 |
| WinTcb | 6 | 关键 Windows 组件,拒绝 PROCESS_TERMINATE |
| Windows | 5 | 处理敏感数据的重要 Windows 组件 |
| Lsa | 4 | Lsass.exe(如果配置为受保护模式) |
| Antimalware | 3 | 反恶意软件服务进程(包括第三方),拒绝 PROCESS_TERMINATE |
| CodeGen | 2 | .NET Native 代码生成 |
| Authenticode | 1 | 承载 DRM 内容 |
| None | 0 | 无效(无保护) |
受保护/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):
#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):
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)中的所有句柄。
四种终止条件:
- 进程中的所有线程退出或被终止
- 任何线程调用
ExitProcess - 外部调用
TerminateProcess(包括由未处理异常触发) - main 函数返回(触发 C/C++ 运行时调用
ExitProcess)
void ExitProcess(UINT exitCode);
BOOL GetExitCodeProcess(HANDLE hProcess, LPDWORD lpExitCode);
BOOL TerminateProcess(HANDLE hProcess, UINT uExitCode);STILL_ACTIVE(0x103)在进程仍在运行时返回。为可靠检测进程是否已终止,使用 WaitForSingleObject(hProcess, 0)。
ExitProcess 的有序关闭流程:
- 终止进程中的所有其他线程
- 以
DLL_PROCESS_DETACH调用所有 DLL 的DllMain - 终止调用线程(永远不会返回)
TerminateProcess 立即终止进程。DLL 的 DllMain 函数不会被调用——存在数据丢失的风险。应作为最后手段使用。
任务管理器"详细信息"选项卡的"结束任务"(End Task)调用 TerminateProcess。"进程"选项卡的"结束任务"则首先向主窗口发送 WM_CLOSE 消息。
枚举进程(Enumerating Processes)
EnumProcesses(PSAPI)
来自 <psapi.h>,仅提供进程 ID:
BOOL EnumProcesses(DWORD *pProcessIds, DWORD cb, DWORD *pBytesReturned);调用者分配缓冲区。如果 pBytesReturned == 缓冲区大小,缓冲区可能太小——使用更大的缓冲区重试。然后对每个 PID 使用 OpenProcess 打开进程,使用 PROCESS_QUERY_LIMITED_INFORMATION 获取基本信息。
获取进程信息的关键函数:
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 进行转换。
QueryFullProcessImageName 的 dwFlags 参数:0 表示普通路径,PROCESS_NAME_NATIVE(1)表示设备格式路径(\Device\HarddiskVolume3\...)。
访问限制:标准用户对许多进程会收到错误 5(ACCESS_DENIED)。具有调试权限(debug privilege)的管理员可获得更好的结果。启用调试权限:
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>,无需提权即可在标准用户级别工作。
HANDLE hSnapshot = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
// 失败返回 INVALID_HANDLE_VALUEPROCESSENTRY32 结构:
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;用法:
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。
BOOL WTSEnumerateProcesses(HANDLE hServer, DWORD Reserved, DWORD Version,
PWTS_PROCESS_INFO *ppProcessInfo, DWORD *pCount);对于本地机器使用 WTS_CURRENT_SERVER_HANDLE。Version 必须为 1。函数分配内存;调用者需通过 WTSFreeMemory 释放。
WTS_PROCESS_INFO 提供:SessionId、ProcessId、pProcessName、pUserSid。在标准用户级别,许多 SID 为 NULL。管理员提权可获取更完整的结果。
WTSEnumerateProcessesEx(Win7+):
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)
- 编写 MiniProcExp(GUI 或控制台版本)——小型 Process Explorer。使用 Toolhelp、WTS 或原生 API。对于无法直接获取的信息,使用适当的函数打开进程句柄以获取。
- 扩展功能:添加进程操作,如终止进程、更改优先级类。
- 根据自己的需要继续扩展。
总结(Summary)
进程是 "Windows 中最基本的构建块"(the most fundamental building block in Windows)。进程包含资源(如地址空间等),使线程能够执行代码。下一章将介绍作业(Job),用于将进程作为单元进行管理。