这不是众所周知的,但是 MinGW 和 MinGW-w64 中的头文件 <excpt.h> 提供了宏 __try1 和 __except1 来生成 gcc 内联汇编来处理异常。这些宏没有文档记录并且不容易使用。它变得更糟。 __try1 和 __except1 的 x86_64 版本与 32 位版本不兼容。它们使用具有不同参数和不同返回值的不同回调。
几个小时后,我几乎有了 x86_64 上的工作代码。我需要声明一个与_gnu_exception_handler in MinGW's runtime 具有相同原型的回调。我的回调是
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 %u\n", 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, @except\n\t"
".seh_handlerdata\n\t"
".long 1\n\t"
".rva .l_startw, .l_endw, ehandler, .l_exceptw\n\t"
".section .text.startup, \"x\"\n"
".l_startw:"
:::: except );
sum = sum1to(n);
__asm__ (".l_endw:");
printf("The sum from 1 to %u is %u\n", 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() 来处理堆栈溢出。