克服 x86 idiv #DE 异常

Posted

技术标签:

【中文标题】克服 x86 idiv #DE 异常【英文标题】:Overcoming the x86 idiv #DE exception 【发布时间】:2016-05-07 06:10:22 【问题描述】:

回复:x86 汇编语言 -

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

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

然后我想通过 n3 将 64 位结果 idiv。

问题在于,如果 64 位有符号结果足够大和/或如果 n3 足够小,则会导致溢出并且 idiv 会抛出 #DE 异常。

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

但#DE 不适合与其他人相处。当它出现时,它会报告“程序已停止工作”,然后将您踢出。

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

我已经搜索过互联网(包括这里),但总体上几乎没有找到相关信息;没有什么特别有用的。

我尝试将代码内联到 c++ try ... 中,但无济于事 - 它仍然报告“程序已停止工作”,然后将你踢出。

以两个支持文件为例:

// 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       $34,    %eax\n\t"
            "movl       $48,    %edx\n\t"
            "imull  %edx\n\t"
            "movl       $13,    %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       $234567890, %eax\n\t"
            "movl       $123456789, %edx\n\t"
            "imull  %edx\n\t"
            "movl       $3, %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”没有捕获溢出,而是系统报告“程序已停止工作”,然后将您踢出。

任何帮助将不胜感激。

【问题讨论】:

在 Cygwin 的 Windows 上使用 GCC?明威? MSYS2?还是其他一些 Windows 环境? 你知道那些asm 语句不安全,对吧?他们破坏寄存器而不用操作数约束告诉编译器。如果您已经知道这一点,那么对 asm 语句的一个小评论会很好地避免未来的读者觉得有必要向您指出这一点。 (或者通过复制代码并实际使用类似的东西)。 在 Windows 上,您必须使用 GCC 不支持的操作系统的结构化异常处理 (SEH) 工具。您可以使用 Microsoft 的 C/C++ 编译器和 __try__except 关键字来捕获 SEH 异常。在 Linux 上,您需要使用信号处理程序。 无论您使用的是 32 位还是 64 位代码,Windows 下的 SEH 处理都是完全不同的。 (如果你的目标是 ARM 或其他一些非 x86 CPU,则不同。)如果你不能使用隐藏这些实现细节的__try__except,你需要清楚你是否正在创建一个32 位 x86 或 64 位 x64 Windows 可执行文件或两者兼有。 我不确定你想说什么。无论您是否恢复,Michael Petch 链接的 32 位 SEH 宏和类在 64 位可执行文件中都不起作用,所以正如我所说,您需要明确您实际构建的内容。如果您只在出现异常后中止,您可以使用AddVectoredExceptionHandler 安装类似 Unix 信号的处理程序。但是,如果您创建的不是 32 位 x86 可执行文件,那么您可能仍需要为您的程序集函数创建 SEH 展开信息(根据 Microsoft 的调用约定),因为“矢量化”异常基于 SEH。 【参考方案1】:

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

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

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

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

现在,两位数是被除数,它有一个个位和一个十位。所以它可以在 0 到 99 之间变化。

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

例如,考虑 77 除以 2:

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

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

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

然后我们可以调用任何除法,这导致商在十位数字段中具有除零以外的任何数字,AN OVERFLOW !!!

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

类似地,在 EDX:EAX 除以 ECX 时,如果 ECX 则会发生溢出

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

                        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  $1, %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  $0,     %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    $31,    %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  $0, %edx
  jne   _dChkC                                  # Go if divHigh <>   0
  bt    $31,    %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    $31,    %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  $1, %eax

  # Then, rotate the Most-significant
  #   32b left, through the carry, 1 bit
  #   (CF --> bit 1 then bit 32 --> CF).
  rcll  $1, %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  $1, %eax

  # Then, rotate the Most-significant
  #   32b left, through the carry, 1 bit
  #   (CF --> bit 1 then bit 32 --> CF).
  rcll  $1, %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

【讨论】:

从另一个方向来看,最大可能的商是INT_MAXUINT_MAX (0xFFFFFFFF)。因此最大的法定红利是ecx * INT_MAXecx * UINT_MAX,加上最大的余数。因此,溢出的最小除数产生的商 1 比它高,没有余数。所以是的,2^32 * ecx &lt;= edx:eax 导致溢出。这确实简化为ecx &lt;= edx。用长除法的类比做得很好,这就是让我这么想的原因。 我认为在签名的情况下,您仍然可以应用完全相同的规则(但使用签名比较)。不过,对ecx &lt;= edx(忽略eax)的简化不适用于负数。 INT_MIN / -1 溢出,因为 abs(INT_MIN) 不能表示为有符号整数。除此之外,您可能可以忽略eax,但您应该写下表达式(a_high &lt; b_high) || (a_high == b_high &amp;&amp; a_low &lt;= b_low),并检查是否简化了a_low = 0 的签名情况。全零 eax 给出的负数比全 1(2 的补偿)更大。 您可能想查看此处给出的 Window 的 MulDiv 的实现,它展示了如何有效地检查有符号溢出:blogs.msdn.microsoft.com/oldnewthing/20120514-00/?p=7633 请注意,由于您的函数最多只能检测一次溢出,因此最有效的实现将是检测异常或信号处理程序中的溢出。 @PeterCordes: (a_high 显然情况并非如此,否则它将无法正常工作。请记住,它实际上是用汇编编写的函数的 C 版本,因此 UInt32Div16To16 函数实际上是 DIV 指令。 if (result &lt; 0) goto overflow; 语句检查无符号除法是否“溢出”到符号位。在那种情况下,一个有符号的除法就会溢出。您的 abs(ECX) 【参考方案2】:

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

试试这个:

.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    $0,       %edx
    # mov    $6742542, %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 $1234, _memory 之类的东西加上后缀的操作数大小,其中没有暗示操作数大小的寄存器。我宁愿省略它。如果它不明确,as 会给你一个错误消息而不是选择一个默认值,所以它总是安全的。

【讨论】:

不幸的是,这并不能解决溢出问题。例如,(678152731 * -19) / 7 一击中 idivl 就会爆炸。 没错;当您的问题包含此功能的大版本时,它被发布为答案。它可能应该是一个评论,但我真的很想发布代码来告诉你我在说什么。 OTOH,它在某种程度上回答了当时问题的一部分。它的目的不仅仅是对 idiv 指令的包装,让您可以从 C 中使用它。 (更新:直到最近的as,只有mov 会在歧义上出错!像add $1, (%rdi) 这样的其他指令会选择 dword 操作数大小,因为它与某些古老的 Unix 事物具有明显的兼容性。现在你至少会收到警告。)

以上是关于克服 x86 idiv #DE 异常的主要内容,如果未能解决你的问题,请参考以下文章

异常处理

java课堂练习——异常

Jvm(49),指令集----异常处理指令

原创X86下ipipe接管中断/异常

如何在不重新启动服务器的情况下克服 Permgen 空间异常..,任何人都可以帮助我

深入理解Linux内核---中断和异常(x86平台)