除以负数会使我在NASM中产生溢出

时间:2018-08-07 00:38:23

标签: assembly nasm x86-64

我正在自学一些使用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

有人可以向我解释我到底在做什么错吗?

2 个答案:

答案 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} raxrdx: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 ,您的代码将产生

  • RDX = 0,RAX = 5。即股息= 0x0000000000000000:0000000000000005,这是正确的。 (对于非负输入,零扩展和符号扩展是相同的操作)。
  • 除数= 0x00000000FFFFFFFE = +4294967294,大而正。

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没有。