如何理解此汇编代码的流程

时间:2019-05-24 16:43:21

标签: linux assembly x86-64 reverse-engineering objdump

我不明白它是如何工作的。
这是main()程序的一部分,由objdump分解并以intel表示法编写。

0000000000000530 <main>:
530:    lea    rdx,[rip+0x37d]        # 8b4 <_IO_stdin_used+0x4>
537:    mov    DWORD PTR [rsp-0xc],0x0
53f:    movabs r10,0xedd5a792ef95fa9e 
549:    mov    r9d,0xffffffcc
54f:    nop
550:    mov    eax,DWORD PTR [rsp-0xc]
554:    cmp    eax,0xd
557:    ja     57c <main+0x4c>
559:   movsxd rax,DWORD PTR [rdx+rax*4]
55d:    add    rax,rdx
560:    jmp    rax

rodata节转储:

.rodata 
 08b0 01000200 ecfdffff d4fdffff bcfdffff  ................
 08c0 9cfdffff 7cfdffff 6cfdffff 4cfdffff  ....|...l...L...
 08d0 3cfdffff 2cfdffff 0cfdffff ecfcffff  <...,...........
 08e0 d4fcffff b4fcffff 0cfeffff           ............

在530中,裂口为[537],因此[rdx] = [537 + 37d] = 8b4。
第一个问题是rdx的值是多少?值是ec还是ecfdffff或其他?如果它具有DWORD,我可以理解它也具有“ ecfdffff”(即使这也是错误的吗?:(),但此程序未声明它。如何判断该值?

然后程序继续。
559年,rax首次出现。
第二个问题是这个rax可以解释为eax的一部分,而此时rax = 0?如果rax为0,则在559中表示rax = DWORD [rdx]且rax的值变为ecfdffff,接下来的[55d]执行rax + = rdx,我认为该值不会出现问题。一定有问题,请告诉我在哪里,或者我是怎么做的。

2 个答案:

答案 0 :(得分:2)

  

但是此程序未声明

您正在寻找反汇编机器码和数据的信息。只是内存中的字节。反汇编程序设法显示的任何标签都是留在可执行文件的符号表中的标签。它们与CPU运行机器代码的方式无关。

(ELF程序标头告诉OS的程序加载器如何将其映射到内存中以及跳转到哪里作为入口点。这与符号无关,除非共享库引用了一些全局变量或函数中定义的函数。可执行文件。)

您可以单步执行GDB中的代码,并观察寄存器值的变化。


  

在559年,rax首次出现。

EAX是RAX的低32位。写入EAX会隐式扩展为RAX零。从mov DWORD PTR [rsp-0xc],0x0到后来的重新加载,我们知道RAX = 0。

这必须是未优化的编译器输出(或使用volatile int idx = 0;来消除常数传播),否则它将在编译时知道RAX = 0并可以优化其他所有内容。


lea rdx,[rip+0x37d] # 8b4

相对于RIP的LEA将static的地址放入寄存器中。这不是从内存中加载。 (稍后,当具有索引寻址模式的movsxd使用RDX作为基地址时。)

反汇编程序为您计算出地址;是RDX = 0x8b4。 (相对于文件的开头;在实际运行时,程序将被映射到0x55555...000之类的虚拟地址上)


554:    cmp    eax,0xd
557:    ja     57c <main+0x4c>
559:   movsxd rax,DWORD PTR [rdx+rax*4]
55d:    add    rax,rdx
560:    jmp    rax

这是一个跳转表。首先,它使用cmp eax,0xd检查越界索引,然后使用EAX(movsxd使用将RAX缩放4的寻址模式)对32位有符号偏移量的表进行索引。到表的基地址以获取跳转目标。

GCC 可以只是创建一个包含64位绝对指针的跳转表,但选择不这样做,以使.rodata也与位置无关,并且不需要加载时修正在PIE可执行文件中。 (即使Linux确实支持这样做。)请参见https://gcc.gnu.org/bugzilla/show_bug.cgi?id=84011进行讨论(尽管该错误的主要焦点是gcc -fPIE不能将开关转换为字符串地址的表查询,并且实际上仍然使用跳转表)

跳转地址表地址在RDX中,这是早期LEA设置的地址。

答案 1 :(得分:2)

我认为我将与Peter讨论的内容有所不同(他提供了很好的信息),并深入探讨了一些我认为会引起您麻烦的问题。当我第一次浏览这个问题时,我以为代码很可能是编译器生成的,而jmp rax可能是某些控制流语句的结果。生成此类代码序列的最可能方法是通过 C switch。由跳转表组成switch语句以根据控制变量执行什么代码的情况并不少见。例如:switch(a)的控制变量是a

这对我来说很有意义,我写了许多评论(现已删除),最终导致jmp rax将要使用的奇怪的内存地址。我有差事要跑,但是当我回来的时候,我有一个糟糕的时刻,就是你可能和我一样感到困惑。 objdump使用-s选项的输出显示为:

.rodata 
 08b0 01000200 ecfdffff d4fdffff bcfdffff  ................
 08c0 9cfdffff 7cfdffff 6cfdffff 4cfdffff  ....|...l...L...
 08d0 3cfdffff 2cfdffff 0cfdffff ecfcffff  <...,...........
 08e0 d4fcffff b4fcffff 0cfeffff           ............

您的问题之一似乎是关于在这里加载什么值。我从未使用过-s选项来查看各节中的数据,并且没有意识到尽管转储将数据分成4个字节(32位值)的组,但它们以字节顺序显示在内存中。起初我以为输出显示的是从最高有效字节到最低有效字节的值,并且objdump -s完成了转换。事实并非如此。

您必须手动反转每组4个字节的字节,以获取将从内存中读取的真实值到寄存器中。

输出中的

ecfdffff实际上表示ec fd ff ff。作为DWORD值(32位),您需要反转字节以获取从内存加载时所期望的HEX值。 ec fd ff ff取反将为ff ff fd ec或32位值0xfffffdec。一旦意识到这一点,那就更有意义了。如果对该表中的所有数据进行相同的调整,则会得到:

.rodata 
 08b0: 0x00020001 0xfffffdec 0xfffffdd4 0xfffffdbc
 08c0: 0xfffffd9c 0xfffffd7c 0xfffffd6c 0xfffffd4c
 08d0: 0xfffffd3c 0xfffffd2c 0xfffffd0c 0xfffffcec
 08e0: 0xfffffcd4 0xfffffcb4 0xfffffe0c

现在,如果我们看一下代码,则其开头为:

530:    lea    rdx,[rip+0x37d]        # 8b4 <_IO_stdin_used+0x4>

这不会从内存中加载数据,它正在计算某些数据的有效地址,并将该地址放置在 RDX 中。从OBJDUMP进行反汇编正在显示代码和数据,并认为该代码和数据已从0x000000000000开始加载到内存中。当它加载到内存中时,它可能会放在其他地址。在这种情况下,GCC正在生成位置独立代码(PIC)。它的生成方式使程序的第一个字节可以从内存中的任意地址开始。

# 8b4注释是我们关注的部分(此后您可以忽略信息)。反汇编的意思是,如果程序以0x0000000000000000加载,则加载到 RDX 中的值将为0x8b4。那是怎么到达的?该指令从0x530开始,但是RIP相对地址是RIP(指令指针)相对于当前指令之后的地址。反汇编程序使用的地址为0x537(当前指令之后的字节是下一条指令的第一个字节的地址)。该指令将0x37d添加到RIP并获得0x537 + 0x37d = 0x8b4。地址0x8b4恰好位于.rodata节中,您将获得该地址的转储项(如上所述)。

我们现在知道RDX包含一些数据的基础。 jmp rax暗示这很可能是一张32位值的表,用于根据switch语句的控制变量中的值来确定要跳转到的存储位置。

此语句似乎将值0作为32位值存储在堆栈中。

537:    mov    DWORD PTR [rsp-0xc],0x0

这些似乎是编译器选择存储在寄存器(而不是内存)中的变量。

53f:    movabs r10,0xedd5a792ef95fa9e 
549:    mov    r9d,0xffffffcc

R10 正在加载64位值0xedd5a792ef95fa9e。 R9D 是64位 R9 寄存器的低32位。值0xffffffcc被加载到 R9 的低32位中但还有其他事情发生。在64位模式下,如果指令的目标位置是32位寄存器,则 CPU自动将值零扩展为寄存器的高32位。 CPU向我们保证高32位被清零。

这是一个NOP,除了将下一条指令与内存地址0x550对齐外,不执行任何操作。 0x550是16字节对齐的值。它具有一些值,并且可能暗示0x550处的指令可能是循环顶部的第一条指令。由于性能原因,优化器可能会将NOP放入代码中,以使循环顶部的第一条指令与内存中的16字节对齐地址对齐:

54f:    nop

之前rsp-0xc的基于32位堆栈的变量被设置为零。这将从内存中读取值0作为32位值,并将其存储在 EAX 中。由于 EAX 是一个32位寄存器,用作指令的目的地,因此CPU自动将 RAX 的高32位填充为0。因此,所有 RAX 为零。

550:    mov    eax,DWORD PTR [rsp-0xc]
现在正在将

EAX 与0xd进行比较。如果它高于(ja),则转到0x57c处的指令。

554:    cmp    eax,0xd
557:    ja     57c <main+0x4c>

然后我们得到以下指示:

559:   movsxd rax,DWORD PTR [rdx+rax*4]

movsxd是一条指令,它将采用32位源操作数(在本例中为内存地址RDX+RAX*4的32位值)将其加载到 RAX ,然后使用符号将值扩展到 RAX 的高32位。有效地,如果32位值为负(最高有效位为1), RAX 的高32位将设置为1。如果32位值不为负,则高32位 RAX 的位将设置为0。

第一次遇到此代码时, RDX 包含从加载到内存的程序开始的0x8b4处某个表的基址。 RAX 设置为0。实际上,表中的前32位被复制到 RAX 并进行符号扩展。如前所述,偏移量0xb84处的值为0xfffffdec。该32位值为负,因此 RAX 包含0xfffffffffffffffdec。

现在要考虑的情况是

55d:    add    rax,rdx
560:    jmp    rax

RDX 仍将地址保留到内存中表的开头。 RAX 被添加到该值,并存储回 RAX RAX = RAX + RDX < / em>)。然后,我们将JMP跳转到 RAX 中存储的地址。因此,这些代码似乎都暗示我们有一个包含32位值的JUMP表,用于确定应该去哪里。于是出现了明显的问题。表中的32位值是什么? 32位值是表的开头与我们要跳转到的指令的地址之间的差。

从程序加载到内存的位置开始,我们知道该表为0x8b4。 C 编译器告诉链接器计算0x8b4与我们要执行的指令所驻留的地址之间的差。如果程序已假设以0x0000000000000000加载到内存中,则 RAX = RAX + RDX 将导致 RAX 为0xfffffffffffffffdec + 0x8b4 = 0x00000000000006a0。然后,我们使用jmp rax跳到0x6a0。您没有显示完整的内存转储,但是当传递给switch语句的值为0时,将在0x6a0处执行代码。JUMP表中的每个32位值都将类似根据{{​​1}}语句中的控制变量将执行的代码的偏移量。如果将0x8b4添加到表中的所有条目,则会得到:

switch

您应该发现,在没有提供给我们的代码中,这些地址与 08b0: 0x000006a0 0x00000688 0x00000670 08c0: 0x00000650 0x00000630 0x00000620 0x00000600 08d0: 0x000005F0 0x000005e0 0x000005c0 0x000005a0 08e0: 0x00000588 0x00000568 0x000006c0 之后出现的代码是一致的。

鉴于内存地址0x550已对齐,我有一种预感,即此jmp rax语句处于循环内,该循环一直以某种state machine的身份执行,直到满足退出的适当条件为止。 switch语句本身中的代码可能会更改用于switch语句的控制变量的值。每次运行switch语句时,控制变量都会具有不同的值,并且会执行不同的操作。

最初检查switch语句的控制变量的值是否大于0x0d(13)。 switch部分中从0x8b4开始的表有14个条目。可以假设.rodata语句可能具有14种不同的状态(案例)。