ben*_*wad 0 x86 assembly gnu-assembler divide-by-zero i386
我有以下功能,涉及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;
}
Run Code Online (Sandbox Code Playgroud)
我注意到,在一些情况下,我正在EXC_I386_DIV使用此功能崩溃.以下调用会产生这样的崩溃:
int res = MulDivRound( 4096, -566, 400 );
Run Code Online (Sandbox Code Playgroud)
我无法清楚地看到正在发生的事情导致此函数除以0:当然它只是移动4096 eax,然后乘以-566,然后将其除以400,返回除法运算结果的两个分量.任何人都可以对此有所了解吗?
x86中的除法/乘法指令...这段代码中有一些错误:
您正在使用带有unsigned mul/divoperations的带符号操作数.因此,您真正执行的操作是:
-566(0xfffffdca作为2补码32位)被解释为无符号42949585384096得到17592183726080(0xfff:0xffdca000in EDX:EAX).注意转换为你期望的低 32位-2318336400但由于高32位是0xfff,4095),结果超出UINT32_MAX并引发异常.如果你通过在xor %%edx,%%edx前面插入一个清除高位32位divl,操作将会成功,但它会返回你不期望的东西 - 也就是说,它通过导入in ()和其余的()in 来分割0xffdca000(4292648960).4000xa3c06610731622EAX0xa0160EDX
就你指示机器做什么而言,这是"正确的",但不是你所期望的.如果您想使用带符号的数字,则需要imul/ idiv代替.
装配最终可简化为以下内容:
__asm__ __volatile__ (
"imull %3 \n"
"idivl %4 \n"
: "=a" (nRet),
"=&d" (nMod)
: "a" (nNumber),
"mr" (nNumerator),
"mr" (nDenominator)
: "cc"
);
Run Code Online (Sandbox Code Playgroud)
这是因为gcc允许指定哪些寄存器用作输入/输出,因此这里根本不需要数据移动.此外,"m"仅限约束在64位上创建次优代码,因为它强制参数进入堆栈; 给它一个替代方案,生成的代码会更好.
编辑:刚刚将nMod约束更改为"=&d"(nMod); 它需要成为gcc所谓的"早期咒语".这意味着在消耗/使用所有输入操作数之前覆盖指定的输出寄存器,并告诉编译器不要传入输入((nDenominator)特别是)EDX.否则,如果发生这种情况,它将导致"有趣"的失败模式.如果你只使用"m"for nNumerator/ nDenominator但是一旦允许寄存器,这不是问题,最好小心一点.
Edit2:另请注意,上述代码当然不能防止溢出异常.你仍然可以称之为MulDivRound(INT32_MAX, 4, 2)触发它们.合法地/按照这些说明的设计方式.如果你必须确保没有发生这种情况,你必须添加代码来比较分母EDX/ RDX之前的分母[i]div并处理它较小的情况.