【CSDN 编者按】1978年6月8日,Intel发布了新款16位微处理器“8086”,也同时开创了一个新时代:x86架构诞生了。x86指的是特定微处理器执行的一些计算机语言指令集,定义了芯片的基本使用规则,一如今天的x64、IA64等。它不仅成就了Intel如日中天的地位,也成为了一种业界标准,即使是在当今强大的多核心处理器上也能看到x86的身影。
微软技术专家Raymond Chen参与Windows开发已经25年了,在实际的操作中,他认为X86构架有很多奇怪之处。
原文地址:
https://devblogs.microsoft.com/oldnewthing/20220418-00/?p=106489
本文由CSDN翻译,转载需注明来源出处。
以下为译文:
最近,我发现x86架构还有一个和其他的架构不同的地方——Windows结构化异常的管理方式。
在Windows上,所有其他架构都是通过使用unwind代码和声明为元数据的其他信息来跟踪异常处理。如果在其他架构上单步执行一个函数,就不会看到与异常处理相关的任何指令。只有在发生异常时,系统才会在元数据中的异常处理信息中查找指令指针,并使用它来决定要执行的操作:应该运行哪个异常处理程序?哪些对象需要销毁?和其他诸如此类的问题。
但奇怪的是,在Windows上,x86在运行时跟踪异常信息。当控制进入一个需要处理异常的函数时(要么是因为它想要处理异常,要么只是因为它想在异常被抛出函数时运行析构函数),代码必须在一个通过堆栈的链接列表中创建一个条目,并以.NET中的值为锚。在Microsoft Visual C++的实现中,链接列表节点还包含一个整数,代表当前函数的进度,每当需要销毁的对象列表发生变化时,该整数就会被更新。它在一个对象的构建完成后立即更新,并在对象的销毁开始前立即更新。fs:[0]
这个特殊的整数是一个非常麻烦的问题,因为优化器视其为废弃储存,想把它优化掉。的确,有时它确实是废弃储存,但有时它不是。
struct S { S(); ~S(); };
void f1();
void f2();
S g()
{
S s1;
f1();
S s2;
f2();
return S();
}
此函数的代码生成过程如下:
struct ExceptionNode
{
ExceptionNode* next;
int (__stdcall *handler)(PEXCEPTION_POINTERS);
int state;
};
S g()
{
// Create a new node
ExceptionNode node;
node.next = fs:[0];
node.handler = exception_handler_function;
node.state = -1; // nothing needs to be destructed
// Make it the new head of the linked list
fs:[0] = &node;
construct s1;
node.state = 0; // s1 needs to be destructed
f1();
construct s2;
node.state = 1; // s1 and s2 need to be destructed
f2();
construct return value;
node.state = 2; // s1, s2, and return value need to be destructed
node.state = 3; // s1 and return value need to be destructed
destruct s2;
node.state = 4; // return value needs to be destructed
destruct s1;
}
每当 "需要销毁的对象 "的列表发生变化时,就会更新unwind状态变量。就优化器而言,所有这些更新看起来都是废弃储存,因为似乎没有人读它们。.state
但确实有人读它们:.the。问题是,对the的调用是不可见的。当一个异常被或函数抛出时,它被调用,或者被对象的析构器调用。
但是,其中有些真的是废弃储存。例如,2的赋值是一个废弃储存,因为它后面紧跟着3的存储,中间没有任何东西,所以当值是2的时候,不会有异常发生。同样,3的存储是废弃的,因为3的析构器是隐含的。当破坏.node.stateSnoexcepts1时,不可能发生异常。
如果或改为.f1f2noexcept,废弃储存就可能被消除。
因此,优化器进退两难。它想消除废弃储存,但识别废弃储存的简单算法在这里不起作用,因为有可能出现异常。
Coroutine使情况变得更糟:当一个coroutine暂停时,异常处理节点需要从堆栈中复制到coroutine框架中,然后从堆栈框架中删除。而当协程恢复时,状态需要从协程框架复制回堆栈,并链接到异常处理程序链中。
确切地知道何时执行此操作取消链接和重新链接是很困难的,因为你仍然必须捕获其中发生的异常,并把它们存储在promise中。但这很可能不可行,因为在返回之前,coroutine可能已经恢复并运行到完成。
.await_suspendawait_suspendawait_suspend
void await_suspend(coroutine_handle<> handle)
{
arrange_for_resumption(handle);
throw oops; // who catches this?
}
抛出的异常被coroutine框架捕获,该框架调用.NET Framework。但是promise可能已经不存在了!
promise.unhandled_exception()
处理所有这些情况使得x86上的异常处理,特别是x86上的coroutine的异常处理,成为一项相当复杂的工作。
END
《新程序员001-004》全面上市,对话世界级大师,报道中国IT行业创新创造
成就一亿技术人