首先进行逐行分析以找出代码的作用。
push 5
该指令将常量值“5”压入堆栈。为什么?嗯,因为...
call ??_U@YAPAXI@Z ; operator new[](uint)
这条指令调用operator new[],它接受一个uint参数。该参数显然以该代码使用的任何调用约定在堆栈上传递。所以,很明显,到目前为止,我们已经调用了operator new[] 来分配一个大小为 5 字节的数组。
在 C++ 中,可以写成:
BYTE* eax = new BYTE[5];
对operator new[] 的调用会在EAX 寄存器中返回其值(指向已分配内存块开头的指针)。这是所有 x86 调用约定的一般规则——函数总是在EAX 寄存器中返回它们的结果。
mov [ebp+esi*4+var_14], eax
以上代码将结果指针(EAX 返回的指针)存储(moves)到EBP + (ESI * 4) + var_14 寻址的内存位置。换句话说,它将ESI寄存器中的值缩放4(大概是uint的大小),加上EBP寄存器的偏移量,然后加上常量var_14的偏移量。
这大致相当于下面的伪 C++ 代码:
void* address = (EBP + (ESI * 4) + var_14);
*address = eax;
add esp, 4
这会清理堆栈,有效地撤消初始 push 5 指令。
push 将一个 32 位(4 字节)值压入堆栈,递减堆栈指针,该指针保存在 ESP 寄存器中(注意堆栈向下增长x86)。这条add 指令增加堆栈指针(同样是ESP 寄存器)4 个字节。
以这种方式平衡堆栈是一种优化。您可以等效地编写pop eax,但这会产生破坏EAX 寄存器中的值的额外副作用。
此指令没有直接的 C++ 等价物,因为它只是做通常会被高级语言隐藏的簿记工作。
inc esi
这会将ESI 寄存器的值加1。相当于:
esi += 1;
mov byte ptr [eax+4], 0
这会将常量值 0 存储在 EAX + 4 的 BYTE 大小的内存块中。它对应于下面的伪C++:
BYTE* ptr = (eax + 4);
*ptr = 0;
cmp esi, 4
这会将ESI 寄存器的值与常量值 4 进行比较。CMP 指令实际上设置了标志,就好像进行了减法一样。
因此,后续指令:
jl short loc_1C1D40
如果ESI寄存器的值小于4,则有条件跳转。
比较和跳转是高级语言中循环结构的标志,例如for 或while 循环。
把它们放在一起,你有这样的东西:
void Foo(char** var_14)
{
for (int esi = 0; esi < 4; ++esi)
{
var_14[esi] = new char[5];
var_14[esi][4] = 0;
}
}
当然,这并不完全正确。从已编译的程序集中重构原始 C 或 C++ 代码很像从碎牛肉饼重构原始奶牛。
不过还不错。其实if you compile the above function in MSVC, optimizing for speed and targeting 32-bit x86, you get the following assembly generated:
void Foo(char**) PROC
push esi
push edi
mov edi, DWORD PTR _var_14$[esp+4]
xor esi, esi
$LL4@Foo:
push 5
call void * operator new[](unsigned int) ; operator new[]
mov DWORD PTR [edi+esi*4], eax
add esp, 4
inc esi
mov BYTE PTR [eax+4], 0
cmp esi, 4
jl SHORT $LL4@Foo
pop edi
pop esi
ret 0
void Foo(char**) ENDP
假设您忽略了序言和尾声(无论如何您都没有在问题中显示),这与您在问题中的内容几乎完全相同。
主要区别在于编译器对MOV 指令应用了相当明显的循环提升优化。而不是原来的代码:
mov [ebp + esi * 4 + var_14], eax
它改为在序言中预先计算 esp + var_14,并将结果缓存在免费的 EDI 寄存器中:
mov edi, DWORD PTR _var_14$[esp + 4]
让循环内的加载指令变得简单:
mov DWORD PTR [edi + esi * 4], eax
我不知道你的代码为什么不这样做,或者为什么它使用EBP 来保存偏移量。