哪个寄存器被退回?
Which one of the registers are being returned?
我尝试编写一个汇编程序,它执行输入数的 4 次幂。它将循环并将 %eax 当前值与原始输入数相乘,直到当 %ecx 值为 0 时跳转到完成。
但是在程序运行之后返回0。我不确定为什么(?),返回的是 %ecx 寄存器值而不是 %eax 吗?
当有多个寄存器使用其中的值时,关于返回什么的规则是什么?
.globl funk
.globl _funk
funk:
_funk:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %edx
movl , %eax
movl , %ecx
jmp check
check:
cmpl [=10=], %ecx
jz done
jmp multiply
done:
popl %ebp
ret
multiply:
mull %edx
dec %ecx
jmp check
在汇编语言中,每个寄存器的值都被return编辑。换句话说,当您从一个过程 ret
urn 时,寄存器没有任何变化。 所有值都将与您在到达 ret
语句之前留下的值完全相同。该过程的调用者将看到一切。在这方面,您可以认为 ret
就像 jmp
一样返回给调用者。 (事实上 ,它很像这样 - 它将调用者的地址从堆栈中弹出,然后跳回它。)
这意味着,如果您想要 "return" 一个值,调用者和被调用者需要就该值将被 return 编辑的位置达成一致。由于您是在汇编中编程,因此您(程序员)可以自己选择。没有什么是预先确定的。您可以决定 "return" EAX
中的值,或 ECX
中的值,或不同寄存器中的值,或 多个 寄存器中的不同值,或甚至 "return" 内存中的值(即在堆栈中)。
现在,您可能想到的是与 C 或 C++ 代码进行互操作时。在这些语言中,通常有一个标准化的 调用约定 (实际上是由 ABI 决定的,而不是语言,但我们不要吹毛求疵)。在 x86 上,我所知道的所有 C/C++ 调用约定 return 整数大小的值都在 EAX
寄存器中。因此,在编写您自己的汇编代码时,为您自己采用这不是一个坏习惯。但请注意,这只是一个惯例,可以做不同的事情以获得更大的灵活性。 始终 检查特定汇编语言过程的文档,了解它 (A) 期望如何获得其输入(参数),(B) where/how 它 returns 它的值(如果有的话),以及 (C) 它破坏了什么寄存器(如果有的话)。 None 这些东西是给定的。它们都是由C/C++代码中的调用约定定义的,在编写汇编时不存在,因此您可以自己编写。
那么,让我们看看您的实际代码。
这条指令:
movl 8(%ebp), %edx
表明您的 funk
指令需要在堆栈上传递一个 DWORD 大小的值。正如您刚刚了解到的,该期望确实应该记录在此函数定义之前的注释中。
在此代码中:
movl , %ecx
jmp check
check:
cmpl [=11=], %ecx
jmp check
毫无意义。 check
是下一个标签。如果你不 jmp
那里,执行将落到那里 无论如何 ,所以你最好跳过跳转。这将使您的代码更简单、更快。
这个:
cmpl [=12=], %ecx
是测试寄存器是否为 0 的低效方法。它当然有效,但不如 testl %ecx, %ecx
好。换句话说,将寄存器与自身按位与运算 (TEST
),这将设置与减法 (CMP
) 完全相同的标志,但速度更快并且需要的指令字节更少。只是一点优化提示。不过,您仍将使用 cmp [=31=], xxx
查看 memory 位置是否为 0,因为您不能使用 test mem, mem
。 (两个操作数都不能是内存位置。)
您代码的真正问题是这条指令:
mull %edx
这是一个32位的乘法,也就是说它在做EDX:EAX = EAX * EDX
。换句话说,它将 EAX
的当前内容乘以 MUL
(EDX
) 的操作数,并将结果存储在 EDX:EAX
中。冒号表示结果的高位DWORD存储在EDX
中,结果的低位DWORD存储在EAX
中。看到这一点后,有两件事应该很明显:(1) 32 位乘法产生 64 位结果,以及 (2) 它破坏了 EAX
和 EDX
寄存器。那么,现在问题应该很明显了:你的下一个乘法是 EAX * EDX
,但是 EDX
是 0,所以结果总是 0。这就是为什么它是 returning 0。事实上,在你的 funk
函数结束时,EAX
、EDX
和 ECX
将 all 为 0!
让我们重写代码来解决这些问题,并重新组织它以使其更易于阅读:
# Computes input to the 4th power.
#
# Parameters: The input is a DWORD-sized value passed on the stack.
# Returns: EAX contains the result
# Clobbers: EAX, ECX, EDX
funk:
# (You used to set up a stack frame here, but you don't really need that,
# so I'm going to skip doing it altogether.)
# Initialize our other registers.
movl , %eax
movl , %ecx
# See if we've done enough multiplications, or should keep looping.
check:
testl %ecx, %ecx
jz done
# Multiply EAX by the parameter (found at an offset of 4 from the stack pointer).
mull 4(%esp)
# Decrement our counter and loop again.
decl %ecx
jmp check
done:
# (We didn't create a stack frame, so we don't need to tear it down.)
# Just return, with the result still in EAX.
ret
我写这个的方式,我每次都从堆栈中重新加载被乘数。我想这有点低效,但在循环中进行乘法也是如此!如果你愿意,你可以使用另一个寄存器(比如,EBX
)来保存被乘数。有很多不同的方法可以编写相同的代码。
另一种可能的优化是重新排列代码,使 check
位于循环的底部,您可以利用 DEC
指令设置的标志,而不是不得不做额外的 TEST
/CMP
。 (这可能并不总是有效。只有当你知道你想循环至少一次时它才有效——do
…while
循环和 C 中的 while
循环。)
另外,在这个版本中,我们不会破坏任何东西,这将使调用者更容易。对于我们需要使用的任何寄存器,我们将通过在函数顶部 push
ing 它们并在底部 pop
ping 它们来保留它们的内容。
(现在,通常会教您创建堆栈帧并使用 EBP
寄存器的固定偏移量,但这不是绝对必要的,C 编译器也不是这样会做的。它只会弄清楚你在堆栈上压入 3 个 DWORD 大小的值,所以这将使堆栈指针递减 3×4 = 12,所以过去在偏移量处找到的参数现在将在偏移量 16 处找到 4。)
# Computes input to the 4th power in a slightly more efficient way.
#
# Parameters: The input is a DWORD-sized value passed on the stack.
# Returns: EAX contains the result
# Clobbers: <none>
funk:
pushl %ebx
pushl %ecx
pushl %edx
movl 16(%esp), %ebx
movl , %eax
movl , %ecx
mull %ebx
decl %ecx
jnz check
popl %edx
popl %ecx
popl %ebx
ret
在我结束这篇谩骂之前,我想提请你注意一个微妙的错误。它可以追溯到我之前提到的关于 32 位乘法 returning 64 位值的内容。它这样做的原因是因为两个 32 位值相乘可能会溢出 一个 32 位值。当您进行指数运算时,这甚至可能 更多。因此,如果您想保护自己免受溢出并支持相对较大的输入值,则需要 return 64 位结果。
C 编译器通常执行此操作的方式是 return,结果在 EDX:EAX
中,就像 MUL
指令一样。尽管您可以随心所欲地进行,但这是一个很好的惯例。
这次让我们全力以赴提高效率。要计算的幂 (4) 是一个常数,因此我们可以完全省去循环。
; Computes input to 4th power, without risking an overflow in the result.
; Parameters: EAX contains the input
; Returns: EDX:EAX contains the result
; Clobbers: ECX
FunkyFunk:
mull %eax # EDX:EAX = EAX * EAX
movl %edx, %ecx
imull %eax, %ecx # ECX = EAX * EDX
mull %eax # EDX:EAX = EAX * EAX
addl %ecx, %edx # EDX = ECX + EDX
addl %ecx, %edx # EDX = ECX + EDX
ret
对了,你为什么把这个函数命名为funk
?为什么不将其命名为 pow4
或类似的显而易见的名称?编程已经够难了,不要让自己和必须阅读您的代码的其他人更难!
我尝试编写一个汇编程序,它执行输入数的 4 次幂。它将循环并将 %eax 当前值与原始输入数相乘,直到当 %ecx 值为 0 时跳转到完成。
但是在程序运行之后返回0。我不确定为什么(?),返回的是 %ecx 寄存器值而不是 %eax 吗?
当有多个寄存器使用其中的值时,关于返回什么的规则是什么?
.globl funk
.globl _funk
funk:
_funk:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %edx
movl , %eax
movl , %ecx
jmp check
check:
cmpl [=10=], %ecx
jz done
jmp multiply
done:
popl %ebp
ret
multiply:
mull %edx
dec %ecx
jmp check
在汇编语言中,每个寄存器的值都被return编辑。换句话说,当您从一个过程 ret
urn 时,寄存器没有任何变化。 所有值都将与您在到达 ret
语句之前留下的值完全相同。该过程的调用者将看到一切。在这方面,您可以认为 ret
就像 jmp
一样返回给调用者。 (事实上 ,它很像这样 - 它将调用者的地址从堆栈中弹出,然后跳回它。)
这意味着,如果您想要 "return" 一个值,调用者和被调用者需要就该值将被 return 编辑的位置达成一致。由于您是在汇编中编程,因此您(程序员)可以自己选择。没有什么是预先确定的。您可以决定 "return" EAX
中的值,或 ECX
中的值,或不同寄存器中的值,或 多个 寄存器中的不同值,或甚至 "return" 内存中的值(即在堆栈中)。
现在,您可能想到的是与 C 或 C++ 代码进行互操作时。在这些语言中,通常有一个标准化的 调用约定 (实际上是由 ABI 决定的,而不是语言,但我们不要吹毛求疵)。在 x86 上,我所知道的所有 C/C++ 调用约定 return 整数大小的值都在 EAX
寄存器中。因此,在编写您自己的汇编代码时,为您自己采用这不是一个坏习惯。但请注意,这只是一个惯例,可以做不同的事情以获得更大的灵活性。 始终 检查特定汇编语言过程的文档,了解它 (A) 期望如何获得其输入(参数),(B) where/how 它 returns 它的值(如果有的话),以及 (C) 它破坏了什么寄存器(如果有的话)。 None 这些东西是给定的。它们都是由C/C++代码中的调用约定定义的,在编写汇编时不存在,因此您可以自己编写。
那么,让我们看看您的实际代码。
这条指令:
movl 8(%ebp), %edx
表明您的 funk
指令需要在堆栈上传递一个 DWORD 大小的值。正如您刚刚了解到的,该期望确实应该记录在此函数定义之前的注释中。
在此代码中:
movl , %ecx
jmp check
check:
cmpl [=11=], %ecx
jmp check
毫无意义。 check
是下一个标签。如果你不 jmp
那里,执行将落到那里 无论如何 ,所以你最好跳过跳转。这将使您的代码更简单、更快。
这个:
cmpl [=12=], %ecx
是测试寄存器是否为 0 的低效方法。它当然有效,但不如 testl %ecx, %ecx
好。换句话说,将寄存器与自身按位与运算 (TEST
),这将设置与减法 (CMP
) 完全相同的标志,但速度更快并且需要的指令字节更少。只是一点优化提示。不过,您仍将使用 cmp [=31=], xxx
查看 memory 位置是否为 0,因为您不能使用 test mem, mem
。 (两个操作数都不能是内存位置。)
您代码的真正问题是这条指令:
mull %edx
这是一个32位的乘法,也就是说它在做EDX:EAX = EAX * EDX
。换句话说,它将 EAX
的当前内容乘以 MUL
(EDX
) 的操作数,并将结果存储在 EDX:EAX
中。冒号表示结果的高位DWORD存储在EDX
中,结果的低位DWORD存储在EAX
中。看到这一点后,有两件事应该很明显:(1) 32 位乘法产生 64 位结果,以及 (2) 它破坏了 EAX
和 EDX
寄存器。那么,现在问题应该很明显了:你的下一个乘法是 EAX * EDX
,但是 EDX
是 0,所以结果总是 0。这就是为什么它是 returning 0。事实上,在你的 funk
函数结束时,EAX
、EDX
和 ECX
将 all 为 0!
让我们重写代码来解决这些问题,并重新组织它以使其更易于阅读:
# Computes input to the 4th power.
#
# Parameters: The input is a DWORD-sized value passed on the stack.
# Returns: EAX contains the result
# Clobbers: EAX, ECX, EDX
funk:
# (You used to set up a stack frame here, but you don't really need that,
# so I'm going to skip doing it altogether.)
# Initialize our other registers.
movl , %eax
movl , %ecx
# See if we've done enough multiplications, or should keep looping.
check:
testl %ecx, %ecx
jz done
# Multiply EAX by the parameter (found at an offset of 4 from the stack pointer).
mull 4(%esp)
# Decrement our counter and loop again.
decl %ecx
jmp check
done:
# (We didn't create a stack frame, so we don't need to tear it down.)
# Just return, with the result still in EAX.
ret
我写这个的方式,我每次都从堆栈中重新加载被乘数。我想这有点低效,但在循环中进行乘法也是如此!如果你愿意,你可以使用另一个寄存器(比如,EBX
)来保存被乘数。有很多不同的方法可以编写相同的代码。
另一种可能的优化是重新排列代码,使 check
位于循环的底部,您可以利用 DEC
指令设置的标志,而不是不得不做额外的 TEST
/CMP
。 (这可能并不总是有效。只有当你知道你想循环至少一次时它才有效——do
…while
循环和 C 中的 while
循环。)
另外,在这个版本中,我们不会破坏任何东西,这将使调用者更容易。对于我们需要使用的任何寄存器,我们将通过在函数顶部 push
ing 它们并在底部 pop
ping 它们来保留它们的内容。
(现在,通常会教您创建堆栈帧并使用 EBP
寄存器的固定偏移量,但这不是绝对必要的,C 编译器也不是这样会做的。它只会弄清楚你在堆栈上压入 3 个 DWORD 大小的值,所以这将使堆栈指针递减 3×4 = 12,所以过去在偏移量处找到的参数现在将在偏移量 16 处找到 4。)
# Computes input to the 4th power in a slightly more efficient way.
#
# Parameters: The input is a DWORD-sized value passed on the stack.
# Returns: EAX contains the result
# Clobbers: <none>
funk:
pushl %ebx
pushl %ecx
pushl %edx
movl 16(%esp), %ebx
movl , %eax
movl , %ecx
mull %ebx
decl %ecx
jnz check
popl %edx
popl %ecx
popl %ebx
ret
在我结束这篇谩骂之前,我想提请你注意一个微妙的错误。它可以追溯到我之前提到的关于 32 位乘法 returning 64 位值的内容。它这样做的原因是因为两个 32 位值相乘可能会溢出 一个 32 位值。当您进行指数运算时,这甚至可能 更多。因此,如果您想保护自己免受溢出并支持相对较大的输入值,则需要 return 64 位结果。
C 编译器通常执行此操作的方式是 return,结果在 EDX:EAX
中,就像 MUL
指令一样。尽管您可以随心所欲地进行,但这是一个很好的惯例。
这次让我们全力以赴提高效率。要计算的幂 (4) 是一个常数,因此我们可以完全省去循环。
; Computes input to 4th power, without risking an overflow in the result.
; Parameters: EAX contains the input
; Returns: EDX:EAX contains the result
; Clobbers: ECX
FunkyFunk:
mull %eax # EDX:EAX = EAX * EAX
movl %edx, %ecx
imull %eax, %ecx # ECX = EAX * EDX
mull %eax # EDX:EAX = EAX * EAX
addl %ecx, %edx # EDX = ECX + EDX
addl %ecx, %edx # EDX = ECX + EDX
ret
对了,你为什么把这个函数命名为funk
?为什么不将其命名为 pow4
或类似的显而易见的名称?编程已经够难了,不要让自己和必须阅读您的代码的其他人更难!