我尝试编写一个汇编程序,它的输入数字为 4 的幂。它会循环并将 %eax 当前值与原始输入数字相乘,直到它在 %ecx 值为 0 时跳转到完成。
但是程序运行后它返回0。我不确定为什么(?),是否返回了 %ecx 寄存器值而不是 %eax?
当有多个寄存器使用其中的值时,关于返回什么的规则是什么?
.globl funk
.globl _funk
funk:
_funk:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %edx
movl $1, %eax
movl $4, %ecx
jmp check
check:
cmpl $0, %ecx
jz done
jmp multiply
done:
popl %ebp
ret
multiply:
mull %edx
dec %ecx
jmp check
Run Code Online (Sandbox Code Playgroud)
在汇编语言中,每个寄存器的值都会被返回。换句话说,当你ret从一个过程中取出时,寄存器没有任何变化。所有值都将与您在到达ret语句之前离开时完全一样。过程的调用者将看到一切照原样。在这方面,您可以将ret工作视为jmp对调用者的回馈。(实际上,它很像这样——它从堆栈中弹出调用者的地址,然后跳回它。)
这意味着,如果您想“返回”一个值,调用者和被调用者需要就返回该值的位置达成一致。由于您在汇编中编程,您(程序员)可以自己选择。没有什么是预先确定的。您可以决定“返回” EAX、 或ECX、 或不同寄存器中的值,或多个寄存器中的不同值,甚至“返回”内存中(即堆栈中)的值。
现在,您可能想到的是何时与 C 或 C++ 代码进行互操作。在这些语言中,通常有一个标准化的调用约定(实际上是由 ABI 决定的,而不是语言,但我们不要分心)。在 x86 上,我知道的所有 C/C++ 调用约定都在EAX寄存器中返回整数大小的值。因此,在编写自己的汇编代码时,这对自己来说并不是一个糟糕的约定。但请注意,这只是一种约定,可以做不同的事情以获得更大的灵活性。总是检查特定汇编语言过程的文档以了解它 (A) 期望如何获取其输入(参数),(B)它在哪里/如何返回其值(如果有),以及(C)什么寄存器(如果有) )它破坏了。这些东西都不是给定的。它们都是由 C/C++ 代码中的调用约定定义的,在编写程序集时不存在,因此您可以自己编写。
现在,让我们看看您的实际代码。
本指令:
movl 8(%ebp), %edx
Run Code Online (Sandbox Code Playgroud)
建议您的funk指令期望在堆栈上传递一个 DWORD 大小的值。正如您刚刚了解到的,该期望确实应该记录在此函数定义之前的注释中。
在这段代码中:
movl $4, %ecx
jmp check
check:
cmpl $0, %ecx
Run Code Online (Sandbox Code Playgroud)
该jmp check是没有意义的。check是下一个标签。如果你不在jmp那里,无论如何执行都会落到那里,所以你最好跳过跳转。这将使您的代码更简单、更快。
这个:
cmpl $0, %ecx
Run Code Online (Sandbox Code Playgroud)
是一种测试寄存器是否为 0 的低效方法。它当然有效,但不如testl %ecx, %ecx. 换句话说,将寄存器与自身 ( TEST) 进行按位与运算,这将设置与减法 ( CMP)完全相同的标志,但这样做会更快,并且需要更少的指令字节。只是一点优化技巧。不过,您仍将使用cmp $0, xxx查看内存位置是否为 0,因为您不能这样做test mem, mem。(两个操作数都不能是内存位置。)
你的代码的真正问题是这个指令:
mull %edx
Run Code Online (Sandbox Code Playgroud)
这是一个 32 位乘法,这意味着它正在执行EDX:EAX = EAX * EDX. 换句话说,它将 的当前内容乘以( )EAX的操作数,并将结果存储在 中。那个冒号表示结果的高位 DWORD 存储在 中,而结果的低位 DWORD 存储在 中。一旦你看到这一点,两件事应该是显而易见的:(1) 32 位乘法产生64 位结果,以及 (2) 它破坏了和寄存器。好吧,现在问题应该很明显了:你的下一个乘法是,但是是 0,所以结果总是 0。这就是它返回 0 的原因。事实上,在你的函数结束时,MULEDXEDX:EAXEDXEAXEAXEDXEAX * EDXEDXfunkEAX,EDX以及ECX将所有为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 $1, %eax
movl $4, %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
Run Code Online (Sandbox Code Playgroud)
我写这个的方式,我每次都从堆栈中重新加载被乘数。我想这有点低效,但在循环中进行乘法也是如此!如果你愿意,你可以使用另一个寄存器(比如,EBX)来保存被乘数。有很多不同的方法可以编写相同的代码。
另一种可能的优化是重新排列代码,使其check位于循环的底部,您可以捎带DEC指令设置的标志,而不必执行额外的TEST/ CMP。(这可能并不总是有效。只有当你知道你想要至少循环一次时才有效——C 中的do...while循环和while循环之间的区别。)
此外,在这个版本中,我们不会破坏任何东西,这将使调用者更容易。对于我们需要使用的任何寄存器,我们将通过push在函数顶部 ing 并在pop底部 ping 来保留它们的内容。
(现在,通常会教您创建堆栈帧并使用EBP寄存器中的固定偏移量,但这并不是绝对必要的,也不是 C 编译器会这样做的方式。它只会弄清楚您将 3 个 DWORD 大小的值推送到堆栈上,这将使堆栈指针减少 3×4 = 12,因此以前在偏移量为 4 处找到的参数现在将在偏移处找到共 16 个。)
# 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 $1, %eax
movl $4, %ecx
mull %ebx
decl %ecx
jnz check
popl %edx
popl %ecx
popl %ebx
ret
Run Code Online (Sandbox Code Playgroud)
在我结束这篇谩骂之前,我想提醒您注意一个微妙的错误。它可以追溯到我之前提到的关于 32 位乘法返回 64 位值的事情。这样做的原因是因为两个 32 位值相乘可能会溢出32 位值。当您进行幂运算时,这种情况更有可能发生。因此,如果您想保护自己免受溢出,并支持相对较大的输入值,则需要返回 64 位结果。
C 编译器通常这样做的方式是将结果返回到 中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
Run Code Online (Sandbox Code Playgroud)
顺便说一下,你为什么给这个函数命名funk?你为什么不命名它pow4或类似的东西?编程已经够难了,不要让你自己和其他必须阅读你的代码的人更难!