我的 HMODULE 的最低位置位意味着什么?
摘要
本文解释了 Windows 加载器通过设置 HMODULE 句柄的最低位来指示该 DLL 是作为数据文件加载的(LOAD_LIBRARY_AS_DATAFILE),这会影响资源查找和释放行为,且不会将其添加到模块列表中。
<p><code>HMODULE</code> 的数值通常是它代表的 DLL 或 EXE 的基址。这些基址<a title="为什么地址空间分配粒度为 64KB?" href="https://devblogs.microsoft.com/oldnewthing/20031008-00/?p=42223">始终是 64KB 的倍数</a>,因此低 16 位全为零。但你可能会遇到最低位被置为 1 的情况。这表示什么?</p>
<p>通常,当你加载一个 DLL 时,它会在已加载模块表中获得一个条目。像 <code>GetModuleHandle</code> 和 <code>EnumProcessModules</code> 这样的函数会查询该表,以识别所有已加载的 DLL。它还用于跟踪每个 DLL 被加载的次数,以便在调用正确次数的 <code>FreeLibrary</code> 后,从内存中移除该 DLL。</p>
<p><code>LoadLibraryEx</code> 函数的许多标志会改变系统定位要加载的 DLL 的方式,但其中一些标志会改变 DLL 本身加载到内存的方式。这里值得关注的是 <code>LOAD_<wbr />LIBRARY_<wbr />AS_<wbr />DATAFILE</code> 标志。</p>
<p>如果你要求将 DLL 作为数据文件加载,并且尚未正常加载该 DLL 的副本,那么加载器会按照其他标志描述的方式在文件系统中搜索该 DLL,然后仅将其映射到内存中,而不执行任何常规操作(如应用重定位),并返回一个 <code>HMODULE</code>,表示 DLL 映射到内存的位置,但同时设置最低位作为自我提示:“这不是以常规方式加载的。”</p>
<p>如果加载器决定直接将该 DLL 映射到内存,那么该 DLL 不会获得已加载模块列表中的条目。虽然从严格意义上讲模块已被加载,但它并非作为<em>功能性</em>模块加载的。代码尚未准备好执行:其依赖项未解析,初始化未运行。它只是一组映射到内存中的字节。如果你调用 <code>GetModuleHandle</code> 或 <code>EnumProcessModules</code>,该模块不会出现,因为这些函数使用“正确”加载的模块列表,而你的数据文件 DLL 并未放入该列表。</p>
<p>像 <code>FindResource</code> 这样的函数会识别这些“并非真正模块”的模块。例如,如果你要求在作为数据文件加载的模块中查找资源,<code>FindResource</code> 函数知道它必须将 PE 头中的 RVA 转换为物理文件偏移量。</p>
<p>当你将 <code>HMODULE</code> 传回 <code>FreeLibrary</code> 时,它会看到最低位已设置,并知道:“哦,这个从未进入模块列表,所以我也不需将其从模块列表中移除。”</p>
<p>这种最低位的特殊行为通过 <code>LoadLibraryEx</code> 文档中提供的宏固定在 ABI 中:</p>
<pre>#define LDR_IS_DATAFILE(handle) (((ULONG_PTR)(handle)) & (ULONG_PTR)1)
</pre>
<p>我不知道这种最低位的使用本意是作为实现细节,还是记录它是刻意的决定,但木已成舟,而且已记录在案,所以现在更改已来不及。</p>
<p><b>额外话题</b>:你可以在文档中看到另一个宏,它揭示了倒数第二位也被用作特殊信号:</p>
<pre>#define LDR_IS_IMAGEMAPPING(handle) (((ULONG_PTR)(handle)) & (ULONG_PTR)2)
</pre>
<p>本文首发于 <a href="https://devblogs.microsoft.com/oldnewthing">The Old New Thing</a>,原文标题为 <a href="https://devblogs.microsoft.com/oldnewthing/20260619-00/?p=112447">What does it mean when the bottom bit of my <CODE>HMODULE</CODE> is set?</a>。</p>
查看缓存全文
缓存时间: 2026/06/20 14:22
# 当我的HMODULE的最低位置位时,意味着什么? - The Old New Thing
来源:https://devblogs.microsoft.com/oldnewthing/20260619-00?p=112447
`HMODULE` 的数值通常是其代表的 DLL 或 EXE 的基地址。这些基地址总是 64KB(https://devblogs.microsoft.com/oldnewthing/20031008-00/?p=42223)的倍数,因此低 16 位全为零。然而,你可能会遇到最低位被置位的情况。这意味着什么?
通常,加载一个 DLL 时,它会在已加载模块表中获得一个条目。`GetModuleHandle` 和 `EnumProcessModules` 等函数会查询该表,以识别所有已加载的 DLL。它还用于跟踪每个 DLL 被加载的次数,以便在调用正确次数的 `FreeLibrary` 后,将 DLL 从内存中移除。
`LoadLibraryEx` 函数的许多标志会改变系统定位要加载的 DLL 的方式,但也有一些标志会改变 DLL 本身加载到内存中的方式。这里值得关注的是 `LOAD_LIBRARY_AS_DATAFILE` 标志。
如果你要求将 DLL 作为数据文件加载,并且该 DLL 尚未以正常方式加载,那么加载器会按照其他标志描述的方式在文件系统中搜索该 DLL,然后直接将其映射到内存中,而不执行任何常规操作(如应用重定位),最后返回一个 `HMODULE`,表示该 DLL 在内存中的映射位置。但同时,它还会设置最低位,作为给自己的一条备注:“这不是以常规方式加载的。”
如果加载器决定将 DLL 直接映射到内存中,那么该 DLL 不会进入已加载模块列表。虽然从严格意义上讲模块已被加载,但它并不是作为一个功能模块加载的。代码尚未准备好执行:依赖项未解析,初始化未运行。它只是一堆映射到内存中的字节。如果你调用 `GetModuleHandle` 或 `EnumProcessModules`,该模块不会出现,因为这些函数使用的是“正常”加载的模块列表,而你的数据文件 DLL 并未加入该列表。
像 `FindResource` 这样的函数会识别这些“并非真正模块”的模块。例如,如果你要求在已作为数据文件加载的模块中查找资源,`FindResource` 函数知道必须将 PE 头中的 RVA 转换为物理文件偏移。
当你将 `HMODULE` 传回 `FreeLibrary` 时,它看到最低位被置位,就会明白:“哦,这个从未加入过模块列表,所以我也不需要将它从模块列表中移除。”
最低位的这种特殊行为已通过 `LoadLibraryEx` 文档中提供的以下宏被锁定到 ABI 中:
```
#define LDR_IS_DATAFILE(handle) (((ULONG_PTR)(handle)) & (ULONG_PTR)1)
```
我不知道这种对最低位的使用是作为实现细节,还是将其记录下来是有意为之的决定,但木已成舟,既然已经记录在案,现在再改变就来不及了。
**额外话题**:你可以在文档中看到另一个宏,它揭示了倒数第二位也被用作特殊信号:
```
#define LDR_IS_IMAGEMAPPING(handle) (((ULONG_PTR)(handle)) & (ULONG_PTR)2)
```
### 分类
### 主题
## 作者
Raymond Chen
Raymond 参与 Windows 的演进已有 30 多年。2003 年,他创建了一个名为“The Old New Thing”的网站,其受欢迎程度远超他最疯狂的想象,这一发展至今仍让他感到不安。该网站还衍生出一本书,巧合的是书名也是 *The Old New Thing*(Addison Wesley 2007)。他偶尔会出现在 Windows Dev Docs 的 Twitter 账号上,讲述一些毫无实用信息的故事。
相似文章
一个未正式卸载却从内存中消失的DLL案例,第二部分
Ray Chen的一篇技术博文,探究一个内存损坏bug:单个字节0x01破坏HMODULE句柄,导致DLL被错误释放,进而在进程终止时崩溃。
一个DLL未正式卸载却从内存消失的案例,第一部分
本文调查了一个崩溃转储文件:进程关闭期间,shell32.dll中发生栈溢出,导致重复的异常处理循环。分析追踪了递归异常处理,并确定了涉及DLL卸载的根本原因。
我们如何确定 CcNamespace.dll 是一组过早卸载的 DLL 中的主谋?
Raymond Chen 解释了他是如何通过分析调试器的已卸载 DLL 列表,并注意命名模式和顺序,从而确定 CcNamespace.dll 是一串过早卸载的 DLL 中的关键 DLL。
关于线程从已卸载的第三方DLL中执行的问题
微软Raymond Chen的一则技术调试故事,讲述了Windows资源管理器崩溃由从已卸载第三方DLL中执行的线程引起,展示了开发者如何调查此类问题。
Windows中窗口和类额外字节的演变
本文追溯了Windows中窗口和类额外字节从16位到32位再到64位的演变,详细描述了随着句柄和指针的扩展,函数名称和数据大小的变化。