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

Linux 如何理解此汇编代码的流程,linux,assembly,x86-64,reverse-engineering,objdump,Linux,Assembly,X86 64,Reverse Engineering,Objdump,我不明白这是怎么回事。 这是由objdump反汇编的主程序的一部分,用英特尔符号编写 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,0xffffff

我不明白这是怎么回事。 这是由objdump反汇编的主程序的一部分,用英特尔符号编写

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
在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,接下来的[55d]do rax+=rdx,我认为这个值不会阻塞。一定是出了什么事,所以告诉我哪里出了什么错,或者我是怎么出的错

但是这个程序没有声明它

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

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

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

559年,rax首次出现

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

这必须是未优化的编译器输出或volatile int idx=0;为了阻止常量传播,否则它将在编译时知道RAX=0,并可以优化其他所有内容

lea rdx,[rip+0x37d]8b4

RIP相对LEA将静态地址放入寄存器。这不是来自记忆的负荷。当具有索引寻址模式的movsxd使用RDX作为基址时,会发生这种情况

拆卸工为你算出了地址;它是RDX=0x8b4。相对于文件的开头;实际运行时,程序将映射到一个虚拟地址,如0x55555…000

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

GCC可以创建一个包含64位绝对指针的跳转表,但选择不这样做。rodata也是位置独立的,不需要在饼图可执行文件中进行加载时间修正。尽管Linux确实支持这样做。尽管该缺陷的主要焦点是gcc-fPIE不能将开关转换为字符串地址的表查找,实际上仍然使用跳转表,但请参见此处讨论的内容

跳转偏移表地址在RDX中,这是使用早期LEA设置的

但是这个程序没有声明它

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

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

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

559年,rax首次出现

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

这必须是未优化的编译器输出或volatile int idx=0;为了阻止常量传播,否则它将在编译时知道RAX=0,并可以优化其他所有内容

lea rdx,[rip+0x37d]8b4

RIP相对LEA将静态地址放入寄存器。这不是来自记忆的负荷。当具有索引寻址模式的movsxd使用RDX作为基址时,会发生这种情况

拆卸工为你算出了地址;它是RDX=0x8b4。相对于文件的开头;实际运行时,程序将映射到一个虚拟地址,如0x55555…000

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

GCC可以创建一个包含64位绝对指针的跳转表,但选择不这样做 在PIE可执行文件中不需要加载时间修正。尽管Linux确实支持这样做。尽管该缺陷的主要焦点是gcc-fPIE不能将开关转换为字符串地址的表查找,实际上仍然使用跳转表,但请参见此处讨论的内容


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

我想我将偏离Peter讨论的内容,他提供了很好的信息,并触及我认为会给您带来问题的一些问题的核心。当我第一次看这个问题时,我假设代码可能是由编译器生成的,而JMPRAX可能是某个控制流语句的结果。生成这样一个代码序列的最有可能的方法是通过C。由跳转表组成的switch语句根据控制变量说明应该执行哪些代码,这种情况并不少见。例如:开关a的控制变量为a

这一切对我来说都是有意义的,我写了一些现在被删除的评论,这些评论最终导致了JMPRAX将访问的奇怪内存地址。我有差事要办,但当我回来的时候,我有一个惊喜,你可能和我一样困惑。使用-s选项的objdump的输出显示为:

现在,如果我们看一下您拥有的代码,它以:

530:    lea    rdx,[rip+0x37d]        # 8b4 <_IO_stdin_used+0x4>
这些似乎是编译器选择存储在寄存器而不是内存中的变量

53f:    movabs r10,0xedd5a792ef95fa9e 
549:    mov    r9d,0xffffffcc
R10正在加载64位值0xedd5a792ef95fa9e。R9D是64位R9寄存器的低32位。值0xFFFFCC正在加载到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都是零

现在将EAX与0xd进行比较。如果高于ja,则转到0x57c处的指令

554:    cmp    eax,0xd
557:    ja     57c <main+0x4c>
是一条指令,它将采用32位源操作数。在本例中,内存地址RDX+RAX*4处的32位值将其加载到RAX的底部32位,然后将该值符号扩展到RAX的上部32位。实际上,如果32位值为负,最高有效位为1,RAX的上32位将设置为1。如果32位值不是负值,RAX的上32位将设置为0

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

现在来谈谈形势的实质:

55d:    add    rax,rdx
560:    jmp    rax
RDX仍然将地址保存在内存中表的开头。RAX被添加到该值并存储回RAX RAX=RAX+RDX中。然后,我们将JMP映射到存储在RAX中的地址。所以这段代码似乎都表明我们有一个带有32位值的跳转表,我们用它来决定我们应该去哪里。那么显而易见的问题就来了。表中的32位值是什么?32位值是表的开头和我们要跳转到的指令地址之间的差值

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

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

考虑到这一点,我 mory地址0x550是对齐的,我有一种预感,这个switch语句在一个循环中,该循环作为某种类型继续执行,直到满足退出的适当条件为止。switch语句本身中的代码可能会更改用于switch语句的控制变量的值。每次运行switch语句时,控制变量都有不同的值,并且将执行不同的操作


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

我想我会偏离彼得讨论的内容,他提供了很好的信息,并触及我认为给你带来问题的一些问题的核心。当我第一次看这个问题时,我假设代码可能是由编译器生成的,而JMPRAX可能是某个控制流语句的结果。生成这样一个代码序列的最有可能的方法是通过C。由跳转表组成的switch语句根据控制变量说明应该执行哪些代码,这种情况并不少见。例如:开关a的控制变量为a

这一切对我来说都是有意义的,我写了一些现在被删除的评论,这些评论最终导致了JMPRAX将访问的奇怪内存地址。我有差事要办,但当我回来的时候,我有一个惊喜,你可能和我一样困惑。使用-s选项的objdump的输出显示为:

现在,如果我们看一下您拥有的代码,它以:

530:    lea    rdx,[rip+0x37d]        # 8b4 <_IO_stdin_used+0x4>
这些似乎是编译器选择存储在寄存器而不是内存中的变量

53f:    movabs r10,0xedd5a792ef95fa9e 
549:    mov    r9d,0xffffffcc
R10正在加载64位值0xedd5a792ef95fa9e。R9D是64位R9寄存器的低32位。值0xFFFFCC正在加载到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都是零

现在将EAX与0xd进行比较。如果高于ja,则转到0x57c处的指令

554:    cmp    eax,0xd
557:    ja     57c <main+0x4c>
是一条指令,它将采用32位源操作数。在本例中,内存地址RDX+RAX*4处的32位值将其加载到RAX的底部32位,然后将该值符号扩展到RAX的上部32位。实际上,如果32位值为负,最高有效位为1,RAX的上32位将设置为1。如果32位值不是负值,RAX的上32位将设置为0

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

现在来谈谈形势的实质:

55d:    add    rax,rdx
560:    jmp    rax
RDX仍然将地址保存在内存中表的开头。RAX被添加到该值并存储回RAX RAX=RAX+RDX中。然后,我们将JMP映射到存储在RAX中的地址。所以这段代码似乎都表明我们有一个带有32位值的跳转表,我们用它来决定我们应该去哪里。那么显而易见的问题就来了。表中的32位值是什么?32位值是表的开头和我们要跳转到的指令地址之间的差值

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

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

考虑到内存地址0x550是对齐的,我有一种预感,这个switch语句位于一个循环中,该循环作为某种类型一直执行,直到满足退出的适当条件为止。switch语句本身中的代码可能会更改用于switch语句的控制变量的值。每次运行switch语句时,控制变量都有不同的值,并且将执行不同的操作


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

非常感谢您的详细回答!答案对我来说很容易理解。如果没有main部分和.rodata部分,我不知道也不知道程序的其他部分,但是当输入正确的comandline参数时,这个程序集被准确地解释为程序的一部分,返回0,如果它不是正确的参数,返回1。非常感谢您提供详细的答案!答案对我来说很容易理解。如果没有main部分和.rodata部分,我不知道程序的其他部分,但是当输入正确的comandline参数时,这个程序集被准确地解释为程序的一部分,返回0,如果它不是正确的参数,返回1。我很感谢您的回答!我没有所有的程序,所以我可能不会使用gdb。但gdb可能是学习汇编的非常有用的工具,所以我将尝试学习gdb@哈恩:在现实生活中,如果没有二进制文件,你永远不会只有objdump输出,因为你必须自己在二进制文件上使用objdump。所以你总是可以gdb它。如果它是潜在的恶意软件,最好是在虚拟机中,和/或注意只执行一步,不要超过任何系统调用或int 0x80指令。或者设置$pc=0x123456并设置寄存器,以开始单步执行一段您感兴趣的代码。虽然通常更容易将asm文本复制/粘贴到文件中并将其组装,但这里有相关数据的情况除外。非常感谢您的回答!我没有所有的程序,所以我可能不会使用gdb。但gdb可能是学习汇编的非常有用的工具,所以我将尝试学习gdb@哈恩:在现实生活中,如果没有二进制文件,你永远不会只有objdump输出,因为你必须自己在二进制文件上使用objdump。所以你总是可以gdb它。如果它是潜在的恶意软件,最好是在虚拟机中,和/或注意只执行一步,不要超过任何系统调用或int 0x80指令。或者设置$pc=0x123456并设置寄存器,以开始单步执行一段您感兴趣的代码。尽管通常更容易将asm文本复制/粘贴到文件中并进行汇编,但此处有关联数据的情况除外。
559:   movsxd rax,DWORD PTR [rdx+rax*4]
55d:    add    rax,rdx
560:    jmp    rax
 08b0:            0x000006a0 0x00000688 0x00000670
 08c0: 0x00000650 0x00000630 0x00000620 0x00000600
 08d0: 0x000005F0 0x000005e0 0x000005c0 0x000005a0
 08e0: 0x00000588 0x00000568 0x000006c0