VC++实战Windows进程监控:Psapi库核心API详解与健壮性编程
1. 项目概述为什么VC与Psapi库值得深挖在Windows平台做C开发尤其是涉及到系统监控、性能分析或者安全工具开发时你迟早会碰到一个需求如何获取当前系统里正在运行的进程、它们加载了哪些模块DLL、占用了多少内存这些信息操作系统都管理着但直接去内核里翻显然不现实。这时候Psapi.dllProcess Status API这个库就登场了。它就像是Windows系统进程信息的一个官方“查询接口”。你可能用过Taskmgr.exe任务管理器它那些漂亮的列表和图表底层数据很多就是通过Psapi库获取的。我们这次要做的就是用原汁原味的VC这里特指使用Visual C编译器工具链和Win32 API的C编程而非托管C或C/CLI来完整地实践一遍Psapi库的核心功能。这不仅仅是调用几个API那么简单它涉及到Win32编程的精髓内存管理、错误处理、结构体与缓冲区的配合以及对Windows进程模型的深入理解。网上关于Psapi的代码片段很多但往往七零八落只给个函数调用缺了关键的步骤和原理讲解。比如为什么调用EnumProcesses前要先分配缓冲区GetProcessMemoryInfo返回的那些内存计数器到底什么意思MODULEINFO里为什么有个EntryPoint这些细节不清不楚写出来的代码就不健壮也学不到东西。所以我打算用这篇长文结合我这些年调试系统工具的实际经验把Psapi库从环境配置、函数解析到实战封装彻底讲透让你能写出工业级可用的代码。2. 环境准备与Psapi库初探2.1 开发环境搭建与项目配置工欲善其事必先利其器。我们使用Visual Studio以VS 2019/2022为例进行开发。新建一个“Windows桌面向导”项目选择“空项目”语言为C。这里的关键在于项目属性的配置很多新手在这里栽跟头。首先要确保我们链接到正确的库。Psapi库有两个版本一个是最常见的Psapi.lib动态链接库Psapi.dll的导入库另一个是Psapi.lib对应的静态库较少用。对于大多数情况我们使用动态链接。在项目属性页“链接器” - “输入” - “附加依赖项”中添加Psapi.lib。你也可以在代码中用#pragma comment(lib, Psapi.lib)来指定但项目属性设置更清晰便于团队协作。注意从Windows 7和Windows Server 2008 R2开始Psapi库中的许多功能已被并入Kernel32.dll。为了获得最佳兼容性和性能微软建议直接链接Kernel32.lib并使用Kernel32.dll中的对应函数函数名通常相同如EnumProcesses。但为了清晰地学习Psapi库的完整体系并且确保在更早的系统如Windows XP上也能明确使用这些API我们依然显式链接Psapi.lib。在实际产品中你可以根据目标系统版本决定使用哪种方式。其次字符集设置。Psapi库的API大多有ANSIA后缀和UnicodeW后缀两个版本。在现代Windows编程中强烈建议使用Unicode宽字符版本。将项目属性中“高级” - “字符集”设置为“使用Unicode字符集”。这样编译器会默认使用EnumProcesses这样的函数名实际上是EnumProcessesW避免很多潜在的编码问题。2.2 Psapi库核心函数全景图Psapi库的函数可以大致分为几类我们这次实践会覆盖最主要的部分进程枚举与基本信息EnumProcesses,GetProcessImageFileName,GetProcessMemoryInfo。模块DLL信息枚举EnumProcessModules,GetModuleFileNameEx,GetModuleInformation。工作集信息GetProcessWorkingSetSize,EmptyWorkingSet这个要慎用。性能计数器GetPerformanceInfo这个函数从Psapi.dll2.0版本即Windows Vista开始提供。我们先从最基础的进程列表获取开始。很多教程一上来就让你调用EnumProcesses但很少告诉你背后的机制。这个函数的工作原理是向一个你提供的缓冲区一个DWORD数组填充所有进程的IDPID。这里有两个关键点一是缓冲区可能不够大二是函数返回的是写入的字节数而不是进程数量。#include windows.h #include psapi.h #include iostream #include vector int main() { DWORD aProcesses[1024], cbNeeded, cProcesses; // 第一次调用获取所需缓冲区大小 if (!EnumProcesses(aProcesses, sizeof(aProcesses), cbNeeded)) { std::cerr EnumProcesses failed. Error: GetLastError() std::endl; return 1; } // 计算实际返回的进程ID数量 cProcesses cbNeeded / sizeof(DWORD); std::cout Total processes enumerated: cProcesses std::endl; for (DWORD i 0; i cProcesses; i) { if (aProcesses[i] ! 0) { // PID 0是系统空闲进程通常跳过 std::wcout LProcess ID: aProcesses[i] std::endl; } } return 0; }这段代码有个潜在问题如果系统进程数超过1024个cbNeeded可能会等于传入的缓冲区大小这意味着数据可能被截断。一个健壮的实现应该动态分配缓冲区。3. 核心细节解析与健壮性编程实践3.1 动态缓冲区与进程句柄获取正确的做法是循环调用EnumProcesses直到返回值cbNeeded小于缓冲区大小。同时获取PID只是第一步我们通常需要进程名或其他信息这需要打开进程句柄。std::vectorDWORD GetProcessList() { std::vectorDWORD pids; DWORD bufferSize 256; // 初始大小 std::vectorDWORD buffer(bufferSize); while (true) { DWORD bytesReturned 0; if (!EnumProcesses(buffer.data(), static_castDWORD(buffer.size() * sizeof(DWORD)), bytesReturned)) { // 处理错误... break; } DWORD numPids bytesReturned / sizeof(DWORD); // 如果返回的字节数等于缓冲区大小说明缓冲区可能已满需要扩大 if (bytesReturned buffer.size() * sizeof(DWORD)) { bufferSize * 2; buffer.resize(bufferSize); } else { pids.assign(buffer.begin(), buffer.begin() numPids); break; } } // 移除PID为0的项系统空闲进程 pids.erase(std::remove(pids.begin(), pids.end(), 0), pids.end()); return pids; }有了PID要获取进程名需要OpenProcess获取句柄然后用GetProcessImageFileName或GetModuleFileNameEx。这里就涉及到进程访问权限这个深坑。OpenProcess需要特定的权限标志对于查询基本信息通常需要PROCESS_QUERY_INFORMATION或PROCESS_QUERY_LIMITED_INFORMATION后者权限更低更安全从Vista开始支持。对于枚举模块还需要PROCESS_VM_READ。std::wstring GetProcessName(DWORD pid) { std::wstring processName Lunknown; HANDLE hProcess OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid); if (hProcess ! NULL) { WCHAR szProcessName[MAX_PATH] {0}; // 方法1: GetProcessImageFileName - 返回设备路径如 \Device\HarddiskVolume1\Windows\System32\notepad.exe // 方法2: GetModuleFileNameEx - 返回DOS路径如 C:\Windows\System32\notepad.exe (更常用) if (GetModuleFileNameEx(hProcess, NULL, szProcessName, MAX_PATH) ! 0) { processName szProcessName; } else { // 如果GetModuleFileNameEx失败可能是权限不足或进程已退出可以尝试GetProcessImageFileName if (GetProcessImageFileName(hProcess, szProcessName, MAX_PATH) ! 0) { processName szProcessName; } } CloseHandle(hProcess); } else { DWORD err GetLastError(); // ERROR_ACCESS_DENIED 很常见对于系统进程或受保护进程我们可能没有权限打开。 // 可以尝试使用 PROCESS_QUERY_LIMITED_INFORMATION if (err ERROR_ACCESS_DENIED) { hProcess OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); if (hProcess ! NULL) { WCHAR szProcessName[MAX_PATH] {0}; if (GetModuleFileNameEx(hProcess, NULL, szProcessName, MAX_PATH) ! 0) { processName szProcessName; } CloseHandle(hProcess); } } } return processName; }实操心得在实际开发中你会遇到大量ERROR_ACCESS_DENIED错误码5。对于像csrss.exe,wininit.exe这样的关键系统进程即使用管理员权限也可能无法用PROCESS_QUERY_INFORMATION打开。PROCESS_QUERY_LIMITED_INFORMATION是更友好、更安全的选择它是在Vista之后引入的旨在减少不必要的权限暴露。如果你的工具需要兼容XP则需要进行运行时检测或提供降级方案。3.2 深入进程内存信息不仅仅是“占了多少M”GetProcessMemoryInfo函数返回一个PROCESS_MEMORY_COUNTERS或PROCESS_MEMORY_COUNTERS_EX结构体里面包含了丰富的内存使用指标。很多人只关心WorkingSetSize工作集大小即物理内存占用但其他指标对于深度分析更有价值。void PrintProcessMemoryInfo(DWORD pid) { HANDLE hProcess OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid); if (hProcess NULL) { return; } PROCESS_MEMORY_COUNTERS_EX pmc {0}; pmc.cb sizeof(pmc); // 这一步至关重要忘记设置cb大小会导致调用失败。 if (GetProcessMemoryInfo(hProcess, (PPROCESS_MEMORY_COUNTERS)pmc, sizeof(pmc))) { std::wcout L PageFaultCount: pmc.PageFaultCount std::endl; // 页面错误次数 std::wcout L PeakWorkingSetSize: pmc.PeakWorkingSetSize / 1024 L KB std::endl; // 峰值工作集 std::wcout L WorkingSetSize: pmc.WorkingSetSize / 1024 L KB std::endl; // 当前工作集物理内存 std::wcout L QuotaPeakPagedPoolUsage: pmc.QuotaPeakPagedPoolUsage / 1024 L KB std::endl; // 分页池峰值 std::wcout L QuotaPagedPoolUsage: pmc.QuotaPagedPoolUsage / 1024 L KB std::endl; // 分页池当前使用 std::wcout L QuotaPeakNonPagedPoolUsage: pmc.QuotaPeakNonPagedPoolUsage / 1024 L KB std::endl; // 非分页池峰值 std::wcout L QuotaNonPagedPoolUsage: pmc.QuotaNonPagedPoolUsage / 1024 L KB std::endl; // 非分页池当前使用 std::wcout L PagefileUsage: pmc.PagefileUsage / 1024 L KB std::endl; // 页面文件使用量提交内存 std::wcout L PeakPagefileUsage: pmc.PeakPagefileUsage / 1024 L KB std::endl; // 页面文件峰值 // PROCESS_MEMORY_COUNTERS_EX 独有的私有内存使用量 std::wcout L PrivateUsage: pmc.PrivateUsage / 1024 L KB std::endl; // 私有提交内存 } else { std::wcout L GetProcessMemoryInfo failed. Error: GetLastError() std::endl; } CloseHandle(hProcess); }这里需要解释几个关键概念工作集Working Set进程当前在物理内存中的部分。这是任务管理器“内存”列通常显示的值。提交内存Pagefile Usage / PrivateUsage进程承诺使用的虚拟内存总量包括物理内存中的和页面文件中的。这是任务管理器“提交大小”列的值。PrivateUsage特指进程私有的提交内存不包括共享的DLL等。分页池/非分页池内核模式的内存池。分页池可以交换到磁盘非分页池必须常驻物理内存。驱动程序和内核对象会使用它们。如果这里异常增长可能暗示有内核模式的内存泄漏。理解这些指标你就能写出比任务管理器更专业的监控工具。4. 模块枚举与信息获取实战4.1 枚举进程加载的所有模块一个进程不仅仅是一个EXE它还加载了众多DLL模块。EnumProcessModules函数类似于EnumProcesses也是填充一个缓冲区HMODULE数组。这里同样需要注意缓冲区管理。std::vectorHMODULE GetProcessModules(HANDLE hProcess) { std::vectorHMODULE modules; DWORD bytesNeeded; // 第一次调用获取所需大小 if (!EnumProcessModules(hProcess, NULL, 0, bytesNeeded)) { return modules; // 可能进程已退出或无权限 } DWORD moduleCount bytesNeeded / sizeof(HMODULE); modules.resize(moduleCount); if (!EnumProcessModules(hProcess, modules.data(), bytesNeeded, bytesNeeded)) { modules.clear(); return modules; } return modules; }获取到模块句柄HMODULE实际上是模块在进程地址空间中的基地址后我们可以用GetModuleFileNameEx获取模块的完整路径用GetModuleInformation获取更详细的信息。4.2 解析模块详细信息基地址、入口点与大小GetModuleInformation填充一个MODULEINFO结构体包含三个关键字段lpBaseOfDll模块加载的基地址。同一个DLL在不同进程中的基地址可能不同由于ASLR地址空间布局随机化。SizeOfImage模块映像在内存中的大小以字节为单位。这有助于了解模块占用的虚拟地址空间。EntryPoint模块的入口点地址。注意对于DLL这是DllMain函数的地址对于EXE主模块这是main/WinMain的地址。这个信息在逆向分析或高级调试场景中很有用。void PrintModuleInfo(HANDLE hProcess, HMODULE hModule) { WCHAR modName[MAX_PATH] {0}; if (GetModuleFileNameEx(hProcess, hModule, modName, MAX_PATH) 0) { return; } MODULEINFO modInfo {0}; if (GetModuleInformation(hProcess, hModule, modInfo, sizeof(modInfo))) { std::wcout L Module: modName std::endl; std::wcout L Base Address: 0x std::hex modInfo.lpBaseOfDll std::dec std::endl; std::wcout L Image Size: modInfo.SizeOfImage / 1024 L KB std::endl; std::wcout L Entry Point: 0x std::hex modInfo.EntryPoint std::dec std::endl; } }5. 封装与实践构建一个简易的进程浏览器现在我们把上面的知识点串联起来构建一个简单的命令行进程浏览器输出类似任务管理器列表但信息更详细的内容。5.1 设计进程信息结构体与管理器类首先我们设计一个结构体来存放我们关心的进程信息并创建一个管理器类来封装所有操作。struct ProcessInfo { DWORD pid; std::wstring name; std::wstring fullPath; SIZE_T workingSetSizeKB; SIZE_T privateUsageKB; std::vectorstd::wstring modules; // 简单起见只存模块名 }; class ProcessExplorer { public: ProcessExplorer() default; std::vectorProcessInfo SnapshotProcesses(); void PrintSnapshot(const std::vectorProcessInfo procs); private: std::vectorDWORD EnumAllProcesses(); ProcessInfo GatherProcessInfo(DWORD pid); std::vectorstd::wstring EnumProcessModuleNames(HANDLE hProcess); };5.2 实现核心快照功能SnapshotProcesses方法是核心它遍历所有进程收集信息。这里有一个重要的设计决策是否要收集所有进程的所有模块信息对于系统进程或大型软件如Visual Studio、Chrome模块数量可能成百上千全部收集会非常慢。在我们的简易实现中可以选择只收集主模块信息或者限制模块收集的数量。std::vectorProcessInfo ProcessExplorer::SnapshotProcesses() { std::vectorProcessInfo snapshot; auto pids EnumAllProcesses(); // 使用之前实现的动态缓冲区版本 for (DWORD pid : pids) { ProcessInfo info GatherProcessInfo(pid); if (!info.name.empty()) { // 过滤掉无法打开的进程 snapshot.push_back(std::move(info)); } } return snapshot; } ProcessInfo ProcessExplorer::GatherProcessInfo(DWORD pid) { ProcessInfo info; info.pid pid; // 尝试用较低权限打开进程提高成功率 HANDLE hProcess OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_VM_READ, FALSE, pid); if (hProcess NULL) { return info; // 返回空信息 } WCHAR buffer[MAX_PATH] {0}; // 获取进程映像文件路径DOS路径 if (GetModuleFileNameEx(hProcess, NULL, buffer, MAX_PATH)) { info.fullPath buffer; // 从路径中提取进程名 size_t pos info.fullPath.find_last_of(L\\/); if (pos ! std::wstring::npos) { info.name info.fullPath.substr(pos 1); } else { info.name info.fullPath; } } // 获取内存信息 PROCESS_MEMORY_COUNTERS_EX pmc {0}; pmc.cb sizeof(pmc); if (GetProcessMemoryInfo(hProcess, (PPROCESS_MEMORY_COUNTERS)pmc, sizeof(pmc))) { info.workingSetSizeKB pmc.WorkingSetSize / 1024; info.privateUsageKB pmc.PrivateUsage / 1024; } // 可选枚举模块这里只枚举前10个避免耗时过长 info.modules EnumProcessModuleNames(hProcess); CloseHandle(hProcess); return info; } std::vectorstd::wstring ProcessExplorer::EnumProcessModuleNames(HANDLE hProcess) { std::vectorstd::wstring moduleNames; DWORD bytesNeeded; if (!EnumProcessModules(hProcess, NULL, 0, bytesNeeded)) { return moduleNames; } DWORD moduleCount bytesNeeded / sizeof(HMODULE); std::vectorHMODULE moduleHandles(moduleCount); if (!EnumProcessModules(hProcess, moduleHandles.data(), bytesNeeded, bytesNeeded)) { return moduleNames; } // 限制枚举数量只取前10个或主模块 const DWORD maxModulesToShow 10; WCHAR modName[MAX_PATH]; for (DWORD i 0; i moduleCount i maxModulesToShow; i) { if (GetModuleFileNameEx(hProcess, moduleHandles[i], modName, MAX_PATH)) { // 提取文件名 std::wstring fullPath(modName); size_t pos fullPath.find_last_of(L\\/); moduleNames.push_back(pos ! std::wstring::npos ? fullPath.substr(pos 1) : fullPath); } } return moduleNames; }5.3 输出与格式化展示最后实现一个打印函数将收集到的信息以清晰的格式输出到控制台。void ProcessExplorer::PrintSnapshot(const std::vectorProcessInfo procs) { std::wcout std::left std::setw(10) LPID std::setw(30) LProcess Name std::setw(15) LWS (KB) std::setw(15) LPrivate (KB) LMain Path std::endl; std::wcout std::wstring(80, L-) std::endl; for (const auto proc : procs) { std::wcout std::left std::setw(10) proc.pid std::setw(30) (proc.name.length() 28 ? proc.name.substr(0, 25) L... : proc.name) std::setw(15) proc.workingSetSizeKB std::setw(15) proc.privateUsageKB (proc.fullPath.length() 50 ? proc.fullPath.substr(0, 47) L... : proc.fullPath) std::endl; // 可选打印前几个模块 if (!proc.modules.empty()) { std::wcout L Loaded Modules: ; for (size_t i 0; i proc.modules.size() i 3; i) { // 只显示前3个 std::wcout proc.modules[i]; if (i proc.modules.size() - 1 i 2) std::wcout L, ; } if (proc.modules.size() 3) { std::wcout L, ...; } std::wcout std::endl; } } }在主函数中调用int main() { ProcessExplorer explorer; auto snapshot explorer.SnapshotProcesses(); explorer.PrintSnapshot(snapshot); return 0; }6. 高级话题与性能优化6.1 性能考量与快照一致性我们实现的SnapshotProcesses是顺序同步的打开一个进程收集信息关闭句柄再处理下一个。在进程数量多比如上百个且部分进程模块众多时这可能比较慢。一个优化思路是使用多线程并发收集。但这里有个陷阱系统状态是动态变化的。在你枚举进程列表调用EnumProcesses和之后逐个查询详细信息之间可能有进程结束或新建。多线程并发可能会加剧这种不一致性。对于大多数监控工具这种微小的时间差是可以接受的。如果要求强一致性快照Windows提供了CreateToolhelp32Snapshot函数系列在Kernel32.dll中tlhelp32.h头文件它可以创建一份进程、线程、模块或堆的静态快照。虽然Psapi库是更现代的推荐方式但ToolHelp快照在需要原子性视图时仍有其价值。你可以根据需求混合使用。6.2 处理64位与32位进程Wow64在64位Windows上运行32位进程Wow64进程时会有一些特殊情况。Psapi库的函数在默认情况下从64位程序查看32位进程或者反过来行为可能不一致。例如EnumProcessModules在64位进程中枚举32位目标进程的模块时默认只能枚举到64位的模块如ntdll.dll的64位版本而看不到进程实际加载的32位DLL。为了解决这个问题需要使用EnumProcessModulesEx函数并指定LIST_MODULES_ALL标志它可以枚举目标进程的所有模块包括32位和64位。同时在打开进程句柄时可能需要添加PROCESS_WOW64相关的访问权限实际上PROCESS_QUERY_INFORMATION等权限在Wow64场景下通常已足够关键是API的选择。// 在64位系统中枚举目标进程可能是32位的所有模块 std::vectorHMODULE GetAllProcessModulesEx(HANDLE hProcess) { std::vectorHMODULE modules; DWORD bytesNeeded; // 使用 EnumProcessModulesEx 并指定 LIST_MODULES_ALL if (!EnumProcessModulesEx(hProcess, NULL, 0, bytesNeeded, LIST_MODULES_ALL)) { return modules; } DWORD moduleCount bytesNeeded / sizeof(HMODULE); modules.resize(moduleCount); if (!EnumProcessModulesEx(hProcess, modules.data(), bytesNeeded, bytesNeeded, LIST_MODULES_ALL)) { modules.clear(); } return modules; }6.3 错误处理与日志记录工业级代码必须有完善的错误处理。每个Psapi函数调用后都应检查返回值并用GetLastError()获取错误码。FormatMessage函数可以将错误码转换为可读的消息。建议将错误处理封装成辅助函数。std::wstring GetLastErrorString(DWORD errorCode GetLastError()) { LPWSTR msgBuffer nullptr; DWORD msgLength FormatMessageW( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, errorCode, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPWSTR)msgBuffer, 0, NULL); std::wstring message; if (msgLength 0) { message.assign(msgBuffer, msgLength); LocalFree(msgBuffer); } else { message LUnknown error code: std::to_wstring(errorCode); } // 移除末尾的换行符 if (!message.empty() (message.back() L\n || message.back() L\r)) { message.pop_back(); } if (!message.empty() message.back() L\r) { message.pop_back(); } return message; } // 使用示例 if (!EnumProcesses(/*...*/)) { std::wcerr LEnumProcesses failed: GetLastErrorString() std::endl; }7. 常见问题与排查技巧实录在实际使用Psapi库的过程中我踩过不少坑这里总结几个最常见的问题和解决方法。7.1 “访问被拒绝” (ERROR_ACCESS_DENIED)这是最常见的问题尤其在尝试打开系统进程或受保护进程如Antimalware Service Executable时。排查步骤检查权限你的程序是否以管理员权限运行对于部分系统进程即使有管理员权限也可能无法用PROCESS_ALL_ACCESS或PROCESS_QUERY_INFORMATION打开。降低权限要求优先尝试使用PROCESS_QUERY_LIMITED_INFORMATION。这个权限从Vista开始支持能打开绝大多数进程用于信息查询。检查进程是否存在在调用OpenProcess前进程可能已经退出。可以先用GetExitCodeProcess需要另一个已有句柄或通过快照API确认进程状态但更简单的方法是直接OpenProcess并处理错误。启用调试权限如果你的工具需要最高权限通常用于安全或内核调试类工具可以在启动时启用SeDebugPrivilege。但这非常危险会绕过很多系统安全限制仅限专业管理工具在受控环境下使用。BOOL EnableDebugPrivilege() { HANDLE hToken; if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, hToken)) { return FALSE; } TOKEN_PRIVILEGES tp; LUID luid; if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, luid)) { CloseHandle(hToken); return FALSE; } tp.PrivilegeCount 1; tp.Privileges[0].Luid luid; tp.Privileges[0].Attributes SE_PRIVILEGE_ENABLED; BOOL result AdjustTokenPrivileges(hToken, FALSE, tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL); CloseHandle(hToken); return result (GetLastError() ERROR_SUCCESS); }7.2 缓冲区大小不足与数据截断EnumProcesses和EnumProcessModules都可能因为缓冲区太小而无法返回全部数据。函数通过cbNeeded参数告诉你需要多少字节。正确做法像我们之前实现的那样使用循环动态分配缓冲区直到bytesReturned bufferSize。切勿假设一个固定的最大值如1024总是够用。7.3 获取的路径是设备路径而非DOS路径GetProcessImageFileName返回的是NT设备路径如\Device\HarddiskVolume1\Windows\System32\notepad.exe而不是常见的驱动器号路径如C:\Windows\System32\notepad.exe。解决方案优先使用GetModuleFileNameEx它通常返回DOS路径。如果必须处理设备路径可以使用QueryDosDevice等函数进行转换但这比较复杂。对于显示给用户看GetModuleFileNameEx的结果更友好。7.4 在32位进程中枚举64位进程或反之的问题在32位操作系统上这不是问题。在64位系统上一个32位程序Wow64试图枚举64位进程的模块时可能会失败或得到不完整的信息。解决方案确保你的程序与目标进程的“位数”匹配或者使用EnumProcessModulesEx并传递LIST_MODULES_ALL标志。对于纯粹的64位系统信息收集工具建议直接编译为64位程序。7.5 内存信息数值异常巨大PROCESS_MEMORY_COUNTERS中的大小单位是字节。直接显示给用户可能是一个很长的数字。需要转换为KB、MB或GB。另外注意WorkingSetSize是物理内存使用量它可能因为内存共享如DLL的代码段而被多个进程重复计算。PrivateUsage更能反映进程独占的提交内存。显示技巧实现一个友好的格式化函数。std::wstring FormatMemorySize(SIZE_T bytes) { const wchar_t* units[] { LB, LKB, LMB, LGB, LTB }; int unitIndex 0; double size static_castdouble(bytes); while (size 1024.0 unitIndex 4) { // 最多到TB size / 1024.0; unitIndex; } std::wstringstream ss; ss std::fixed std::setprecision(2) size L units[unitIndex]; return ss.str(); }把这些坑都趟过一遍你写出来的Psapi相关代码就足够稳健可以应对生产环境中的各种复杂情况了。记住系统编程无小事每一个API调用背后的细节都值得推敲。

相关新闻

大语言模型可控生成技术实战与优化

大语言模型可控生成技术实战与优化

1. AI Agent可控生成的核心挑战 在大语言模型应用开发中,最让我头疼的就是输出的不可预测性。去年做一个客服机器人项目时,明明设计了详细的业务场景对话模板,但LLM时不时就会给出包含政治敏感隐喻的回复,或者突然开始用莎士比亚文…

2026/9/21 13:39:05 阅读更多 →
Claude Mythos 引发网络安全震动,Java 安全更新部署刻不容缓!

Claude Mythos 引发网络安全震动,Java 安全更新部署刻不容缓!

如今,AI 模型已具备发现并利用零日漏洞的能力。Java 有一套明确的安全更新交付策略,但你是否有清晰的部署策略呢?随着 AI、大语言模型(LLM)和检索增强生成(RAG)的迅速普及,IT 安全领…

2026/9/25 7:22:56 阅读更多 →
WechatDecrypt微信解密工具:3分钟掌握聊天记录恢复的终极方案

WechatDecrypt微信解密工具:3分钟掌握聊天记录恢复的终极方案

WechatDecrypt微信解密工具:3分钟掌握聊天记录恢复的终极方案 【免费下载链接】WechatDecrypt 微信消息解密工具 项目地址: https://gitcode.com/gh_mirrors/we/WechatDecrypt 在数字时代,微信聊天记录已成为我们重要的数字资产。然而&#xff0c…

2026/9/19 6:02:04 阅读更多 →

最新新闻

plannotator 架构决策记录(ADR)实践指南:从 ADR-0001 到 007 的决策治理体系

plannotator 架构决策记录(ADR)实践指南:从 ADR-0001 到 007 的决策治理体系

【免费下载链接】plannotator Annotate and review coding agent plans and code diffs visually, share with your team, send feedback to agents with one click. 项目地址: https://gitcode.com/gh_mirrors/pl/plannotator 点击查看 免费下载 导读 本文围绕 p…

2026/9/25 7:22:45 阅读更多 →
Origin主成分分析(PCA)完全指南:从数据标准化到得分图绘制

Origin主成分分析(PCA)完全指南:从数据标准化到得分图绘制

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/25 7:22:45 阅读更多 →
低功耗遥测终端机RTU选型指南:从功耗核算到Modbus RTU对接实战

低功耗遥测终端机RTU选型指南:从功耗核算到Modbus RTU对接实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/25 7:22:45 阅读更多 →
IIS日志中的布尔盲注分析实战:从闽盾杯到真实攻防

IIS日志中的布尔盲注分析实战:从闽盾杯到真实攻防

1. 这不是一道CTF题,而是一次真实攻防现场的复盘“网络安全日志分析-题集1-[闽盾杯 2021]日志分析”——光看标题,很多人会下意识划走:又一道CTF模拟题,无非是给点Apache日志、写个Python脚本、跑出flag完事。但我在福建某市网信办…

2026/9/25 7:22:45 阅读更多 →
Apache DataFusion 下载与发布物验证指南:Cargo 依赖引入、官方源码发布包与 GPG/SHA-512 校验

Apache DataFusion 下载与发布物验证指南:Cargo 依赖引入、官方源码发布包与 GPG/SHA-512 校验

大数据数据分析后端 【免费下载链接】datafusion Apache DataFusion SQL Query Engine 项目地址: https://gitcode.com/gh_mirrors/datafu/datafusion 点击查看 免费下载 Apache DataFusion 是使用 Rust 编写、以 Apache Arrow 为内存格式的可扩展查询引擎。无论是…

2026/9/25 7:22:45 阅读更多 →
Havoc Framework 实战指南:现代可塑化后渗透 C2 框架的架构、部署与配置全解析

Havoc Framework 实战指南:现代可塑化后渗透 C2 框架的架构、部署与配置全解析

网络安全 【免费下载链接】Havoc The Havoc Framework 项目地址: https://gitcode.com/gh_mirrors/ha/Havoc 点击查看 免费下载 导读:Havoc 是一个由 C5pider 创建的现代可塑(malleable)后渗透 C2(Command and Contro…

2026/9/25 7:21:45 阅读更多 →

日新闻

AI元人文:从工具使用到思维重构的深度探索

AI元人文:从工具使用到思维重构的深度探索

最近半年我一直在琢磨一件事:AI元人文到底是什么?说白了,就是“用元视角重新审视人与AI的关系”,也在“探索AI如何反向逼着我们发现自己的思考边界”。标题里的“元探索”,在我看就是一层套一层的追问——当你用AI解决…

2026/9/25 0:00:41 阅读更多 →
Python+CNN车牌识别实战:从数据预处理到模型训练与部署

Python+CNN车牌识别实战:从数据预处理到模型训练与部署

简介:基于Python与卷积神经网络的车牌识别项目,面向计算机视觉初学者及智能交通开发者,目标是帮助用户掌握从数据预处理、模型构建到实际部署的完整流程。压缩包共25个文件,包含jpg/png图像样本、py训练脚本、md说明文档、dat数据…

2026/9/25 0:00:41 阅读更多 →
Vim基础操作全攻略:保存退出、模式切换与高频命令实战

Vim基础操作全攻略:保存退出、模式切换与高频命令实战

1. 项目概述1.1 核心需求解析今天聊聊Vim。写这个题目的原因是:几乎每个后端开发者、运维人员、数据工程师某天都会遇到一个场景——深夜加班,服务器登录界面只有黑底白字,编辑器只有vi/vim,你必须在五分钟内完成一次配置修改并保…

2026/9/25 0:00:41 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/24 14:34:13 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/24 9:10:42 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/24 14:33:56 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/24 12:50:34 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/24 14:33:48 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/24 12:49:17 阅读更多 →