将VC++的__try/__except EXCEPTION_STACK_OVERFLOW移植到MinGW

Porting VC++'s __try/__except EXCEPTION_STACK_OVERFLOW to MinGW

本文关键字:OVERFLOW STACK MinGW except VC++ try EXCEPTION      更新时间:2023-10-16

我正在尝试使用vc++的try-except语句移植一些代码到MinGW:

bool success = true;
__try {
    //...
} __except ((EXCEPTION_STACK_OVERFLOW == GetExceptionCode())
            ? EXCEPTION_EXECUTE_HANDLER
            : EXCEPTION_CONTINUE_SEARCH) {
    success = false;
    _resetstkoflw();
}
return success;

是否可以使用mingw++编写捕获堆栈溢出异常的代码?

你需要手动调用注册异常处理的Windows API函数;也就是说,AddVectoredExceptionHandler。注意,通过使用不尊重SEH异常的MinGW,抛出任何SEH异常或试图捕获任何此类异常将导致未定义的行为,因为正常的c++堆栈展开语义没有完成。(Windows怎么知道销毁堆栈上的所有std::string ?)

您还需要在希望调用SEH异常处理程序的时间结束时调用RemoveVectoredExceptionHandler

一般来说,MinGW缺乏对Windows特性(如SEH和COM)的支持。你为什么要用它而不是msvc++(考虑到这两个编译器都是免费的?)

这一点并不为人所知,但是MinGW和MinGW-w64中的头文件<excpt.h>提供了宏__try1__except1来生成用于处理异常的gcc内联程序集。这些宏没有文档记录,也不容易使用。更糟的是。__try1__except1的x86_64版本与32位版本不兼容。它们使用不同的回调函数,带有不同的参数和不同的返回值。

几个小时后,我几乎有了x86_64上的工作代码。我需要在MinGW的运行时中声明一个具有与_gnu_exception_handler相同原型的回调。我的回调是

long CALLBACK
ehandler(EXCEPTION_POINTERS *pointers)
{
    switch (pointers->ExceptionRecord->ExceptionCode) {
    case EXCEPTION_STACK_OVERFLOW:
        return EXCEPTION_EXECUTE_HANDLER;
    default:
        return EXCEPTION_CONTINUE_SEARCH;
    }
}
我的try-except代码是
    __try1 (ehandler) {
        sum = sum1to(n);
        __asm__ goto ( "jmp %l[ok]n" :::: ok);
    } __except1 {
        printf("Stack overflow!n");
        return 1;
    }
ok:
    printf("The sum from 1 to %u is %un", n, sum);
    return 0;

它工作,直到我启用优化与gcc -O2。这会导致汇编错误,因此我的程序不再编译。__try1__except1宏被gcc 5.0.2中的一个优化打破了,它将函数从.text移到了不同的部分。

当宏工作时,控制流是愚蠢的。如果发生堆栈溢出,程序跳过__except1。如果没有发生堆栈溢出,程序将通过__except1落到相同的位置。我需要我的奇怪的__asm__ goto跳转到ok:,以防止掉线。我不能使用goto ok;,因为gcc会删除__except1无法访问。

又过了几个小时,我把程序修好了。我复制并修改了汇编代码,以改善控制流程(不再跳转到ok:),并在gcc -O2优化中幸存下来。这段代码很难看,但对我来说很好用:
/* gcc except-so.c -o except-so */
#include <windows.h>
#include <excpt.h>
#include <stdio.h>
#ifndef __x86_64__
#error This program requires x86_64
#endif
/* This function can overflow the call stack. */
unsigned int
sum1to(unsigned int n)
{
    if (n == 0)
        return 0;
    else {
        volatile unsigned int m = sum1to(n - 1);
        return m + n;
    }
}
long CALLBACK
ehandler(EXCEPTION_POINTERS *pointers)
{
    switch (pointers->ExceptionRecord->ExceptionCode) {
    case EXCEPTION_STACK_OVERFLOW:
        return EXCEPTION_EXECUTE_HANDLER;
    default:
        return EXCEPTION_CONTINUE_SEARCH;
    }
}
int main(int, char **) __attribute__ ((section (".text.startup")));
/*
 * Sum the numbers from 1 to the argument.
 */
int
main(int argc, char **argv) {
    unsigned int n, sum;
    char c;
    if (argc != 2 || sscanf(argv[1], "%u %c", &n, &c) != 1) {
        printf("Argument must be a number!n");
        return 1;
    }
    __asm__ goto (
        ".seh_handler __C_specific_handler, @exceptnt"
        ".seh_handlerdatant"
        ".long 1nt"
        ".rva .l_startw, .l_endw, ehandler, .l_exceptwnt"
        ".section .text.startup, "x"n"
        ".l_startw:"
            :::: except );
    sum = sum1to(n);
    __asm__ (".l_endw:");
    printf("The sum from 1 to %u is %un", n, sum);
    return 0;
except:
    __asm__ (".l_exceptw:");
    printf("Stack overflow!n");
    return 1;
}

你可能想知道Windows如何在全栈上调用ehandler()。所有这些对sum1to()的递归调用必须留在堆栈上,直到我的处理程序决定做什么。Windows内核中有一些神奇的东西;当它报告堆栈溢出时,它还映射一个额外的堆栈页,以便ntdll.exe可以调用我的处理程序。如果我在处理程序上放置一个断点,我可以在gdb中看到这一点。在我的机器上,堆栈增长到地址0x54000。来自sum1to()的堆栈帧在0x54000处停止,但是异常处理程序在从0x53000到0x54000的堆栈的额外页面上运行。Unix信号没有这样的魔力,这就是为什么Unix程序需要sigaltstack()来处理堆栈溢出。

您可能需要查看LibSEH,以便为MinGW添加结构化异常处理兼容性。

MinGW不支持结构化异常的关键字;但是,正如Billy O'Neal在他的回答中所说,你可以调用某些本机函数来获得相同的效果。

问题是你是否想要同样的效果。我坚信结构化异常是一个错误。操作系统将告诉您的结构化异常列表包括"试图将整数除以0","无法使用传递给函数的HANDLE参数","试图执行非法机器码指令"answers"试图在未经许可的情况下访问内存"。对于这些错误,您确实不能做任何明智的事情,但是结构化异常给了您机会:(1)声称您已经有了,(2)允许程序再多走一段时间。最好找出为什么代码试图除以0,传递一个无效的HANDLE参数,试图访问内存未经允许这样做,等等,并修复代码,使其永远不会做

有一种观点认为,您可以使用结构化异常来检测问题、显示对话框并退出。我不确定这是否比让操作系统显示一个对话框并退出程序更好(特别是如果操作系统在进程中向您发送一个迷你转储),这是未处理异常的默认行为。