我的 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 账号上,讲述一些毫无实用信息的故事。
相似文章
Windows DLL加载器锁:Rust线程如何使JVM挂起
QuestDB工程师调试一个偶发的Windows挂起问题,该问题由涉及Windows DLL加载器锁、Rust线程本地存储销毁、JNI分离和JVM垃圾收集安全点机制的死锁引起。
如何告知Windows我正在写入二进制文件?
本文解释了Windows在操作系统层面并没有内置二进制与文本模式的概念;这种区别是由运行时库(如C运行时)处理的。它澄清了Windows将所有文件视为字节,内容转换必须手动执行或通过库来完成。
Mark-of-the-Web 与安装程序固定至站点
解释Windows中的Mark-of-the-Web(MoTW)机制如何被用来使安装程序根据其下载来源网站表现出不同行为,利用NTFS备用数据流。
理解在试图绕过规则时规则背后的原理
本文来自微软的《老东西》博客,解释了Windows内核回调函数最佳实践背后的原理,特别是为什么阻塞或等待工作项会违背其目的,并通过一个关于驱动程序导致系统挂起的警示故事来说明。
为Windows 3.1改造WM_COPYDATA消息
本文解释如何利用共享地址空间,将针对32位Windows引入的WM_COPYDATA消息改造到16位Windows 3.1上,使得16位程序间的通信无需任何更改。