哪个寄存器被退回?

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编辑。换句话说,当您从一个过程 return 时,寄存器没有任何变化。 所有值都将与您在到达 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) 它破坏了 EAXEDX 寄存器。那么,现在问题应该很明显了:你的下一个乘法是 EAX * EDX,但是 EDX 是 0,所以结果总是 0。这就是为什么它是 returning 0。事实上,在你的 funk 函数结束时,EAXEDXECXall 为 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。 (这可能并不总是有效。只有当你知道你想循环至少一次时它才有效——dowhile 循环和 C 中的 while 循环。)

另外,在这个版本中,我们不会破坏任何东西,这将使调用者更容易。对于我们需要使用的任何寄存器,我们将通过在函数顶部 pushing 它们并在底部 popping 它们来保留它们的内容。

(现在,通常会教您创建堆栈帧并使用 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 或类似的显而易见的名称?编程已经够难了,不要让自己和必须阅读您的代码的其他人更难