C++ 到汇编,GCC 与 CL

逆向工程 拆卸 部件 C++ 反汇编者 海湾合作委员会
2021-07-01 09:58:40

我有以下 C++ 代码:

int main(){

  int a = 1;
  double d = 1.2;

  return 0;
}

并使用 GCC 6.2 -m32 获得以下程序集:

main:
        lea     ecx, [esp+4]
        and     esp, -8
        push    DWORD PTR [ecx-4]
        push    ebp
        mov     ebp, esp
        push    ecx
        sub     esp, 20
        mov     DWORD PTR [ebp-12], 1
        fld     QWORD PTR .LC0
        fstp    QWORD PTR [ebp-24]
        mov     eax, 0
        add     esp, 20
        pop     ecx
        pop     ebp
        lea     esp, [ecx-4]
        ret
.LC0:
        .long   858993459
        .long   1072902963

并使用 MS CL 19:

_d$ = -12                                         ; size = 8
_a$ = -4                                                ; size = 4
_main   PROC
        push     ebp
        mov      ebp, esp
        sub      esp, 12              ; 0000000cH
        mov      DWORD PTR _a$[ebp], 1
        movsd    xmm0, QWORD PTR __real@3ff3333333333333
        movsd    QWORD PTR _d$[ebp], xmm0
        xor      eax, eax
        mov      esp, ebp
        pop      ebp
        ret      0
_main   ENDP

我有几个问题。

  1. GCC 版本中的前三行是什么意思?

    lea ecx, [esp+4]

    and esp, -8

    push DWORD PTR [ecx-4]

  2. MS CL 版本分配 12 个字节,4 个用于 int,8 个用于 double:

    sub esp, 12 // that's great.

    但是为什么 GCC 分配了 24?

    push ecx

    sub esp, 20

1个回答

鉴于您没有指定任何优化标志并使用 -m32,GCC 不会对您的代码执行任何优化。-m32 标志为配置为默认生成 64 位代码的编译器指定生成 32 位代码。在 32 位模式下,即使激活优化,GCC 也会生成次优代码,因为在 Intel 机器上以 32 位模式进行浮点计算的唯一方法是通过x87指令。如果删除 -m32 标志并添加 -O3(GCC 中的第三级优化),您将获得以下汇编代码(与 Microsoft 的 CL 生成的非常相似):

.LC1:
        .string "%d %lf\n"
        .section   .text.startup,"ax",@progbits
        .p2align 4,,15
        .globl  main
        .type   main, @function
main:
.LFB0:
        .cfi_startproc
        subq    $8, %rsp
        .cfi_def_cfa_offset 16
        movl    $1, %esi
        movl    $.LC1, %edi
        movsd   .LC0(%rip), %xmm0
        movl    $1, %eax
        call    printf
        xorl    %eax, %eax
        addq    $8, %rsp
        .cfi_def_cfa_offset 8
        ret
        .cfi_endproc
.LFE0:
        .size   main, .-main
        .section   .rodata.cst8,"aM",@progbits,8
        .align 8
.LC0:
        .long   858993459
        .long   1072902963 

注意:在代码中添加了一个printf,因为如果 GCC 优化阶段发现没有使用这两个变量,它们将被删除(死代码消除)。我邀请您查看我关于优化与非优化代码(汇编代码 - GCC 优化与非优化)主题的帖子

您还可以注意到 CL 使用XMM寄存器来存储.LC0 中存储的 64 位精度元素。XMM寄存器是 SSE(流式 SIMD 扩展)指令集的一部分,主要用于浮点标量和向量运算。它的实现比 x87 指令集更清晰、更快。

第一季度:

lea     ecx, [esp+4]      //load the content of [esp + 4] into ecx
and     esp, -8           //align the stack pointer to 8 bytes (same as esp & ~7)
push    DWORD PTR [ecx-4] //push the content of [ecx - 4] on the stack 

[ecx - 4] = [[esp + 4] - 4]

让我们假设堆栈处于这种状态:

     |       main      |
     |      return     |
     |      address @  |
     +-----------------+  <--- esp + 4 ---> ecx
     |    some value   |
     +-----------------+  <--- esp = ebp

第一条指令将现有堆栈内容(主返回地址@)放入ecx。它相当于:

mov ecx, esp
sub ecx, 4
mov ecx, [ecx]

你可以看到 lea 指令在一个 take 中完成了这些指令在三个 take 中所做的。

第二条指令在 8 字节边界上对齐 esp。这意味着 esp 指向的地址的低 3 位将为 0。当在 2 的幂边界上对齐时,Intel 机器上的内存访问速度更快。

第三条指令将堆栈的状态更改为以下内容:

     |         @       |
     +-----------------+  <--- esp + 4 ---> ecx
     |    some value   |
     +-----------------+  <--- ebp
     |       @ - 4     |  
     +-----------------+  <--- esp

因此,当main函数完成时,它会返回@-4。

问题二:

让我们从数学上推理:

         We have : EBP = ESP0 
         push ecx implies ESP1 = ESP0 - 4 
         then : ESP2 = ESP1 - 20 
         therefore : ESP0 = ESP2 - 24
         mov DWORD PTR [ebp-12], 1 implies x = EBP - 12 = ESP0 - 12
         We know that ESP0 = ESP2 - 24
         Therefore x = ESP2 - 24 - 12 = ESP2 - 36
         fstp    QWORD PTR [ebp-24] implies y = EBP - 24 = ESP0 - 24
         Therefore y = ESP2 - 24 - 24 = ESP2 - 48

现在,从这个演示中,我们提取了整数x = ESP2 - 36的位置,以及双精度 y = ESP2 - 48 的位置为了计算两个变量之间的距离,我们从 x 中减去 y 并获得以下结果:x - y = ESP2 - 36 - ESP2 + 48 = 48 - 36 = 12这就是 GCC 用于存储 32 位/4 字节和 64 位/8 字节变量的字节数。