微软技术专家谈x86架构的怪异之处

【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翻译,转载需注明来源出处。

译者 | 章雨铭 责编 | 张红月
出品 | CSDN(ID:CSDNnews)

以下为译文:

最近,我发现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的异常处理,成为一项相当复杂的工作。

猜你喜欢

转载自blog.csdn.net/csdnnews/article/details/124301287