我有以下功能,涉及GAS语法中的i386程序集片段:
inline int MulDivRound(
int nNumber,
int nNumerator,
int nDenominator )
{
int nRet, nMod;
__asm__ __volatile__ (
"mov %2, %%eax \n"
"mull %3 \n"
"divl %4 \n"
"mov %%eax, %0 \n"
"mov %%edx, %1 \n"
: "=m" (nRet),
"=m" (nMod)
: "m" (nNumber),
"m" (nNumerator),
"m" (nDenominator)
: "eax", "edx"
);
return nRet + nMod*2 / nDenominator;
}
我注意到,在少数情况下,我使用此功能遇到EXC_I386_DIV
崩溃。以下调用产生了这样的崩溃:
int res = MulDivRound( 4096, -566, 400 );
我无法清楚地看到导致此函数除以0的情况:当然它只是将4096移动到eax
,然后将其乘以-566,然后将其除以400,返回两个组件分工的结果。任何人都可以对此有所了解吗?
答案 0 :(得分:5)
除法/乘法指令...这段代码中出现了一些错误:
您已使用带有无符号 mul
/ div
操作的已签名操作数。因此,真正执行的操作是:
-566
(0xfffffdca
为2补全32位)被解释为无符号4294958538
4096
,从而导致17592183726080
(0xfff:0xffdca000
中的EDX:EAX
)。请注意转换为-2318336
的 lower 32位,因为您预期" 400
,但由于高位32位是0xfff
,4095
),结果超出UINT32_MAX
,例外情况是提高。如果您通过在xor %%edx,%%edx
之前插入divl
来清除高位32位,则操作会成功,但它会返回您不期望的内容 - 即,它会分开0xffdca000
4292648960
400
0xa3c066
导致10731622
EAX
0xa0
160
,其余为EDX
imul
1}})idiv
。
那"正确"至于你指示机器做什么,但不是你所期望的。如果您想使用带符号的号码,则需要__asm__ __volatile__ (
"imull %3 \n"
"idivl %4 \n"
: "=a" (nRet),
"=&d" (nMod)
: "a" (nNumber),
"mr" (nNumerator),
"mr" (nDenominator)
: "cc"
);
/ "m"
。
最终可以将装配简化为以下内容:
nMod
这是因为gcc允许指定哪些寄存器用作输入/输出,所以这里根本不需要数据移动。此外,仅"=&d"(nMod)
约束在64位上创建次优代码,因为它将参数强制到堆栈上;给它一个替代方案,生成的代码会更好。
修改刚刚将(nDenominator)
约束更改为EDX
;它需要成为gcc所称的"早期的破坏者"。这意味着在消耗/使用所有输入操作数之前覆盖指定的输出寄存器,并告诉编译器不要在"m"
中传递输入(特别是nNumerator
)。否则,如果发生这种情况,它将导致一个有趣的"故障模式。如果您仅使用nDenominator
/ MulDivRound(INT32_MAX, 4, 2)
的{{1}},这不是问题,但一旦允许寄存器,最好小心。
Edit2:另请注意,上述代码当然不能防止溢出异常。您仍然可以像EDX
一样调用它来触发它们。合法地/按照这些说明的设计方式。如果您必须确保不会发生这种情况,那么您必须添加代码,将RDX
/ [i]div
与{{1}} / {{1}}之前的分母进行比较并处理它变小了。
答案 1 :(得分:4)
您没有得到除零错误,而是溢出错误。
divl
除以rdx:rax / operand
(rdx中的高位字)并将结果存储在eax
中,其余部分存储在edx
中。
在您的代码中,您最终得到rdx=4095
和rax=0
,因此您尝试将75539416981840613867520 / 400
除以188848542454601534668 remainder 320
。
188848542454601534668
为0x 000a 3ccc cccc cccc cccc
,不适合32位结果寄存器eax
,因此出现溢出错误。
您需要确保rax
包含您的值4095
和rdx=0
。这在rax(result)和rdx(余数)中给出了正确的结果:
rax 0xa 10
rdx 0x5f 95