克服 x86 idiv #DE 异常

Overcoming the x86 idiv #DE exception

回复:x86 汇编语言 -

我有三个 32 位有符号数:n1、n2 和 n3。

我想用 n2 模拟 n1 以获得 64 位签名结果。

然后我想将该 64 位结果除以 n3。

问题是如果64位有符号结果足够大and/or如果n3足够小,会导致溢出,idiv会抛出#DE异常。

如果 idiv 只是在溢出时设置 #DE,我可以检查以确认 ((n1 * n2) / n3) * n3 + ((n1 * n2) mod n3) = (n1 * n2)。如果没有,就会发生溢出,我可以相应地进行。

但是#DE不善于与人相处。当它被提升时,它报告 "Program has stopped working" 然后把你踢出去。

所以我要么需要找到一些方法来预先检查 idiv 是否会在我进行除法之前导致溢出,要么我需要用汇编语言做相当于 try ... catch 的操作。

我已经在互联网上搜索过(包括这里),总体上发现很少;没有什么特别有用的。

我试过在 C++ 中内联代码 try ... catch 无济于事 - 它仍然报告 "Program has stopped working" 然后把你踢出去。

例如,有两个支持文件:

// TC.h
// Version 1.0.0
// MDJ 2016/05/06

extern "C" int q;
extern "C" int m;

// TC.s
// Version 1.0.0
// MDJ 2016/05/06

    .globl _q
    .globl _m

    .bss
    .align 4
_q:
   .space 4

_m:
    .space 4

此文件运行完成并产生正确的结果:

// TryCatch.cpp
// Version 1.0.0
// MDJ 2016/05/06

#include <iostream>
#include "TC.h"

using namespace std;

int main() {

    cout << endl;

    try {

        # AT&T syntax
        asm(
            "movl       ,    %eax\n\t"
            "movl       ,    %edx\n\t"
            "imull  %edx\n\t"
            "movl       ,    %ecx\n\t"
            "idivl  %ecx\n\t"
            "movl       %eax,   _q\n\t"
            "movl       %edx,   _m\n\t"
        );
    }
    catch(int e) {
        cout << "Caught." << endl;
    }

    cout << "Reached the End." << endl;
    cout << "q = " << q << endl;
    cout << "m = " << m << endl;
    cout << endl;

    return 0;
}

但是,如果我像这样更改 n1、n2 和 n3:

// TryCatch.cpp
// Version 1.0.0
// MDJ 2016/05/06

#include <iostream>
#include "TC.h"

using namespace std;

int main() {

    cout << endl;

    try {

        # AT&T syntax
        asm(
            "movl       4567890, %eax\n\t"
            "movl       3456789, %edx\n\t"
            "imull  %edx\n\t"
            "movl       , %ecx\n\t"
            "idivl  %ecx\n\t"
            "movl       %eax,   _q\n\t"
            "movl       %edx,   _m\n\t"
        );
    }
    catch(int e) {
        cout << "Caught." << endl;
    }

    cout << "Reached the End." << endl;
    cout << "q = " << q << endl;
    cout << "m = " << m << endl;
    cout << endl;

    return 0;
}

"catch" 没有捕捉到溢出,系统反而报告 "Program has stopped working" 然后把你踢出去。

如有任何帮助,我们将不胜感激。

我突然想到我完全走错了路(作为一名模型铁路工人,这真是令人发指的罪行)双关语:-)。

但是,真的,我一直在努力解决这个问题。

相反,我应该采取简单的方法:我应该回到 1950 年代的文法学校和我的第一次长除法冒险。

与其对 EDX:EAX 除以 ECX 感到困惑,不如考虑将两位数(无符号)数除以一位数(无符号)数。

现在,两位数就是股息,它有个位和十位。所以它可以在 0 到 99 之间变化。

而且,一位数是除数,而且只有个位。因此,它可以在 1 和 9 之间变化(因为不允许除以零)。

例如,考虑 77 除以 2:

                            3 8
                           _____
                        2 | 7 7
                            6
                            _
                            1 7
                            1 6
                            ___
                              1

所以,结果是:商是38,余数是1。

但是,在这里,就像被除数一样,我们允许商也有两位数:十位数和个位数。如果我们改为将商限制为只有个位数,会发生什么情况。

然后我们可以调用任何除法,这会导致商在十位数字段中有任何非零数字,溢出 !!!

但是,那么,产生这种溢出所需的条件是什么:小于或等于股息的十位数字的任何除数!

同理,EDX:EAX除以ECX,如果ECX <= EDX 就会溢出!!!

这就是我们对溢出的简单测试:

                        ECX <= EDX

这适用于无符号除法。

有符号除法溢出的预检查要复杂得多。我认为这会起作用,但我仍在测试。

从 EDX:EAX 中的 64 位带符号被除数开始,在 ECX 中从 32 位带符号除数开始。那么:

  # Temporarily save the dividend
  movl  %edx, _dividendHigh                     # Most-significant 32b
  movl  %eax, _dividendLow                      # Least-significant 32b

  # Check the divisor for zero
  testl %ecx, %ecx                              # Is divisor = 0 ?
  jz    _DivideByZero                           # Go if Yes

  # Check the divisor for +/- 1
  cmpl  , %ecx
  je    _dChkA                                  # Go if divisor =  1
  cmpl  $-1,    %ecx
  je    _dChkA                                  # Go if divisor = -1
  jmp   _dChkC                                  # Else continue

_dChkA:
  # If dividendHigh < -1 or > 0 and divisor = +/- 1
  #   then overflow will occur.
  cmpl  $-1,        %edx
  jl    _DivideOverflow                         # Go if divHigh < -1
  cmpl  [=12=],     %edx
  jg    _DivideOverflow                         # Go if divHigh >    0

  # If dividendHigh = -1 and bit 31 of dividendLow = 0
  #   and divisor = +/- 1 then overflow will occur.
  cmpl  $-1,    %edx
  jne   _dChkB                                  # Go if divHigh <>  -1
  bt    ,    %eax
  jnc   _DivideOverflow                         # Go if divLow b31 = 0

_dChkB:
  # If dividendHigh = 0 and bit 31 of dividendLow = 1
  #   and divisor = +/- 1 then overflow will occur.
  cmpl  [=12=], %edx
  jne   _dChkC                                  # Go if divHigh <>   0
  bt    ,    %eax
  jc    _DivideOverflow                         # Go if divLow b31 = 1

  # Check for non-unary overflow
  #   Overflow will occur if the 
  #   most-significant 33b can be
  #   divided by the divisor. NOTE:
  #   that's 33 bits, not 32, 
  #   because all numbers are signed.

  # Do dividend shift and manual sign extension
  # Check bit 63 to determine if dividend is positive or negative
_dChkC: 
  bt    ,    %edx
  jc    _dChkD                                  # Go if negative

  # Do the 64-bit shift                         # Positive
  # First, shift the Least-significant
  #   32b left 1 bit (bit 32 --> CF).
  shll  , %eax

  # Then, rotate the Most-significant
  #   32b left, through the carry, 1 bit
  #   (CF --> bit 1 then bit 32 --> CF).
  rcll  , %edx

  # Move it to %eax and manually positive-sign extend it
  movl  %edx,   %eax
  jmp       _dChkE

_dChkD:                                             # Negative  
  # Do the 64-bit shift                                     
  # First, shift the Least-significant
  #   32b left 1 bit (bit 32 --> CF).
  shll  , %eax

  # Then, rotate the Most-significant
  #   32b left, through the carry, 1 bit
  #   (CF --> bit 1 then bit 32 --> CF).
  rcll  , %edx

  # Move it to %eax and manually negative-sign extend it
  movl  %edx,   %eax
  movl  $-1,    %edx

  # Do the Test Divide of the 
  #   Most-Significant 33b
_dChkE:
  idivl %ecx                                    # EDX:EAX / ECX
                                                #   EAX = test quotient
                                                #   EDX = test remainder
  testl %eax,   %eax
  jnz       _DivideOverflow                     # Go if Quotient <> 0

  # Get the full dividend
  movl  _dividendHigh,  %edx                    # Most-significant 32b
  movl  _dividendLow,   %eax                    # Least-significant 32b

  # Perform the 64b by 32b division
  idivl ecx                                     #   EDX:EAX / ECX
                                                #     EAX = quotient
                                                #     EDX = remainder

你的DivideTester太可笑了。您只需要保留调用者的 %ebx%esi%edi%ebp%esp。您似乎在同一个函数中 saving/restoring 大量寄存器,并在最后多次恢复同一个寄存器。

试试这个:

.globl _DivideTester
_DivideTester:
# extern "C" void DivideTester(void);
# clobbers eax and edx.  The C compiler will assume this, because the standard calling convention says functions are allowed to clobber eax, ecx, and edx

    # mov    [=10=],       %edx
    # mov    42542, %eax
    # Instead, set the globals from C, and print before calling

    mov    _dividendHigh, %edx        # load from globals
    mov    _dividendLow,  %eax
    # movl    _divisor, %ecx

    idivl   _divisor                  # EDX:EAX / divisor
    mov    %eax, _quotient            #       EAX = Quotient
    mov    %edx, _remainder           #       EDX = Remainder

    # print the results from C, or use a debugger to inspect them
    ret

或者,如果您坚持将常量硬编码到 asm 中,您仍然可以这样做。您仍然可以从 C 打印它们。

注意到这个函数更容易阅读了吗?除了 idiv 之外,基本上没有什么可以出错的。从 asm 中获得正确的函数调用需要更多的工作,所以不要在这上面浪费你的时间。让编译器做它擅长的事。你仍然可以通过反汇编/单步执行它的代码来准确地看到编译器做了什么,所以这并不是说你因为将那部分留给 C 而失去了调试能力。它更像是你避免了整个 类 错误(就像你一开始的那样)。

您只需要像 mov 34, _memory 这样的东西加上操作数大小后缀,其中没有寄存器来暗示操作数大小。我宁愿省略它。如果它不明确,as 会给你一条错误消息而不是选择默认值,所以它总是安全的。