Dependencies 源码解析①:PE 文件内存映射全景图——Process Hacker phlib 到底做了什么
【免费下载链接】DependenciesA rewrite of the old legacy software "depends.exe" in C# for Windows devs to troubleshoot dll load dependencies issues.项目地址: https://gitcode.com/gh_mirrors/de/Dependencies
Dependencies是一个用 C# 重写的经典 Windows 开发工具(旧版 depends.exe),帮助开发者快速排查PE 文件(.exe / .dll)的DLL 依赖加载问题——比如"缺少某个 DLL"、"位数不匹配"这类让新手抓狂的报错。它的解析引擎并非从零手搓,而是深度集成了 Process Hacker 项目中的phlib内存映射库。本文带你走一遍 PE 文件从磁盘到内存的完整链路,看懂 phlib 到底做了什么。
一、先建立全景图:一次 PE 解析的调用链
当你把一个 exe 拖进 Dependencies 时,数据是这样流动的:
C# 业务层 (DependenciesLib/FindPeModule.cs) │ 调用 PE 类的 Load() ▼ C++/CLI 桥接层 (ClrPhlib/src/managed/PE.cpp) │ 把托管字符串转成原生宽字符串 ▼ UnmanagedPE 封装 (ClrPhlib/src/unmanaged/UnmanagedPE.cpp) │ 持有 PH_MAPPED_IMAGE 结构体 ▼ Process Hacker phlib (third_party/phlib/mapimg.c) │ NtCreateSection + NtMapViewOfSection ▼ Windows 内存映射视图 —— PE 内容以"指针偏移"方式被读取理解这条链的关键是:phlib 把整个 PE 文件一次性映射进进程地址空间,之后所有解析都只是在内存视图里"按偏移取数据",不再碰磁盘。这就是它又快又稳的根本原因。
二、PE 文件如何进入内存:内存映射技术
1. 打开文件并创建内核 Section 对象
入口函数在 mapimg.c 的PhLoadMappedImage,它先调用PhMapViewOfEntireFile完成三件事:
- 以只读方式打开目标文件(允许其他进程同时读取);
- 调用
NtCreateSection基于文件创建 Section 对象; - 调用
NtMapViewOfSection把整个文件映射到当前进程地址空间,得到一个viewBase指针。
相关实现可精读 mapimg.c。这里有两个值得注意的细节:
- 权限按需申请:只读解析只申请
FILE_READ_ATTRIBUTES | FILE_READ_DATA,不会锁死文件; - 卸载同样直接:
PhUnloadMappedImage只是一句NtUnmapViewOfSection,立即释放内存并解除对文件的占用(见 mapimg.c)。
2. PhInitializeMappedImage:逐层"体检" PE 结构
拿到内存视图后,PhInitializeMappedImage 开始校验文件是否真的是一个合法 PE:
| 校验步骤 | 做什么 | 失败返回值 |
|---|---|---|
| DOS 头魔数 | 检查文件头是否为MZ | STATUS_INVALID_IMAGE_NOT_MZ |
| e_lfanew 偏移 | 必须非 0 且小于文件大小 | STATUS_INVALID_IMAGE_FORMAT |
| PE 签名 | 检查PE\0\0签名 | STATUS_INVALID_IMAGE_FORMAT |
| Optional Header 魔数 | 必须是 32 位或 64 位魔数 | STATUS_INVALID_IMAGE_FORMAT |
| 内存探测 | 用 SEH 异常探测指针访问越界 | 返回异常码 |
最后它把NtHeaders指针、节区表、可选头信息全部填进PH_MAPPED_IMAGE结构体——这个结构体就是后续所有解析的"锚点"。
💡为什么用 SEH 探测而不是先判断长度?因为 PE 文件可能被恶意构造或截断,光看声明的长度不可信,真正访问一下内存才能确认数据安全。
三、映射之后:phlib 提供的三大解析能力
PH_MAPPED_IMAGE就绪后,mapimg.h 中声明的一系列 API 就能"无磁盘 I/O"地读取 PE 的各个目录:
PhGetMappedImageImports/PhGetMappedImageDelayImports:解析导入表(含延迟加载导入),返回按 DLL 分组的PH_MAPPED_IMAGE_IMPORTS结构——这正是 Dependencies 界面里"导入的 DLL 列表"的数据来源;PhGetMappedImageExports:解析导出表,得到PH_MAPPED_IMAGE_EXPORTS,支持按名称或序号查函数地址;PhGetMappedImageResources:枚举资源段,用于提取嵌入的清单(manifest);PhCheckSumMappedImage:计算 PE 校验和,与文件头声明值比对,判断二进制是否被修改过。
四、ClrPhlib 桥接层:C# 如何拥抱 phlib
phlib 是纯 C 代码,而 Dependencies 上层是 C#。两者之间的翻译官是ClrPhlib项目(C++/CLI),核心结构分两层:
1. UnmanagedPE —— 原生封装层
UnmanagedPh.h 中定义的UnmanagedPE类直接持有PH_MAPPED_IMAGE、导入/延迟导入/导出等原生结构体。它的 LoadPE 方法 只做了两件事:确保旧映像已卸载,然后调用PhLoadMappedImage加载新文件。析构时自动卸载,杜绝内存泄漏。
2. PE 类 —— C# 可见的托管门面
PE.cpp 把上述原生能力翻译成 C# 友好的接口,公开类型在 ClrPhlib.h 中声明:
Load()/Unload():映射与卸载,并填充PeProperties(机器类型、映像基址、入口点、子系统、校验和等,见 InitProperties);GetImports()/GetExports():惰性解析 + 本地缓存,首次调用才解析,之后直接返回缓存列表;GetManifest():从资源段提取 UTF-8 嵌入清单,供 SxS(侧载)分析使用;IsWow64Dll()/GetProcessor():判断 32 位/64 位、x86/arm/arm64 架构——这正是"32 位进程装 64 位 DLL 失败"这类问题的判断依据。
一个容易被忽视的细节:NativeFile
NativeFile.h 是System.IO.File的部分重写,专门绕开 64 位系统中的WoW64 文件系统重定向(32 位程序访问 System32 会被悄悄转到 SysWOW64)。分析 DLL 依赖时这一点致命——它保证了 32 位 Dependencies 也能如实看到 64 位目录下的文件,还顺带实现了基于 bcrypt 的 SHA256 文件哈希。
五、源码导读:建议按这个顺序精读
| 顺序 | 文件 | 看点 |
|---|---|---|
| 1 | third_party/phlib/mapimg.c | 内存映射与 PE 校验的完整实现(约 1800 行) |
| 2 | third_party/phlib/include/mapimg.h | PH_MAPPED_IMAGE系列结构与全部 API 声明 |
| 3 | ClrPhlib/src/unmanaged/UnmanagedPE.cpp | 最薄的原生封装,理解桥接层的最佳起点 |
| 4 | ClrPhlib/src/managed/PE.cpp | 托管门面:缓存策略、清单提取、架构判断 |
| 5 | DependenciesLib/FindPeModule.cs | C# 业务层如何组合使用 PE 类做 DLL 搜索 |
| 6 | test/manifest-regress/ | 清单回归测试样例,含.dll.manifest实例 |
📌 后续文章会基于这条链路继续深入:ApiSet Schema 解析(Windows 8 之后 DLL 名称如何被虚拟化)与DLL 搜索顺序模拟(FindPeModule 是如何复刻加载器行为的)。
六、小结
用三句话记住本文:
- phlib 的核心思想是"先映射、后解析"——一次
NtCreateSection + NtMapViewOfSection把 PE 文件搬进内存视图,之后全是指针运算; - 健壮性靠 SEH 探测——不信任文件自报的头部长度,访问即校验,畸形 PE 不会让工具崩溃;
- 分层清晰——C 层(phlib)做重活,C++/CLI 层(ClrPhlib)做翻译,C# 层(DependenciesLib)做体验,各层通过
PH_MAPPED_IMAGE这一个结构体解耦。
掌握这条 PE 内存映射全景链路,你就拿到了读懂 Dependencies 整个解析引擎的地图。🗺️
【免费下载链接】DependenciesA rewrite of the old legacy software "depends.exe" in C# for Windows devs to troubleshoot dll load dependencies issues.项目地址: https://gitcode.com/gh_mirrors/de/Dependencies
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考