测验:猜猜这些 x86 汇编指令执行什么





5.00/5 (4投票s)
猜猜具有相同源操作数和目标操作数的汇编指令会做什么?
引言
今天的文章是一个有趣的测验,猜猜下面这 3 条 Intel x86 汇编指令到底做了什么。这些是由 Visual C++ 编译器生成的汇编指令。我在学习 Windows 调试器时,在代码列表里偶然发现了这些指令。在这篇文章里,我事先告诉你,你不会学到什么有用的东西,也不会成为更好的汇编程序员。它们只是有趣的 x86 知识。为了测验的目的,请假设它们是 32 位 (x86) 指令。
XOR EAX, EAX
我们的第一个问题,这条指令是对两个操作数进行按位异或 (bitwise exclusive OR) 操作,并将结果赋给目标操作数。对于 MASM 和 Intel 格式,左操作数是目标操作数,右操作数是源操作数。在 AT&T 格式中,它们是相反的。在我们的例子中,使用哪种格式并不重要,因为操作数是同一个寄存器。由于这篇文章基于 Visual C++,它使用 MASM,所以你可以假设是 MASM/Intel 格式。Intel 指令集要求其中一个操作数必须是寄存器类型,所以我们不能有两个内存操作数。
花点时间猜猜它实现了什么?为了帮助你猜测答案,这里是异或 (XOR) 的真值表。
答案:它将寄存器清零!Visual C++ 优化器生成此代码是因为此操作比将字面值零移动到寄存器更快。
TEST EBX, EBX
我们的第二个问题是 TEST
指令对两个操作数进行按位与 (bitwise AND) 操作。SF
, ZF
, PF
标志会被修改,而 AND
的结果会被丢弃。OF
和 CF
标志被设为 0
,而 AF
标志是未定义的。然后,会根据标志调用条件跳转指令。
猜猜它实现了什么?为了帮助你猜测答案,我为你呈现按位与 (AND) 的真值表。
答案:它在测试操作数是否为零。后面跟着 JZ
或 JNZ
。
MOV EDI, EDI
第三个问题是 MOV
指令将源操作数的值移动到目标操作数。当你谷歌搜索时,你会得到 许多关于这条无用指令的结果,它出现在 32 位 Windows 上每个 Windows API 的开头。猜猜它实现了什么?
答案:什么也没做。它用于热补丁 (hot patching)。在热补丁过程中,这条两条指令会被替换为一条两条指令的短跳转指令,范围是从 -128
到 127
。如果被修补函数的地址比 127
字节远,那么这条指令就无用了。函数之前的空间会用一系列 NOP
(无操作) 指令填充。所以,短跳转指令会跳转到该位置,然后从那里开始,再执行一个长跳转到热补丁函数。
我不轻信他人的话。我必须自己弄清楚,通过让 Visual C++ 生成那条 MOV
指令。请按照以下步骤操作。创建一个名为 Test
的新 Visual C++ 控制台项目,并使用下面的代码。或者,你也可以在文章顶部下载示例项目。
#include <cstdio>
int my_func(int n)
{
n += 2;
return n;
}
int main()
{
int n = 10;
n = my_func(n);
printf("Value: %d\n", n);
}
要将 Visual C++ 设置为生成汇编代码,请转到 **项目属性** -> **配置属性** -> **C/C++** -> **输出文件** -> **汇编器输出**,然后选择 “**带源代码的汇编 (/FAs)**”。
要将 Visual C++ 设置为生成热补丁指令,请转到 **项目属性** -> **配置属性** -> **C/C++** -> **命令行**,然后在 **x86/Win32** 平台上,在 “**附加选项**” 中添加 “**/hotpatch**”。在 x64 平台上,即使不指定,热补丁也是默认启用的。然而,我无法让 Visual C++ 为 x64 平台生成热补丁指令。如果你知道原因,请在下方评论告诉我。
要将 Visual C++ 设置为在函数之前生成 NOP
指令,请转到 **项目属性** -> **配置属性** -> **链接器** -> **命令行**,然后在 “**附加选项**” 中添加 “**/FUNCTIONPADMIN**”。此步骤是可选的。但我们对生成的 NOP
指令的类型感兴趣。
然后,在 **x86/Win32** 平台上构建你的项目。在 Release 文件夹中,使用你喜欢的文本编辑器或 Visual Studio 查看 Test.asm。
?my_func@@YAHH@Z PROC ; my_func, COMDAT
; 4 : {
npad 2
push ebp
mov ebp, esp
; 5 : n += 2;
mov eax, DWORD PTR _n$[ebp]
add eax, 2
mov DWORD PTR _n$[ebp], eax
; 6 : return n;
mov eax, DWORD PTR _n$[ebp]
; 7 : }
pop ebp
ret 0
?my_func@@YAHH@Z ENDP ; my_func
NPAD
不是有效的 Intel x86 汇编指令。启动 Windows Debugger 打开 Test.exe,并在命令窗口中使用命令 uf Test!my_func
来反汇编 my_func
。
my_func
的第一条指令不是 mov edi, edi
,而是 xchg ax, ax
,它占用了两个字节,相当于“无操作”。NPAD 2
似乎是告诉链接器用两条指令填充函数的指令。
在命令窗口中使用此 WinDbg 命令 ub Test!my_func
向后反汇编 my_func
,以显示 my_func
之前的指令。
这八条填充的 CC
指令被称为 “**中断 3**”,也就是 DebugBreak
。如果调试器附加到进程,它会像达到断点一样中断。如果没有调试器存在,它实际上是 NOP
。函数之间的空间是无人区,意味着没有进程会执行这些 CC
指令。
我希望你喜欢猜测这些指令的作用,就像我喜欢研究/编写这篇文章一样。
参考文献
- 维基百科 TEST (x86 instruction)
- MSDN /hotpatch (Create Hotpatchable Image)
- MSDN /FUNCTIONPADMIN (Create Hotpatchable Image)
历史
- 2021 年 12 月 5 日:首次发布