assembly - 为什么要将他已经拥有的相同值复制到 rax 中?

标签 assembly x86 gdb x86-64 reverse-engineering

有人可以向我解释一下为什么我们将主函数@0x6f5中的rax中的值移至rdi,然后将值复制到rdiget_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 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]

这些都不是必需的。这只是繁忙的工作,是未优化代码的常见名片。在优化的构建或手写程序集中,它可以简单地编写为寄存器到寄存器的移动,例如:

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

关于assembly - 为什么要将他已经拥有的相同值复制到 rax 中?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44534733/

相关文章:

assembly - x86 程序集 - 为什么 [e]bx 保留在调用约定中?

assembly - 在 Commodore 64 上使用程序集写入磁盘文件

assembly - 什么是好的或有趣的类似汇编程序的语言,但在更高的层次上?

assembly - 在不使用相关性的情况下直接在 ASM 中调用/跳转(x86)

c - GDB,中断条件,检查空指针

linux - 如何使用 gdb 调试 gstreamer?

c++ - 我的应用程序可以安排 gdb 断点或观察吗?

gcc - 与 glibc 的静态链接,无需调用 main

parsing - 我想写一个编译器;你推荐什么程序集(x86)/后端?是否使用 BISON?

c - 如何刷新 Linux 中地址空间区域的 CPU 缓存?