如何理解这段汇编代码的流程

How to comprehend the flow of this assembly code

我不明白这是怎么回事。
这是 main() 程序的一部分,由 objdump 反汇编并用 intel notation

编写
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 中,rip 是 [537] 所以 [rdx] = [537 + 37d] = 8b4.
第一个问题是rdx的值有多大?值是 ec 还是 ecfdffff 或其他?如果它有DWORD,我可以理解为有'ecfdffff'(即使这也是错误的?:()但是这个程序没有声明它。我如何判断值?

然后程序继续。
559年,rax首次出现
第二个问题是这个rax可以解释为eax的一部分,此时rax=0?如果 rax 为 0,在 559 中意味着 rax = DWORD[rdx] 并且 rax 的值变为 ecfdffff 并且 next [55d] do rax += rdx,我认为这个值不能 jamp。一定有什么地方不对,请告诉我哪里错了,或者我是怎么错的。

but this program don't declare it

您正在查看机器代码 + 数据的反汇编。这只是内存中的字节。反汇编程序设法显示的任何标签都是留在 executable 的符号 table 中的标签。它们与 CPU 如何运行机器代码无关。

(ELF程序头告诉OS的程序加载器如何将其映射到内存,以及跳转到哪里作为入口点。这与符号无关,除非是共享库引用了 executable.)

中定义的一些全局变量或函数

您可以single-step GDB 中的代码并观察寄存器值的变化。


In 559, rax is first appeared.

EAX为RAX的低32位。将 EAX zero-extends 隐式写入 RAX。从mov DWORD PTR [rsp-0xc],0x0和后面的reload,我们知道RAX=0.

这一定是 un-optimized 编译器输出(或 volatile int idx = 0; 以阻止常量传播),否则它会在编译时知道 RAX=0并且可以优化其他一切。


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

A RIP-relative 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

这是一个跳跃table。首先,它使用 cmp eax,0xd 检查 out-of-bounds 索引,然后使用 EAX(movsxd 使用将 RAX 缩放 4 的寻址模式索引 table 的 32 位带符号偏移量), 并将其添加到 table 的基地址以获得跳转目标。

GCC 可以 只跳转 table 64 位绝对指针,但选择不这样做 .rodata 是 position-independent也不需要在 PIE executable 中进行 load-time 修正。 (尽管 Linux 确实支持这样做。)参见 https://gcc.gnu.org/bugzilla/show_bug.cgi?id=84011 讨论的地方(尽管该错误的主要焦点是 gcc -fPIE 无法将开关转换为 table查找字符串地址,其实还是用了跳转table)

jump-offsettable地址在RDX中,这是用较早的LEA设置的

我想我会偏离 Peter 讨论的内容(他提供了很好的信息)并深入到我认为给您带来问题的一些问题的核心。当我第一眼看到这个问题时,我假设代码可能是编译器生成的,而 jmp rax 可能是某些控制流语句的结果。生成此类代码序列的最可能方法是通过 C switch。由跳转 table 构成的 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。一旦你意识到这一点,那么这就更有意义了。如果您对 table 中的所有数据进行同样的调整,您将得到:

.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 表明这可能是一个 table 的 32 位值,用于根据 switch 的控制变量中的值来确定要跳转到的内存位置声明。

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

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

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

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

R10 正在加载 64 位值 0xedd5a792ef95fa9e。 R9D是64位的低32位R9register.The值0xffffffcc正在加载到64位的低32位R9 但还有其他事情发生。在 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全为0。

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 位值)将其加载到底部的 32 位RAX 然后将值符号扩展到 RAX 的高 32 位。实际上,如果 32 位值为负(最高有效位为 1),RAX 的高 32 位将被设置为 1。如果 32 位值不为负,则RAX 的高 32 位将被设置为 0。

第一次遇到此代码时 RDX 包含一些 table 的基数,从程序加载到内存的开始处起 0x8b4。 RAX 设置为 0。有效地将 table 中的前 32 位复制到 RAX 并进行符号扩展。如前所述,偏移量 0xb84 处的值为 0xfffffdec。该 32 位值为负,因此 RAX 包含 0xfffffffffffffdec。

现在进入正题:

55d:    add    rax,rdx
560:    jmp    rax

RDX 仍然保存内存中 table 开头的地址。 RAX 被添加到该值并存储回 RAX (RAX = RAX+RDX)。然后我们 JMP 到存储在 RAX 中的地址。所以这段代码似乎都在暗示我们有一个带有 32 位值的 JUMP table,我们用它来确定我们应该去哪里。那么显而易见的问题。 table 中的 32 位值是什么? 32 位值是 table 的开头与我们要跳转到的指令地址之间的差异。

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

 08b0:            0x000006a0 0x00000688 0x00000670
 08c0: 0x00000650 0x00000630 0x00000620 0x00000600
 08d0: 0x000005F0 0x000005e0 0x000005c0 0x000005a0
 08e0: 0x00000588 0x00000568 0x000006c0

您应该会发现,在您没有向我们提供的代码中,这些地址与出现在 jmp rax 之后的代码一致。

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

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