windows下用enumprocessmodules获取模块列表最直接可靠,需用getcurrentprocess()、动态分配缓冲区并链接psapi.lib;linux用/proc/self/maps解析,macos用_dyld_image_count();均无法获取延迟加载模块和调试符号文件。

Windows下用 EnumProcessModules 获取模块列表
在 Windows 平台,最直接的方式是调用 Psapi.dll 提供的 EnumProcessModules。它能枚举当前进程(或指定进程)已加载的模块句柄,配合 GetModuleFileNameEx 或 GetModuleBaseName 可提取路径和名称。
注意:必须启用 SE_DEBUG_NAME 权限才能查其他进程;查当前进程时一般无需提权,但链接时需显式导入 psapi.lib,且运行时依赖 psapi.dll(Win7+ 默认存在,XP 需单独部署)。
-
EnumProcessModules返回的是模块基地址(HMODULE),不是文件路径,别误以为拿到的就是路径字符串 - 传入缓冲区大小要用
sizeof(HMODULE)乘以数组长度,不是sizeof(void*)—— 在 64 位下二者相同,但语义错误易引发后续理解偏差 - 模块数量可能动态变化,一次调用未必拿全,建议先调一次获取所需缓冲区大小,再分配后重试
Linux/macOS 下读取 /proc/self/maps 或 _dyld_image_count
Linux 没有统一 API,常规做法是解析 /proc/self/maps —— 它按内存映射段列出所有加载区域,含可执行文件、共享库、堆、栈等。筛选出含 .so、.dll(极少)、或路径非 [stack]/[heap] 的行即可近似得到模块列表。
macOS 则用 dyld 提供的私有接口:_dyld_image_count() 和 _dyld_get_image_name(i),返回的是镜像路径(包括主程序、dylib、framework),不包含 dlopen 动态加载的模块(除非调用 NSAddImage 等注册过)。
-
/proc/self/maps中同一模块可能因不同权限(r-x/rw-)拆成多行,需合并判断起始地址和路径是否一致 - 某些模块(如 vDSO、vvar)无磁盘路径,显示为
[vdso],不应视为“加载的模块”参与业务逻辑 - macOS 上
_dyld_*函数声明在mach-o/dyld.h,但属于非 ABI 稳定接口,仅适合调试或工具类用途,生产环境慎用
跨平台封装要注意符号可见性与延迟加载
如果写跨平台模块枚举工具,别试图抽象出统一接口——Windows 的模块是 HMODULE,Linux/macOS 是路径字符串,语义层级不同。更实际的做法是定义一个结构体存路径 + 基址 + 大小,各平台分别填充。
特别注意:GCC/Clang 默认隐藏符号,若把枚举逻辑放在 .so 里且未加 __attribute__((visibility("default"))),外部调用会失败;MSVC 则需 __declspec(dllexport) 导出函数。
- Linux 下
dlopen(NULL, ...)返回的是主程序的句柄,但不能靠它反推所有模块——只反映 dlopen 显式加载的部分 - Windows 上
GetModuleHandle(NULL)返回主模块句柄,但无法遍历其余模块,必须用EnumProcessModules - 某些安全加固环境(如某些容器、EDR 注入)会 hook 或拦截这些 API,返回结果可能被过滤或篡改
常见误判:把内存映射段当模块,或漏掉延迟加载的 DLL
看到 /proc/self/maps 里一堆 libc-2.31.so 就认为是“加载模块”,其实可能是其内部 mmap 分配的匿名段;同理,Windows 上 VirtualQuery 扫出来的 MEM_IMAGE 区域也不全是模块——有些是资源段或延迟加载 stub。
真正“模块”的关键特征是:有可执行代码段(至少一个 PAGE_EXECUTE_READ 或类似权限)、有 PE/ELF 文件头、且被系统 loader 登记过。用户态代码没法 100% 精确还原 loader 内部状态,所以所有方案都是近似。
- Windows 延迟加载的 DLL(Delay-Load)在首次调用函数前不会出现在
EnumProcessModules结果中,得结合IMAGE_DELAY_IMPORT_DESCRIPTOR解析 PE 才能预判 - Linux 的
LD_PRELOAD库会出现在/proc/self/maps,但它属于主程序映射的一部分,是否算“模块”取决于你的定义边界 - 调试器附加时,某些模块可能被临时卸载或替换,此时枚举结果与正常运行时不一致
模块枚举本身不难,难的是定义清楚“什么是模块”以及“你要这个列表来干什么”。路径?基址?导出符号?还是仅用于崩溃 dump 关联?目标不同,实现方式和容错策略就完全不同。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











