我想我会偏离彼得所讨论的内容(他提供了很好的信息),并深入了解我认为给您带来问题的一些问题的核心。当我第一次看到这个问题时,我认为代码可能是编译器生成的,jmp rax 可能是某些控制流语句的结果。生成此类代码序列的最可能方法是通过 C switch。由跳转表组成的switch 语句根据控制变量说明应执行的代码并不少见。例如:switch(a) 的控制变量是a。
这一切对我来说都是有意义的,我写了一些 cmets(现已删除),最终导致 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 处的指令可能是循环顶部的第一条指令。出于性能原因,优化器可能会将NOPs 放入代码中,以将循环顶部的第一条指令与内存中的 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 的底部 32 位 em> 然后将值符号扩展为 RAX 的高 32 位。实际上,如果 32 位值为负(最高有效位为 1),RAX 的高 32 位将设置为 1。如果 32 位值不为负,则高 32- RAX 的位将设置为 0。
当第一次遇到此代码时,RDX 包含某个表的基址,位于内存中加载的程序开头的 0x8b4 处。 RAX 设置为 0。实际上,表中的前 32 位被复制到 RAX 并进行符号扩展。如前所述,偏移量 0xb84 处的值是 0xfffffdec。该 32 位值是负数,因此 RAX 包含 0xfffffffffffffdec。
现在进入正题:
55d: add rax,rdx
560: jmp rax
RDX 仍然保存内存中表开头的地址。 RAX 被添加到该值并存储回 RAX (RAX = RAX+RDX)。然后我们 JMP 到存储在 RAX 中的地址。所以这段代码似乎都表明我们有一个带有 32 位值的 JUMP 表,我们用它来确定我们应该去哪里。那么显而易见的问题。表中的 32 位值是多少? 32 位值是表的开头和我们要跳转到的指令地址之间的差异。
从我们的程序加载到内存中的位置,我们知道表是 0x8b4。 C 编译器告诉链接器计算 0x8b4 和我们要执行的指令所在的地址之间的差异。如果程序已在 0x0000000000000000(假设)处加载到内存中,RAX = RAX+RDX 将导致 RAX em> 为 0xfffffffffffffdec + 0x8b4 = 0x00000000000006a0。然后我们使用jmp rax 跳转到0x6a0。您没有显示整个内存转储,但是当传递给 switch 语句的值为 0 时,将执行 0x6a0 处的代码。JUMP 表中的每个 32 位值将是类似的偏移量将根据switch 语句中的控制变量执行的代码。如果我们将 0x8b4 添加到表中的所有条目,我们会得到:
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 开始的表有 14 个条目。可以假设switch 语句可能有 14 种不同的状态(案例)。