【问题标题】:What is the C/C++ equivalent for this assembly code?此汇编代码的 C/C++ 等效项是什么?
【发布时间】:2019-09-01 01:23:45
【问题描述】:

我正在尝试理解这段汇编代码,谁能帮我用 C/C++ 语言编写它?

这是代码:

 loc_1C1D40:             ; unsigned int
 push    5
 call    ??_U@YAPAXI@Z   ; operator new[](uint)
 mov     [ebp+esi*4+var_14], eax
 add     esp, 4
 inc     esi
 mov     byte ptr [eax+4], 0
 cmp     esi, 4
 jl      short loc_1C1D40

据我了解,前两行只是调用“operator new”,它返回 eax 中的地址。 在此之后,“mov [ebp+esi*4+var_14], eax”意味着该地址可能被保存在某种数组中。 esi 增加的原因非常明显。 但是为什么我们要在 esp 上加 4 呢?

【问题讨论】:

  • 这是调用约定。见“cdecl”。
  • 请注意,没有称为 C/C++ 的语言。
  • add esp,4 平衡循环内的push。在循环外推一次可能更有效,然后是mov dword [esp], 5 / call,但对于代码大小来说更糟,对于一个小循环来说不值得。很明显,这是一个分配数组的循环,将指针存储在另一个数组中。 (并将0 存储到每个数组中)。
  • 谢谢我的朋友,虽然我不明白关于平衡阵列的第一部分。我会做一些功课:) @PeterCordes
  • 平衡堆栈,而不是数组。

标签: assembly x86 masm


【解决方案1】:

首先进行逐行分析以找出代码的作用

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,则有条件跳转。

比较和跳转是高级语言中循环结构的标志,例如forwhile 循环。


把它们放在一起,你有这样的东西:

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 来保存偏移量。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-18
    • 2020-08-17
    • 1970-01-01
    相关资源
    最近更新 更多