65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2021 年 12 月 4 日

CPOL

4分钟阅读

viewsIcon

9612

downloadIcon

112

猜猜具有相同源操作数和目标操作数的汇编指令会做什么?

引言

今天的文章是一个有趣的测验,猜猜下面这 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) 的真值表。

XOR table

答案:它将寄存器清零!Visual C++ 优化器生成此代码是因为此操作比将字面值零移动到寄存器更快。

TEST EBX, EBX

我们的第二个问题是 TEST 指令对两个操作数进行按位与 (bitwise AND) 操作。SF, ZF, PF 标志会被修改,而 AND 的结果会被丢弃。OFCF 标志被设为 0,而 AF 标志是未定义的。然后,会根据标志调用条件跳转指令。

猜猜它实现了什么?为了帮助你猜测答案,我为你呈现按位与 (AND) 的真值表。

AND table

答案:它在测试操作数是否为零。后面跟着 JZJNZ

MOV EDI, EDI

第三个问题是 MOV 指令将源操作数的值移动到目标操作数。当你谷歌搜索时,你会得到 许多关于这条无用指令的结果,它出现在 32 位 Windows 上每个 Windows API 的开头。猜猜它实现了什么?

答案:什么也没做。它用于热补丁 (hot patching)。在热补丁过程中,这条两条指令会被替换为一条两条指令的短跳转指令,范围是从 -128127。如果被修补函数的地址比 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 指令。

我希望你喜欢猜测这些指令的作用,就像我喜欢研究/编写这篇文章一样。

参考文献

历史

  • 2021 年 12 月 5 日:首次发布
© . All rights reserved.