Assembly 用负数除法会在NASM中产生溢出
我在自学x86-64 Mac操作系统的汇编编程。我想弄明白为什么当一个正整数除以一个负整数时会产生溢出。例如,Assembly 用负数除法会在NASM中产生溢出,assembly,nasm,x86-64,Assembly,Nasm,X86 64,我在自学x86-64 Mac操作系统的汇编编程。我想弄明白为什么当一个正整数除以一个负整数时会产生溢出。例如,5/-2必须返回-2。然而,在我的例子中,当我执行-554/2而不是-277时,它返回2147483371。。。这是我的程序集文件中的内容: ; compiling using: nasm -f macho64 -o divide.o divide.s [bits 64] global _divide section .text ; int divide(int dividend, i
5/-2
必须返回-2
。然而,在我的例子中,当我执行-554/2
而不是-277
时,它返回2147483371
。。。这是我的程序集文件中的内容:
; 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);
}
#包括
外部整数除法(整数除数,整数除数);
内部主(空)
{
printf(“divide:%d\n\n”,divide(-554,2));
返回(0);
}
输出:除:2147483371
有人能解释一下我到底做错了什么吗?32位值
-554signed
相当于4294966742 unsigned
,其中一半确实是2147483371
,您得到的答案。因此,它看起来像一个已签名/未签名的问题。通过研究,我们发现:
然后,rdx:rax
值将是128位的值:
rdx:rax : 11112222 FFFFEEEE 01234567 89ABCDEF
ffffffff 32-bit: -1.
ffffffff ffffffff 64-bit proper: -1.
00000000 ffffffff 64-bit improper: 4,294,967,295.
现在,由于您正在将rdx归零,因此组合值被视为正值,因为顶部位为零。实际上需要做的是将rax
符号扩展到rdx:rax
,这是一种将符号保留在扩展值中的方法。例如,考虑32位<代码> -1 < /代码>,将符号适当地扩展和不正确地扩展成64位值:
rdx:rax : 11112222 FFFFEEEE 01234567 89ABCDEF
ffffffff 32-bit: -1.
ffffffff ffffffff 64-bit proper: -1.
00000000 ffffffff 64-bit improper: 4,294,967,295.
为了正确地进行符号扩展,如果最右边的位(rax
)形成负数,则最左边的位(rdx
)应全部为一位,否则全部为零位
当然,那些聪明的英特尔工程师已经想到了这个用例,所以您可以使用cqo
将四字转换为八字
指令来实现,该指令的符号扩展正确。考虑到这一点,设置eax
的代码将变成:
mov rax, rdi ; Get dividend and
cqo ; sign extend to rdx:rax.
但是,您可能还有一个额外的问题。尽管System V x86-64 ABI指定参数在64位寄存器(
rXX
)中传递,但传递32位值实际上可能会留下包含垃圾的高位(我认为您也可以在返回值的上部留下垃圾。有关详细信息,请参阅)
因此,您不应该假设在整个64位寄存器中只有最右边的32位具有sane值
在您的情况下(假设为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
,因为我非常确定这不是必需的。它似乎没有损坏(此函数既不更改它,也不调用任何其他可能更改它的函数),而且似乎您从未在原始代码中正确还原过它。您的代码也因负除数而被破坏:除(5,-2)
将得到零。这纯粹是通过调用约定来解释的。您的零扩展名而不是符号扩展名错误(请参阅@paxdiablo的答案)只有负红利才重要
您告诉编译器您的函数采用
int
args,并且int
是x86-64 System V调用约定中的32位类型
您假设您的输入是符号扩展到64位的,但调用约定并不要求这样,因此编译器不会在10字节mov r64、imm64上浪费代码大小,因为它可以使用5字节mov r32、imm32
有关更多详细信息,请参见以下问答(第二个基本上是第一个问答的副本):
因此,编译器将为main
发出如下代码:
mov edi, 5 ; RDI = 0x0000000000000002
mov esi, -2 ; RSI = 0x00000000FFFFFFFE
call _divide
我检查过了,这就是gcc和clang真正做到的,即使对于未优化的代码也是如此
对于除以(5,-2)
,您的代码将导致
- RDX=0,RAX=5。即股息=0x0000000000000000:00000000000000005,这是正确的。(零和符号扩展对于非负输入是相同的操作)
- 除数=0x00000000FFFFFE=+4294967294,大且为正
64位idiv
计算5/4294967294
产生商=RAX=0,余数=RDX=5
如果您只修复了类型宽度/操作数大小不匹配的错误,您仍然会遇到诸如@paxdiablo的答案所解释的负红利问题。但是这两个修复对于除法(-554,2)
实际工作是必要的。
那么你应该怎么写呢?
您可以将原型更改为int64\t
或long
(在x86-64系统V中为64位),并使用cqo
设置有符号除法。()
或者您可以使用movsxd-rax、edi
/movsxd-rcx、esi
将32位输入扩展到64位。但这太傻了。只需使用32位操作数大小,因为这是您告诉编译器要传递的
这很好,因为64位除法比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
无需推送RBP;我们不调用任何其他函数,因此重新调整堆栈并不重要,我们也不修改RBP以用作帧指针
我们可以在不保存/恢复RDX的情况下关闭它:它是x86-64 System V和Windows x64中的一个调用关闭寄存器(与大多数32位调用约定相同)。这是有意义的,因为一些常见指令(如idiv
)会隐式使用它
如果您用C编写gcc和clang(当然启用了优化),它就会发出这样的信息
int divide(int dividend, int divisor) {
return dividend / divisor;
}
(请参阅上面的Godbolt链接,其中我将其与\uuuu属性(noinlin)一起包含。)
mov edi, 5
mov rsi, -2
call _divide