我正在自学一些使用x86-64 Mac OS的汇编程序。我试图弄清楚为什么要将正整数与负整数相除会给我溢出。例如,5/-2
必须返回-2
。但是,就我而言,它在执行2147483371
而不是-554/2
时返回-277
。这就是我的汇编文件中的内容:
; compiling using: nasm -f macho64 -o divide.o divide.s
[bits 64]
global _divide
section .text
; int divide(int dividend, int divisor)
_divide:
xor rdx, rdx ; making this to 0
push rbp ; base stack pointer
mov rax, rdi ; dividend
mov rcx, rsi ; divisor
idiv rcx ; integer division
add rsp, 8
ret
在我的main.c
文件中,我有这个:
#include <stdio.h>
extern int divide(int dividend, int divisor);
int main(void)
{
printf("divide: %d\n\n", divide(-554,2));
return (0);
}
输出:divide: 2147483371
有人可以向我解释我到底在做什么错吗?
答案 0 :(得分:4)
32位值-554signed
等于4,294,966,742unsigned
,其中一半是确实 2,147,483,371
,即您得到的答案。因此,它看起来像一个已签名/未签名的问题。并且,在检查the x86 docs for idiv
时,我们看到:
IDIV r/m64 Signed divide RDX:RAX by r/m64, result stored in:
RAX <- Quotient,
RDX <- Remainder.
请注意,第一行是“有符号除以 rdx:rax 除”位。英特尔谈论rdx:rax
时,是指由这两个64位寄存器形成的128位值。假设这两个64位寄存器包含(十六进制)值:
rax : 01234567 89ABCDEF
rdx : 11112222 FFFFEEEE
然后rdx:rax
值将是128位值:
rdx:rax : 11112222 FFFFEEEE 01234567 89ABCDEF
现在,因为您要清零rdx
,所以合并的值被视为正,因为最高位为零。实际上,您实际上要做的是将符号扩展{em} rax
到rdx:rax
中,该方法将符号保留在扩展值中。例如,考虑32位-1
,正确且不正确地将其符号扩展为64位值:
ffffffff 32-bit: -1.
ffffffff ffffffff 64-bit proper: -1.
00000000 ffffffff 64-bit improper: 4,294,967,295.
要正确地对进行符号扩展,如果最右边的位(对您来说rdx
)构成一个整数,则最左边的位(在您的情况下为rax
)应全部为一位。负数,否则全部为零。
当然,那些聪明的英特尔工程师已经想到了这种用例,因此您可以使用cqo
convert-quadword-to-octoword
指令来做到这一点,该指令可以正确扩展。考虑到这一点,您用于设置eax
的代码将变为:
mov rax, rdi ; Get dividend and
cqo ; sign extend to rdx:rax.
但是,您可能还会遇到 extra 问题。即使System V x86-64 ABI指定参数是在64位寄存器(rXX
)中传递的,传递32位值也可能实际上会留下包含垃圾的高位(我想您也可以在返回值的顶部留下垃圾。有关详细信息,请参见this excellent answer。
因此,您应该不假定您在整个64位寄存器中只有一个最右边的32位具有合理的值。
在您的情况下(假设32位整数),应使用较小宽度的除法指令对32到64而不是64到128进行符号扩展。这将导致更像:
global _divide
section .text
; int32_t divide(int32_t ediDividend, int32_t esiDivisor)
_divide:
mov eax, edi ; Get 32-bit dividend and
cdq ; sign extend to 64-bit edx:eax.
idiv esi ; Weave magic here,
; zeros leftmost rax.
ret ; Return quotient in rax/eax.
这未经测试,但是应该做您想做的。实际上,我已经取消了对rbp
的推动,因为我敢肯定这是没有必要的。它似乎没有被破坏(此函数既不会更改它,也不会调用任何其他可能更改它的函数),并且看来您实际上从来没有在原始代码中正确地恢复过它。
答案 1 :(得分:3)
您的代码也因负除数而破损:divide(5,-2)
将给出零。这纯粹是通过调用约定来解释的。您的零扩展名而不是符号扩展名错误(请参阅@paxdiablo的答案)仅对负红利有效。
您告诉编译器您的函数使用int
参数,而int
是x86-64 System V调用约定中的32位类型。
您假设输入被符号扩展为64位,但是调用约定不需要这样做,因此编译器不会浪费10字节{ {1}}可以使用5字节的mov r64, imm64
代替。
有关更多详细信息,请参阅这些问答。 (第二个基本上是第一个的重复):
因此,编译器将为mov r32, imm32
发出如下代码:
main
我检查了on the Godbolt compiler explorer,这就是gcc和clang真正做到的 1 ,即使对于未优化的代码也是如此。
对于mov edi, 5 ; RDI = 0x0000000000000002
mov esi, -2 ; RSI = 0x00000000FFFFFFFE
call _divide
,您的代码将产生
64位divide(5,-2)
计算idiv
,产生商= RAX = 0,余数= RDX = 5。
如果仅修复了type-width /操作数大小不匹配的错误,那么您仍然会遇到负红利问题,例如@paxdiablo的答案说明。 但这两个修复程序对于5 / 4294967294
的实际运行都是必需的。
您可以将原型更改为divide(-554,2)
或int64_t
(在x86-64 System V中为64位),然后使用long
进行有符号除法。 (When and why do we sign extend and use cdq with mul/div?)
或者您可以使用cqo
/ movsxd rax, edi
将32位输入符号扩展为64位。但这将是愚蠢的。只需使用32位操作数大小,因为这就是您告诉编译器通过的大小。
这很好,因为64位除法比32位除法慢得多。 (https://agner.org/optimize/和C++ code for testing the Collatz conjecture faster than hand-written assembly - why?)。
这就是我要做的:
movsxd rcx, esi
无需推送RBP;我们没有调用任何其他函数,因此重新调整堆栈无关紧要,也没有修改RBP用作帧指针。
我们允许在不保存/恢复RDX的情况下对其进行破坏:它是x86-64 System V和Windows x64中的呼叫密集寄存器。 (与大多数32位调用约定相同)。这是有道理的,因为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
之类的一些常见指令已隐式使用它。
如果您使用C编写,这就是gcc和clang发出的(当然启用了优化)。
idiv
(请参见上面的Godbolt链接,我将其包含在int divide(int dividend, int divisor) {
return dividend / divisor;
}
中,因此我仍然可以看到__attribute__((noinline))
实际上是在设置函数args。我可以将其命名为其他名称。)
和往常一样,查看编译器输出以查看代码和编译器所做的工作之间的差异,这可以使您发现错误的地方。 (或者为您提供一个更好的优化起点。但是,在这种情况下,编译器不会丢失任何优化。)请参见How to remove "noise" from GCC/clang assembly output?。
如果要查看64位整数的代码源,可以将类型更改为main
(在x86-64 System V中为64位,与Windows x64不同)。并查看呼叫方如何变化,例如
long
脚注1 :有趣的是, mov edi, 5
mov rsi, -2
call _divide
的asm输出中有clang -O3
,但是mov esi, -2
将其写为clang -O0
。
这两者都汇编成同一条指令,当然是zeroing the upper 32 bits of RDI,因为这是AMD设计AMD64的方式,而不是例如隐式地将符号扩展到完整的寄存器中,would have been a valid design choice但可能并不完全像零扩展一样便宜。
顺便说一句,Godbolt有针对Linux的编译器,但这是相同的调用约定。唯一的区别是OS X用前导mov edi, 4294967294
装饰函数名,而Linux没有。