您的代码也因负除数而损坏:divide(5,-2) 将给出零。这纯粹是通过调用约定来解释的。您的零扩展而不是符号扩展错误(请参阅@paxdiablo 的答案)仅对负股息很重要。
您告诉编译器您的函数采用 int 参数,而 int 在 x86-64 System V 调用约定中是 32 位类型。
您假设您的输入被符号扩展为 64 位,但调用约定不需要这样做,因此编译器不会在 10 字节 @ 上浪费代码大小987654335@,当它可以使用5字节mov r32, imm32时。
有关更多详细信息,请参阅这些问答。 (第二个基本上是第一个的副本):
因此,您的编译器将为您的main 发出类似这样的代码:
mov edi, 5 ; RDI = 0x0000000000000002
mov esi, -2 ; RSI = 0x00000000FFFFFFFE
call _divide
我检查了on the Godbolt compiler explorer,这就是 gcc 和 clang 真正做的事情1,即使对于未优化的代码也是如此。
对于divide(5,-2),您的代码将导致
- RDX=0,RAX=5。即股息= 0x0000000000000000:0000000000000005,这是正确的。 (零和符号扩展对于非负输入是相同的操作)。
- 除数 = 0x00000000FFFFFFFE = +4294967294,大而正。
64 位 idiv 计算 5 / 4294967294 产生商=RAX=0,余数=RDX=5。
如果您只修复了类型宽度/操作数大小不匹配的错误,您仍然会遇到像 @paxdiablo 的回答所解释的负股息问题。 但要让divide(-554,2) 真正工作,这两个修复都是必要的。
那你应该怎么写呢?
您可以将原型更改为int64_t 或long(在x86-64 System V 中为64 位),并使用cqo 设置签名除法。 (When and why do we sign extend and use cdq with mul/div?)
或者您可以使用 movsxd rax, edi / movsxd rcx, esi 将您的 32 位输入符号扩展为 64 位。但那将是愚蠢的。只需使用 32 位操作数大小,因为这是您告诉编译器通过的。
这很好,因为 64 位除法比 32 位除法慢得多。 (https://agner.org/optimize/ 和 C++ code for testing the Collatz conjecture faster than hand-written assembly - why?)。
这就是我要做的:
global _divide
; inputs: int32_t dividend in EDI, int32_t divisor in ESI
; output: int32_t quotient in EAX, int32_t remainder in EDX
; (C callers won't be able to access the remainder, unfortunately)
_divide:
mov eax, edi
cdq ; sign-extend the dividend into edx:eax
idiv esi ; no need to copy to ecx/rcx first
ret
无需推送 RBP;我们没有调用任何其他函数,因此重新对齐堆栈无关紧要,我们也没有修改 RBP 以用作帧指针。
我们可以在不保存/恢复的情况下破坏 RDX:它是 x86-64 System V 和 Windows x64 中的调用破坏寄存器。 (与大多数 32 位调用约定相同)。这是有道理的,因为它被 idiv 等一些常见指令隐式使用。
如果你用 C 语言编写,这就是 gcc 和 clang 发出的(当然启用了优化)。
int divide(int dividend, int divisor) {
return dividend / divisor;
}
(请参阅上面的 Godbolt 链接,我将它包含在 __attribute__((noinline)) 中,因此我仍然可以看到 main 实际设置函数 args。我本可以将其命名为其他名称。)
像往常一样,查看编译器输出以了解您的代码与编译器所做的事情之间的差异可以提示您做错了什么。 (或者为您提供更好的优化起点。不过,在这种情况下,编译器不会错过任何优化。)请参阅How to remove "noise" from GCC/clang assembly output?。
如果您想查看 64 位整数的代码生成,可以将类型更改为 long(在 x86-64 System V 中为 64 位,与 Windows x64 不同)。并查看调用者如何变化,例如
mov edi, 5
mov rsi, -2
call _divide
脚注1:有趣的是clang -O3的asm输出有mov esi, -2,但clang -O0写成mov edi, 4294967294。
它们都组装到相同的指令,当然,zeroing the upper 32 bits of RDI,因为这就是 AMD 设计 AMD64 的方式,而不是例如隐式符号扩展到完整寄存器中,would have been a valid design choice 但可能不如零便宜- 扩展。
顺便说一句,Godbolt 有针对 Linux 的编译器,但这是相同的调用约定。唯一的区别是 OS X 使用前导 _ 修饰函数名称,而 Linux 没有。