为什么要复制他已有的相同值到rax?

Why copy the same value to rax that he already has?

谁能解释一下为什么我们在主函数@0x6f5中将rax中的值移动到rdi,然后将rdi处的值复制到堆栈的 get_v,然后将其移回 rax @0x6c8?。可能是x86-64的一种约定俗成,但是我没看懂它的逻辑。

 main:
   0x00000000000006da <+0>:     push   rbp
   0x00000000000006db <+1>:     mov    rbp,rsp
   0x00000000000006de <+4>:     sub    rsp,0x10
   0x00000000000006e2 <+8>:     mov    rax,QWORD PTR fs:0x28
   0x00000000000006eb <+17>:    mov    QWORD PTR [rbp-0x8],rax
   0x00000000000006ef <+21>:    xor    eax,eax
   0x00000000000006f1 <+23>:    lea    rax,[rbp-0xc]
 =>0x00000000000006f5 <+27>:    mov    rdi,rax
   0x00000000000006f8 <+30>:    call   0x6c0 <get_v>
   0x00000000000006fd <+35>:    mov    eax,0x0
   0x0000000000000702 <+40>:    mov    rdx,QWORD PTR [rbp-0x8]
   0x0000000000000706 <+44>:    xor    rdx,QWORD PTR fs:0x28
   0x000000000000070f <+53>:    je     0x716 <main+60>
   0x0000000000000711 <+55>:    call   0x580
   0x0000000000000716 <+60>:    leave  
   0x0000000000000717 <+61>:    ret    

 get_v
   0x00000000000006c0 <+0>:     push   rbp
   0x00000000000006c1 <+1>:     mov    rbp,rsp
   0x00000000000006c4 <+4>:     mov    QWORD PTR [rbp-0x8],rdi
 =>0x00000000000006c8 <+8>:     mov    rax,QWORD PTR [rbp-0x8]
   0x00000000000006cc <+12>:    mov    DWORD PTR [rax],0x2
   0x00000000000006d2 <+18>:    mov    rax,QWORD PTR [rbp-0x8]
   0x00000000000006d6 <+22>:    mov    eax,DWORD PTR [rax]
   0x00000000000006d8 <+24>:    pop    rbp
   0x00000000000006d9 <+25>:    ret    

这是未优化的代码。这里有很多说明是多余的,而且意义不大,所以我不确定你为什么要固定在特定的指示上。考虑它前面的说明:

xor    eax,eax
lea    rax,[rbp-0xc]

首先,RAX 被清除(对 64 位寄存器的低 32 位进行操作的指令隐式清除高位,因此 xor reg32, reg32 等效于 xor reg32, reg32xor reg64, reg64),然后 RAX 加载了一个值。绝对没有理由先清除 RAX,所以第一条指令可以完全省略。

在此代码中:

lea    rax,[rbp-0xc]
mov    rdi,rax

RAX被加载,然后它的值被复制到RDI。如果您需要在 RAXRDI 中使用相同的值,这是有道理的,但您不需要。该值只需要在 RDI 中以准备函数调用。 (System V AMD64 调用约定在 RDI 寄存器中传递第一个整数参数。)所以这可能只是:

lea   rdi, [rbp-0xc]

但是,同样,这是未优化的代码。编译器优先考虑 快速 代码生成和在单个(高级语言)语句上设置断点的能力,而不是生成 高效 代码(这生产时间更长,调试更难)。

get_v 中堆栈的循环溢出重载是未优化代码的另一个症状:

mov    QWORD PTR [rbp-0x8],rdi
mov    rax,QWORD PTR [rbp-0x8]

None 是必需的。这只是忙碌的工作,是未优化代码的普通名片。在优化构建或手写汇编中,它会被简单地编写为寄存器到寄存器的移动,例如:

mov    rax, rdi

您会发现 GCC 总是 遵循您在未优化构建中观察到的模式。考虑这个函数:

void SetParam(int& a)
{
    a = 0x2;
}

使用 -O0(禁用优化),GCC 发出以下内容:

SetParam(int&):
    push    rbp
    mov     rbp, rsp
    mov     QWORD PTR [rbp-8], rdi
    mov     rax, QWORD PTR [rbp-8]
    mov     DWORD PTR [rax], 2
    nop
    pop     rbp
    ret

看着眼熟?

现在启用优化,我们得到更明智的:

SetParam(int&):
    mov     DWORD PTR [rdi], 2
    ret

在这里,直接存储到 RDI 寄存器中传递的地址中。不需要设置或拆除堆栈框架。事实上,堆栈被完全绕过。不仅代码更简单易懂,而且速度更快。

这是一个教训:当您尝试分析编译器的目标代码输出时,始终启用优化。研究未优化的构建在很大程度上是浪费时间,除非您真的对编译器如何生成未优化的代码感兴趣(例如,因为您正在编写或对编译器本身进行逆向工程)。否则,您关心的是优化代码,因为它更易于理解并且更真实。

您的整个 get_v 功能可以简单地是:

mov   DWORD PTR [rdi], 0x2
mov   eax, DWORD PTR [rdi]
ret

没有理由使用堆栈来回洗牌。没有理由从地址 RBP-8 重新加载数据,因为我们已经将该值加载到 RDI.

但实际上,我们可以做得更好,因为我们将一个 常量 移动到存储在 RDI:

中的地址中
mov   DWORD PTR [rdi], 0x2
mov   eax, 0x2
ret

事实上,这正是 GCC 为我想象中的 get_v 函数生成的内容:

int get_v(int& a)
{
    a = 0x2;
    return a;
}

未优化:

get_v(int&):
    push    rbp
    mov     rbp, rsp
    mov     QWORD PTR [rbp-8], rdi
    mov     rax, QWORD PTR [rbp-8]
    mov     DWORD PTR [rax], 2
    mov     rax, QWORD PTR [rbp-8]
    mov     eax, DWORD PTR [rax]
    pop     rbp
    ret

优化:

get_v(int&):
    mov     DWORD PTR [rdi], 2
    mov     eax, 2
    ret