如何在汇编中的for循环中找到起始值?
How to find the starting value in a for loop in assembly?
当我将它转换为 C 时,我无法理解下面的汇编代码的作用。我知道它是一个循环,但我不知道从哪里开始转换它。
我有点明白输入必须是 6 个数字,在循环中它会加 5 并进行比较。
我主要停留在我们如何知道起始值的问题上?
0x0000000000400f15 <+9>: callq 0x4016e5 <read_six_numbers>
0x0000000000400f1a <+14>: lea 0x4(%rsp),%rbx
0x0000000000400f1f <+19>: lea 0x18(%rsp),%rbp
0x0000000000400f24 <+24>: mov -0x4(%rbx),%eax
0x0000000000400f27 <+27>: add [=10=]x5,%eax
0x0000000000400f2a <+30>: cmp %eax,(%rbx)
0x0000000000400f2c <+32>: je 0x400f33 <phase_2+39>
0x0000000000400f2e <+34>: callq 0x4016c3 <explode_bomb>
0x0000000000400f33 <+39>: add [=10=]x4,%rbx
0x0000000000400f37 <+43>: cmp %rbp,%rbx
0x0000000000400f3a <+46>: jne 0x400f24 <phase_2+24>
函数read_six_numbers
接收数组的地址,用于在寄存器%rsi
中存储数字。 %rsi
设置为指向堆栈底部的位置 (%rsp
),其中一些 space 分配了 sub [=14=]x28,%rsp
。 0x400f24
处的循环使用寄存器 %rbx
作为指向数组的指针,从开头开始。它检查前一个值 + 5 是否等于当前值。如果不是,它会不带参数调用 explode_bomb()
。循环迭代5次,直到指针指向数组末尾。
这里有一些您的问题中没有指定的事项需要考虑(ABI、处理器体系结构、可执行文件格式等)。并非所有这些都是回答您的问题所必需的,但理解这一点可能会提高您对函数、方法或过程在广泛的可执行上下文中的调用方式的一般理解。
ABI
特别是,不同的 CPU 体系结构、操作系统,甚至可执行二进制格式可能具有不同的签名来处理程序输入。因为很明显您使用的是 AMD64 架构 CPU,您可能会发现 this wikipedia page 很有用。特别是,您似乎正在根据代码段中的某些上下文使用 "System V x86-64 ABI" 。 (稍后我们将对您的代码段进行全面分析。)
堆栈
C 编程语言确实有任何堆栈的概念,因此虽然这与您的代码片段相关,但它不是 C 程序的要求,并且您的程序的可移植版本可能无法使用堆栈。事实上,虽然介绍性编译器课程似乎仍然倾向于使用堆栈在调用帧之间传递状态,但在 AMD64 上的 SysV ABI 中通常不会使用堆栈。
(在 x86 上执行此操作更为常见,因为 32 位架构受寄存器限制。在此类架构上使用寄存器传递状态的开销可能更高,因为寄存器可能会需要将它们复制到堆栈中,以便可以重复使用它们,并且因为可能需要保留它们的其他函数调用。)
您的代码段
SysV ABI 特别使用 %rdi
、%rsi
、%rdx
、%rcx
、%r8
、%r9
和 %xmm0-7
,按照这个顺序。
0x0000000000400f0c <+0>: push %rbp
0x0000000000400f0d <+1>: push %rbx
这通过将表示堆栈帧的寄存器压入堆栈顶部来保留调用者的堆栈帧。 %rbp
和 %rbx
是 "callee-save" 寄存器,这意味着调用的函数必须保留它们的值,因为调用者需要它们的值来保留其状态。
0x0000000000400f0e <+2>: sub [=11=]x28,%rsp
这会在堆栈上分配 40 个字节的 space。为什么是 40 字节?我们已经将 16 个字节压入堆栈,保留 %rbp
和 %rbx
。对于 read_six_numbers
,我们需要额外的 24 个字节用于我们的 scratch space,所以 16 + 24 == 40.
0x0000000000400f12 <+6>: mov %rsp,%rsi
这会将堆栈的基地址移动到%rsi
。现在,因为我假设是 SysV ABI,这意味着地址实际上是我们将要调用的函数的 second 参数。 space 的内容未定义,很可能是随机值。这是 read_six_numbers
.
使用的 scratch space
0x0000000000400f15 <+9>: callq 0x4016e5 <read_six_numbers>
这会调用函数 read_six_numbers
。由于我们的 scratch space 是第二个参数(根据 SysV ABI),这意味着我们的调用函数在 %rdi
中有一个值,该值未经修改就传递给 read_six_numbers
。如果我不得不猜测,我会说这个值回答了你的问题,所以我们需要查看这个 phase_2
函数的调用者以获得任何进一步的见解。
0x0000000000400f1a <+14>: lea 0x4(%rsp),%rbx
read_six_numbers
读取6个32位数字共24个字节。起始编号位于 0x0(%rsp)
并且 lea
为我们提供了特定值的地址。因此,这为我们提供了指向数组中第二个值的指针,并将其放入 %rbx
.
0x0000000000400f1f <+19>: lea 0x18(%rsp),%rbp
数组的第一个值在0x0(%rsp)
,第6个在0x14(%rsp)
; 0x18(%rbp)
是数组末尾的第一个大小对齐地址。
0x0000000000400f24 <+24>: mov -0x4(%rbx),%eax
0x0000000000400f27 <+27>: add [=16=]x5,%eax
0x0000000000400f2a <+30>: cmp %eax,(%rbx)
0x0000000000400f2c <+32>: je 0x400f33 <phase_2+39>
0x0000000000400f2e <+34>: callq 0x4016c3 <explode_bomb>
0x0000000000400f33 <+39>: add [=16=]x4,%rbx
0x0000000000400f37 <+43>: cmp %rbp,%rbx
0x0000000000400f3a <+46>: jne 0x400f24 <phase_2+24>
用户 chqrlie 很好地解释了这个循环。如果前一个(-0x4(%rbx)
)和当前+5相等,我们就继续循环。否则我们调用 explode_bomb
。我要补充一点,尽管 chqrlie 说它不需要参数,但不能保证它不需要。我们实际上并没有触及 %rdi
或 %rsi
,因此该上下文仍然可供它使用。要断言 explode_bomb
不接受任何参数,我们必须查看它的反汇编;此上下文并不能证明它不需要参数。
但是,所比较的实际值在此上下文中未定义。我们只是在这里循环内存。
0x0000000000400f3c <+48>: add [=17=]x28,%rsp
0x0000000000400f40 <+52>: pop %rbx
0x0000000000400f41 <+53>: pop %rbp
这将恢复调用者上下文(记住我们在开始时被调用者保存了调用者堆栈的状态)并且...
0x0000000000400f42 <+54>: retq
returns到调用者的下一个IP。
也许这里有您不知道的东西。否则,只是一个长篇大论的解释,告诉你 chqrlie 已经做了什么:循环的起始值比 read_six_numbers
.
填充的数组基数高 4 个字节
当我将它转换为 C 时,我无法理解下面的汇编代码的作用。我知道它是一个循环,但我不知道从哪里开始转换它。
我有点明白输入必须是 6 个数字,在循环中它会加 5 并进行比较。
我主要停留在我们如何知道起始值的问题上?
0x0000000000400f15 <+9>: callq 0x4016e5 <read_six_numbers>
0x0000000000400f1a <+14>: lea 0x4(%rsp),%rbx
0x0000000000400f1f <+19>: lea 0x18(%rsp),%rbp
0x0000000000400f24 <+24>: mov -0x4(%rbx),%eax
0x0000000000400f27 <+27>: add [=10=]x5,%eax
0x0000000000400f2a <+30>: cmp %eax,(%rbx)
0x0000000000400f2c <+32>: je 0x400f33 <phase_2+39>
0x0000000000400f2e <+34>: callq 0x4016c3 <explode_bomb>
0x0000000000400f33 <+39>: add [=10=]x4,%rbx
0x0000000000400f37 <+43>: cmp %rbp,%rbx
0x0000000000400f3a <+46>: jne 0x400f24 <phase_2+24>
函数read_six_numbers
接收数组的地址,用于在寄存器%rsi
中存储数字。 %rsi
设置为指向堆栈底部的位置 (%rsp
),其中一些 space 分配了 sub [=14=]x28,%rsp
。 0x400f24
处的循环使用寄存器 %rbx
作为指向数组的指针,从开头开始。它检查前一个值 + 5 是否等于当前值。如果不是,它会不带参数调用 explode_bomb()
。循环迭代5次,直到指针指向数组末尾。
这里有一些您的问题中没有指定的事项需要考虑(ABI、处理器体系结构、可执行文件格式等)。并非所有这些都是回答您的问题所必需的,但理解这一点可能会提高您对函数、方法或过程在广泛的可执行上下文中的调用方式的一般理解。
ABI
特别是,不同的 CPU 体系结构、操作系统,甚至可执行二进制格式可能具有不同的签名来处理程序输入。因为很明显您使用的是 AMD64 架构 CPU,您可能会发现 this wikipedia page 很有用。特别是,您似乎正在根据代码段中的某些上下文使用 "System V x86-64 ABI" 。 (稍后我们将对您的代码段进行全面分析。)
堆栈
C 编程语言确实有任何堆栈的概念,因此虽然这与您的代码片段相关,但它不是 C 程序的要求,并且您的程序的可移植版本可能无法使用堆栈。事实上,虽然介绍性编译器课程似乎仍然倾向于使用堆栈在调用帧之间传递状态,但在 AMD64 上的 SysV ABI 中通常不会使用堆栈。
(在 x86 上执行此操作更为常见,因为 32 位架构受寄存器限制。在此类架构上使用寄存器传递状态的开销可能更高,因为寄存器可能会需要将它们复制到堆栈中,以便可以重复使用它们,并且因为可能需要保留它们的其他函数调用。)
您的代码段
SysV ABI 特别使用 %rdi
、%rsi
、%rdx
、%rcx
、%r8
、%r9
和 %xmm0-7
,按照这个顺序。
0x0000000000400f0c <+0>: push %rbp
0x0000000000400f0d <+1>: push %rbx
这通过将表示堆栈帧的寄存器压入堆栈顶部来保留调用者的堆栈帧。 %rbp
和 %rbx
是 "callee-save" 寄存器,这意味着调用的函数必须保留它们的值,因为调用者需要它们的值来保留其状态。
0x0000000000400f0e <+2>: sub [=11=]x28,%rsp
这会在堆栈上分配 40 个字节的 space。为什么是 40 字节?我们已经将 16 个字节压入堆栈,保留 %rbp
和 %rbx
。对于 read_six_numbers
,我们需要额外的 24 个字节用于我们的 scratch space,所以 16 + 24 == 40.
0x0000000000400f12 <+6>: mov %rsp,%rsi
这会将堆栈的基地址移动到%rsi
。现在,因为我假设是 SysV ABI,这意味着地址实际上是我们将要调用的函数的 second 参数。 space 的内容未定义,很可能是随机值。这是 read_six_numbers
.
0x0000000000400f15 <+9>: callq 0x4016e5 <read_six_numbers>
这会调用函数 read_six_numbers
。由于我们的 scratch space 是第二个参数(根据 SysV ABI),这意味着我们的调用函数在 %rdi
中有一个值,该值未经修改就传递给 read_six_numbers
。如果我不得不猜测,我会说这个值回答了你的问题,所以我们需要查看这个 phase_2
函数的调用者以获得任何进一步的见解。
0x0000000000400f1a <+14>: lea 0x4(%rsp),%rbx
read_six_numbers
读取6个32位数字共24个字节。起始编号位于 0x0(%rsp)
并且 lea
为我们提供了特定值的地址。因此,这为我们提供了指向数组中第二个值的指针,并将其放入 %rbx
.
0x0000000000400f1f <+19>: lea 0x18(%rsp),%rbp
数组的第一个值在0x0(%rsp)
,第6个在0x14(%rsp)
; 0x18(%rbp)
是数组末尾的第一个大小对齐地址。
0x0000000000400f24 <+24>: mov -0x4(%rbx),%eax
0x0000000000400f27 <+27>: add [=16=]x5,%eax
0x0000000000400f2a <+30>: cmp %eax,(%rbx)
0x0000000000400f2c <+32>: je 0x400f33 <phase_2+39>
0x0000000000400f2e <+34>: callq 0x4016c3 <explode_bomb>
0x0000000000400f33 <+39>: add [=16=]x4,%rbx
0x0000000000400f37 <+43>: cmp %rbp,%rbx
0x0000000000400f3a <+46>: jne 0x400f24 <phase_2+24>
用户 chqrlie 很好地解释了这个循环。如果前一个(-0x4(%rbx)
)和当前+5相等,我们就继续循环。否则我们调用 explode_bomb
。我要补充一点,尽管 chqrlie 说它不需要参数,但不能保证它不需要。我们实际上并没有触及 %rdi
或 %rsi
,因此该上下文仍然可供它使用。要断言 explode_bomb
不接受任何参数,我们必须查看它的反汇编;此上下文并不能证明它不需要参数。
但是,所比较的实际值在此上下文中未定义。我们只是在这里循环内存。
0x0000000000400f3c <+48>: add [=17=]x28,%rsp
0x0000000000400f40 <+52>: pop %rbx
0x0000000000400f41 <+53>: pop %rbp
这将恢复调用者上下文(记住我们在开始时被调用者保存了调用者堆栈的状态)并且...
0x0000000000400f42 <+54>: retq
returns到调用者的下一个IP。
也许这里有您不知道的东西。否则,只是一个长篇大论的解释,告诉你 chqrlie 已经做了什么:循环的起始值比 read_six_numbers
.