整数无故发生神秘变化,而本应无代码生成影响
摘要
一位开发者发现,交换两个等效宏竟导致无关函数中出现意外的整数变化,这篇博客文章深入探究了这一谜团,并对某个大语言模型(LLM)关于控制流保护的解释提出了质疑。
<p>一位同事进行了一些代码更改,按理说不应影响生成的二进制文件。具体来说,他将 <a href="https://github.com/tpn/winsdk-10/blob/9b69fd26ac0c7d0b83d378dba01080e93349c2ed/Include/10.0.16299.0/km/ndis.h#L1153"><code>NDIS_<wbr />STRING_<wbr />CONST</code> 宏</a> 替换为了 <a href="https://github.com/tpn/winsdk-10/blob/9b69fd26ac0c7d0b83d378dba01080e93349c2ed/Include/10.0.14393.0/shared/ntdef.h#L2037">更类型安全的 <code>RTL_<wbr />CONSTANT_<wbr />STRING</code> 宏</a>。这两个宏最终产生相同的结果,因此预期不会对二进制文件造成任何影响。</p>
<p>但他们发现二进制文件发生了变化。</p>
<p>具体来说,有四个函数发生了变化,而最奇怪的是,它们没有一个与宏变更直接相关。其中三个函数位于同一个源文件中,第四个甚至来自一个完全没有被修改的源文件!</p>
<p>变化情况如下:</p>
<table class="cp3" style="border-collapse: collapse;" border="1" cellspacing="0" cellpadding="3">
<tbody>
<tr>
<th>之前</th>
<th>之后</th>
</tr>
<tr>
<td colspan="2" align="center"><tt>contoso!EvtWdfWidgetContextCleanup</tt></td>
</tr>
<tr>
<td><tt>mov rax, [contoso!WdfFunctions_01031]</tt><br />
<tt>lea rcx, [??_C@__0DK@MPBCIIPN@...]</tt><br />
<tt>mov [rsp+20h], rcx</tt><br />
<tt>mov r9d, <span style="border: solid 1px currentcolor;">62Bh</span></tt><br />
<tt>mov r8d, 52467443h</tt><br />
<tt>mov rcx, [contoso!WdfDriverGlobals]</tt><br />
<tt>mov rdx, rbx</tt><br />
<tt>mov rax, [rax+670h]</tt><br />
<tt>call __guard_dispatch_call</tt></td>
<td><tt>mov rax, [contoso!WdfFunctions_01031]</tt><br />
<tt>lea rcx, [??_C@__0DK@MPBCIIPN@...]</tt><br />
<tt>mov [rsp+20h], rcx</tt><br />
<tt>mov r9d, <span style="border: solid 1px currentcolor;">62Ah</span></tt><br />
<tt>mov r8d, 52467443h</tt><br />
<tt>mov rcx, [contoso!WdfDriverGlobals]</tt><br />
<tt>mov rdx, rbx</tt><br />
<tt>mov rax, [rax+670h]</tt><br />
<tt>call __guard_dispatch_call</tt></td>
</tr>
<tr>
<td colspan="2" align="center"><tt>contoso!Function2</tt></td>
</tr>
<tr>
<td><tt>mov rax, [contoso!WdfFunctions_01031]</tt><br />
<tt>lea rcx, [??_C@__0DK@MPBCIIPN@...]</tt><br />
<tt>mov [rsp+20h], rcx</tt><br />
<tt>mov r9d, <span style="border: solid 1px currentcolor;">616h</span></tt><br />
<tt>mov rcx, [contoso!WdfDriverGlobals]</tt><br />
<tt>mov r8d, 52467443h</tt><br />
<tt>mov rdx, rdi</tt><br />
<tt>mov rax, [rax+668h]</tt><br />
<tt>call __guard_dispatch_call</tt></td>
<td><tt>mov rax, [contoso!WdfFunctions_01031]</tt><br />
<tt>lea rcx, [??_C@__0DK@MPBCIIPN@...]</tt><br />
<tt>mov [rsp+20h], rcx</tt><br />
<tt>mov r9d, <span style="border: solid 1px currentcolor;">615h</span></tt><br />
<tt>mov rcx, [contoso!WdfDriverGlobals]</tt><br />
<tt>mov r8d, 52467443h</tt><br />
<tt>mov rdx, rdi</tt><br />
<tt>mov rax, [rax+668h]</tt><br />
<tt>call __guard_dispatch_call</tt></td>
</tr>
<tr>
<td colspan="2" align="center"><tt>contoso!Function3</tt></td>
</tr>
<tr>
<td><tt>mov rax, [contoso!WdfFunctions_01031]</tt><br />
<tt>lea rcx, [??_C@__0DK@MPBCIIPN@...]</tt><br />
<tt>mov [r11-20h], rcx</tt><br />
<tt>xor r8d, r8d</tt><br />
<tt>mov rcx, [contoso!WdfDriverGlobals]</tt><br />
<tt>mov r9d, <span style="border: solid 1px currentcolor;">35Dh</span></tt><br />
<tt>mov rax, [rax+0DB0h]</tt><br />
<tt>call __guard_dispatch_call</tt></td>
<td><tt>mov rax, [contoso!WdfFunctions_01031]</tt><br />
<tt>lea rcx, [??_C@__0DK@MPBCIIPN@...]</tt><br />
<tt>mov [r11-20h], rcx</tt><br />
<tt>xor r8d, r8d</tt><br />
<tt>mov rcx, [contoso!WdfDriverGlobals]</tt><br />
<tt>mov r9d, <span style="border: solid 1px currentcolor;">35Ch</span></tt><br />
<tt>mov rax, [rax+0DB0h]</tt><br />
<tt>call __guard_dispatch_call</tt></td>
</tr>
<tr>
<td colspan="2" align="center"><tt>contoso!Function4</tt></td>
</tr>
<tr>
<td><tt>mov rax, [contoso!WdfFunctions_01031]</tt><br />
<tt>lea rcx, [??_C@__0DK@MPBCIIPN@...]</tt><br />
<tt>mov rdx, [rbp+8]</tt><br />
<tt>mov r9d, <span style="border: solid 1px currentcolor;">377h</span></tt><br />
<tt>mov [rsp+20h], rcx</tt><br />
<tt>mov r8d, 49507443h</tt><br />
<tt>mov rcx, [contoso!WdfDriverGlobals]</tt><br />
<tt>mov rax, [rax+0DB8h]</tt><br />
<tt>call __guard_dispatch_call</tt></td>
<td><tt>mov rax, [contoso!WdfFunctions_01031]</tt><br />
<tt>lea rcx, [??_C@__0DK@MPBCIIPN@...]</tt><br />
<tt>mov rdx, [rbp+8]</tt><br />
<tt>mov r9d, <span style="border: solid 1px currentcolor;">376h</span></tt><br />
<tt>mov [rsp+20h], rcx</tt><br />
<tt>mov r8d, 49507443h</tt><br />
<tt>mov rcx, [contoso!WdfDriverGlobals]</tt><br />
<tt>mov rax, [rax+0DB8h]</tt><br />
<tt>call __guard_dispatch_call</tt></td>
</tr>
</tbody>
</table>
<p>在所有情况下,变化都是某个整数减少了 1。</p>
<p>我的同事向 LLM 询问了此变化的原因,它建议这与控制流防护元数据有关。这合理吗?</p>
<p>我个人觉得不合理,原因有两点。首先,对于防护调度调用,控制流防护的唯一参数是 <tt>rax</tt> 寄存器,即被检查的函数。其他所有寄存器都包含被调用函数的参数。既然变化发生在 <tt>r9d</tt> 寄存器上,那么它们就与控制流防护无关。</p>
<p>其次,控制流防护元数据并不存储在代码中。它存储为<a title="运行时控制流防护检查崩溃时该怎么办" href="https://devblogs.microsoft.com/oldnewthing/20251029-00/?p=111738">二进制文件内部的一个数据块</a>。</p>
<p>那么我们看到了什么?</p>
<p>我仔细查看了 <code>EvtWdfWidgetContextCleanup</code>。</p>
<pre>void EvtWdfWidgetContextCleanup(_In_ WDFOBJECT Object)
{
auto widgetContext = GetContextFromWidgetHandle(Object);
if (widgetContext->NeedsDereference)
{
widgetContext->NeedsDereference = FALSE;
WdfObjectDereferenceWithTag(Object, CONTOSO_WIDGET_TAG);
}
}
</pre>
<p>编译器将 <code>WdfObjectDereferenceWithTag</code> 标记为发生变化的位置。我们看到它<a href="https://github.com/microsoft/Windows-Driver-Frameworks/blob/b6191d9543441329154da32f7ab9bdd97228dd3c/src/publicinc/wdf/kmdf/1.31/wdfobject.h#L848-L849">被定义为一个宏</a>:</p>
<pre>#define WdfObjectDereferenceWithTag(Handle, Tag) \
WdfObjectDereferenceActual(Handle, Tag, __LINE__, __FILE__)
</pre>
<p>而宏本身又<a href="https://github.com/microsoft/Windows-Driver-Frameworks/blob/b6191d9543441329154da32f7ab9bdd97228dd3c/src/publicinc/wdf/kmdf/1.31/wdfobject.h#L701-L715">是一个内联函数:</a></p>
<pre>_IRQL_requires_max_(DISPATCH_LEVEL)
VOID
FORCEINLINE
WdfObjectReferenceActual(
_In_
WDFOBJECT Handle,
_In_opt_
PVOID Tag,
_In_
LONG Line,
_In_z_
PCCH File
)
{
((PFN_WDFOBJECTREFERENCEACTUAL) WdfFunctions[WdfObjectReferenceActualTableIndex])
(WdfDriverGlobals, Handle, Tag, Line, File);
}
</pre>
<p>最后一个小细节是,<a href="https://github.com/microsoft/Windows-Driver-Frameworks/blob/b6191d9543441329154da32f7ab9bdd97228dd3c/src/publicinc/wdf/umdf/1.31/wdf.h#L60"><code>WdfFunctions</code> 是一个宏,展开后为 <code>WdfFunctions_01031</code></a>。WDF 头文件为每个版本分配了唯一的名称,这样版本不匹配会导致链接器错误,而不是运行时未定义行为。</p>
<p>现在我们可以看出这段代码如何对应到编译器输出。</p>
<pre> mov rax, [contoso!WdfFunctions_01031] ; WdfFunctions
lea rcx, [??_C@__0DK@MPBCIIPN@...] ; 某地址
mov [rsp+20h], rcx ; 是 File 参数
mov r9d, 62Bh ; Line 参数
mov r8d, 52467443h ; Tag 参数
mov rcx, [contoso!WdfDriverGlobals] ; 硬编码的参数
mov
查看缓存全文
缓存时间: 2026/07/10 19:38
# 整数不应因无效果的代码更改而神秘变化——旧事新说
来源:https://devblogs.microsoft.com/oldnewthing/20260710-00?p=112514
一位同事进行了一些本应不会对生成二进制文件产生任何影响的代码更改。具体来说,他们从 `NDIS_STRING_CONST` 宏(https://github.com/tpn/winsdk-10/blob/9b69fd26ac0c7d0b83d378dba01080e93349c2ed/Include/10.0.16299.0/km/ndis.h#L1153)迁移到了类型更安全的 `RTL_CONSTANT_STRING` 宏(https://github.com/tpn/winsdk-10/blob/9b69fd26ac0c7d0b83d378dba01080e93349c2ed/Include/10.0.14393.0/shared/ntdef.h#L2037)。这两个宏最终产生相同的结果,因此预期不会对二进制文件造成任何改变。但他们确实发现二进制文件发生了变化。具体来说,四个函数发生了变化,特别奇怪的是,这些函数都与宏更改无关。其中三个函数在同一个源文件中,而第四个函数所在的源文件根本没有被触及!变化如下所示:
| 修改前 | 修改后 |
|--------|--------|
| contoso!EvtWdfWidgetContextCleanup | |
| mov rax, [contoso!WdfFunctions_01031] | mov rax, [contoso!WdfFunctions_01031] |
| lea rcx, [??_C@__0DK@MPBCIIPN@...] | lea rcx, [??_C@__0DK@MPBCIIPN@...] |
| mov [rsp+20h], rcx | mov [rsp+20h], rcx |
| mov r9d,62Bh | mov r9d,62Ah |
| mov r8d, 52467443h | mov r8d, 52467443h |
| mov rcx, [contoso!WdfDriverGlobals] | mov rcx, [contoso!WdfDriverGlobals] |
| mov rdx, rbx | mov rdx, rbx |
| mov rax, [rax+670h] | mov rax, [rax+670h] |
| call __guard_dispatch_call | call __guard_dispatch_call |
| contoso!Function2 | |
| mov rax, [contoso!WdfFunctions_01031] | mov rax, [contoso!WdfFunctions_01031] |
| lea rcx, [??_C@__0DK@MPBCIIPN@...] | lea rcx, [??_C@__0DK@MPBCIIPN@...] |
| mov [rsp+20h], rcx | mov [rsp+20h], rcx |
| mov r9d,616h | mov r9d,615h |
| mov rcx, [contoso!WdfDriverGlobals] | mov rcx, [contoso!WdfDriverGlobals] |
| mov r8d, 52467443h | mov r8d, 52467443h |
| mov rdx, rdi | mov rdx, rdi |
| mov rax, [rax+668h] | mov rax, [rax+668h] |
| call __guard_dispatch_call | call __guard_dispatch_call |
| contoso!Function3 | |
| mov rax, [contoso!WdfFunctions_01031] | mov rax, [contoso!WdfFunctions_01031] |
| lea rcx, [??_C@__0DK@MPBCIIPN@...] | lea rcx, [??_C@__0DK@MPBCIIPN@...] |
| mov [r11-20h], rcx | mov [r11-20h], rcx |
| xor r8d, r8d | xor r8d, r8d |
| mov rcx, [contoso!WdfDriverGlobals] | mov rcx, [contoso!WdfDriverGlobals] |
| mov r9d,35Dh | mov r9d,35Ch |
| mov rax, [rax+0DB0h] | mov rax, [rax+0DB0h] |
| call __guard_dispatch_call | call __guard_dispatch_call |
| contoso!Function4 | |
| mov rax, [contoso!WdfFunctions_01031] | mov rax, [contoso!WdfFunctions_01031] |
| lea rcx, [??_C@__0DK@MPBCIIPN@...] | lea rcx, [??_C@__0DK@MPBCIIPN@...] |
| mov rdx, [rbp+8] | mov rdx, [rbp+8] |
| mov r9d,377h | mov r9d,376h |
| mov [rsp+20h], rcx | mov [rsp+20h], rcx |
| mov r8d, 49507443h | mov r8d, 49507443h |
| mov rcx, [contoso!WdfDriverGlobals] | mov rcx, [contoso!WdfDriverGlobals] |
| mov rax, [rax+0DB8h] | mov rax, [rax+0DB8h] |
| call __guard_dispatch_call | call __guard_dispatch_call |
在所有情况下,变化都是一个整数减少了一。我的同事请教了一个LLM来解释这个变化,它建议这些变化与控制流防护元数据有关。这合理吗?我认为不合理,原因有两点。首先,对于防护调度调用,控制流防护的唯一参数是 `rax` 寄存器,它是要检查的函数。所有其他寄存器都包含被调用函数的参数。由于更改的是 `r9d` 寄存器,它们与控制流防护无关。其次,控制流防护元数据并不存储在代码中。它作为数据块存储在二进制文件内(https://devblogs.microsoft.com/oldnewthing/20251029-00/?p=111738)。那么我们看到了什么?我查看了一下 `EvtWdfWidgetContextCleanup`。
```
void EvtWdfWidgetContextCleanup(_In_ WDFOBJECT Object) {
auto widgetContext = GetContextFromWidgetHandle(Object);
if (widgetContext->NeedsDereference) {
widgetContext->NeedsDereference = FALSE;
WdfObjectDereferenceWithTag(Object, CONTOSO_WIDGET_TAG);
}
}
```
编译器将 `WdfObjectDereferenceWithTag` 指向为变化的位置。我们看到它被定义为一个宏(https://github.com/microsoft/Windows-Driver-Frameworks/blob/b6191d9543441329154da32f7ab9bdd97228dd3c/src/publicinc/wdf/kmdf/1.31/wdfobject.h#L848-L849):
```
#define WdfObjectDereferenceWithTag(Handle, Tag) \
WdfObjectDereferenceActual(Handle, Tag, __LINE__, __FILE__)
```
它本身是一个内联函数(https://github.com/microsoft/Windows-Driver-Frameworks/blob/b6191d9543441329154da32f7ab9bdd97228dd3c/src/publicinc/wdf/kmdf/1.31/wdfobject.h#L701-L715):
```
_IRQL_requires_max_(DISPATCH_LEVEL)
VOID FORCEINLINE
WdfObjectReferenceActual(
_In_ WDFOBJECT Handle,
_In_opt_ PVOID Tag,
_In_ LONG Line,
_In_z_ PCCH File
)
{
((PFN_WDFOBJECTREFERENCEACTUAL) WdfFunctions[WdfObjectReferenceActualTableIndex])
(WdfDriverGlobals, Handle, Tag, Line, File);
}
```
最后一个细节是 `WdfFunctions` 是一个宏,展开为 `WdfFunctions_01031`(https://github.com/microsoft/Windows-Driver-Frameworks/blob/b6191d9543441329154da32f7ab9bdd97228dd3c/src/publicinc/wdf/umdf/1.31/wdf.h#L60)。WDF 头文件为每个版本赋予唯一名称,以便版本不匹配时产生链接器错误,而不是运行时未定义行为。
现在我们可以看到这段代码如何映射到编译器输出。
```
mov rax, [contoso!WdfFunctions_01031] ; WdfFunctions
lea rcx, [??_C@__0DK@MPBCIIPN@...] ; 某个地址
mov [rsp+20h], rcx ; 是 File 参数
mov r9d, 62Bh ; Line 参数
mov r8d, 52467443h ; Tag 参数
mov rcx, [contoso!WdfDriverGlobals] ; 硬编码参数
mov rdx, rbx ; Handle 参数
mov rax, [rax+670h] ; 加载函数指针
call __guard_dispatch_call ; 验证并调用
```
因此,变化的值是**行号**。我回到拉取请求,注意到它从源文件中删除了一行。
```
#include
#include "stringutils.h"
```
拉取请求的一部分包括删除不再需要的头文件,因为它包含了代码不再使用的私有 `NDIS_STRING_CONST` 宏。从源文件中删除一行会导致所有行号偏移一!所以他们看到的只是行号的变化。功能没有任何变化。如果他们真的希望这是一个“无二进制影响”的更改,可以将 `#include "stringutils.h"` 替换为注释或仅保留为空行。或者他们可以接受当你更改行时行号可能会改变。
**附加话题**:但等等,我说有三次更改在一个文件(删除了行的那个文件)中,但第四次在一个根本没有更改的文件中。这是怎么回事?第四个函数包含对修改文件中某个函数的调用,而链接时代码生成决定内联该调用。更改后的行号传播到内联函数中,导致在一个甚至不受拉取请求影响的文件中发生了代码生成变化。
1 回顾一下,在验证并调用模式(https://devblogs.microsoft.com/oldnewthing/20260708-00/?p=112510)中,函数指针通过 `rax` 寄存器传递,其他所有内容都设置得好像你要亲自调用该函数一样。
### 分类
### 主题
## 作者 Raymond Chen
Raymond 参与 Windows 的演进已有超过 30 年。2003 年,他创建了一个名为“The Old New Thing”的网站,其受欢迎程度远远超出了他最疯狂的想象,这一发展至今仍让他感到不安。该网站诞生了一本书,巧合的是也名为《The Old New Thing》(Addison Wesley 2007)。他偶尔出现在 Windows Dev Docs Twitter 账号上,讲述一些不传达有用信息的故事。
相似文章
Bug Archeology:借助LLM解开一个十年的Swift/C++谜题
一位开发者讲述了如何利用LLM解决一个Swift/C++跨平台音乐应用中存在十年的Bug,展示了AI如何协助调试复杂问题。
不是我,是编译器
一位Rust程序员发现了一个编译器错误,其中将'bool as u32'进行类型转换会产生不正确的结果,导致解析器错误。该错误已报告并链接到GitHub issue #158206。
Windows堆栈限制检查回顾,后续
Raymond Chen跟进了他之前关于ARM64堆栈限制检查的文章,指出了堆栈探测函数中x15寄存器的非常规使用细节,并比较了多个架构的寄存器使用。
Go语言中的L1指令缓存集冲突、关联性与代码对齐
深入探讨L1指令缓存集冲突和代码对齐如何导致Go语言中出现意外的性能回退,以及调查过程。
一个未正式卸载却从内存中消失的DLL案例,第二部分
Ray Chen的一篇技术博文,探究一个内存损坏bug:单个字节0x01破坏HMODULE句柄,导致DLL被错误释放,进而在进程终止时崩溃。