微软技术专家揭秘x86架构的神秘面纱

发表时间: 2022-04-21 09:38


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

END


程序员001-004》全面上市,对话世界级大师,报道中国IT行业创新创造


成就一亿技术人