异常编译代码分析

异常编译代码分析

https://lowlevelbits.org/compiling-ruby-part-5/

调用堆栈、堆栈帧和程序计数器

在程序执行期间,机器维护指向正在执行的指令的指针。它被称为程序计数器(或指令指针)。

当调用一个方法时,程序计数器被设置为被调用函数(被调用者)上的第一条指令。一旦子方法完成执行,程序就需要知道如何返回调用站点。

该信息通常使用调用堆栈的概念来维护。

请考虑下面的程序及其右侧的调用堆栈。

 

调用堆栈由堆栈帧组成。每当调用函数时,都会创建一个新的堆栈帧并将其推送到堆栈上。当被调用的函数返回时,堆栈帧将弹出。

在每一点上,调用堆栈都表示实际的堆栈跟踪。

调用堆栈的最顶部表示整个文件的范围,后面是第一个函数的堆栈帧,后面是第二个函数,依此类推。在Ruby中,top函数/文件作用域被简称为top。

现在,假设想将一些信息从第二个函数传递到顶部。发生了一些错误或异常情况,此特定程序状态需要一些特殊处理。

有几种有限的方法来处理这种情况:要么返回一些特殊的值up(因此,调用堆栈上的每个函数都应该知道这一点),要么可以使用一些全局变量与调用方通信(例如,C中的errno),这会再次通过调用堆栈污染业务逻辑。

更优雅地处理这个问题的一种方法是使用特定的语言结构——异常。

可以抛出/引发异常,然后在顶部添加特殊处理,而不是污染整个调用堆栈,如下图所示:

 

堆叠放卷

现在,问题是:如何实现这个功能?为了回答这个问题,了解需要发生什么!

在调用顶部的第一个函数之前,程序处于某种特定状态。现在,程序在第二个函数中的提升错误行附近处于另一个特定状态。

需要以某种方式恢复第一次调用之前的状态,并在顶部的救援之后继续执行(通过相应地更改程序计数器)。

从概念上讲,可以在调用第一个方法并稍后恢复它之前保存机器状态。问题是存储整个机器的状态过于昂贵,并且节省了超出所需的成本,从而增加了开销。

相反,可以将维护程序的责任推给实际的程序开发人员。

大多数语言都提供了处理此问题的有用功能:

Ruby有明确的确保块

Java有明确的finally语句

C++具有RAII和隐式析构函数

(C有setjmp/longjmp,但只讨论有用的特性)

以下是Ruby的工作原理。

每当抛出异常时,程序都会爬上调用堆栈,并从这些终结器执行代码,直到到达异常处理程序。

此过程称为堆栈放卷。

这里是一个更新的例子,在堆栈展开期间显式恢复状态。

如果不执行来自确保块的代码,假设的锁将永远不会被释放,从而以可怕的方式破坏程序。

 

从块返回

return语句的行为不同,这取决于它们所属的词法范围。

有一个小谜题。

将在屏幕上打印的内容:

 

 

 Ruby中的异常

现在,可以讨论Ruby中不同类型的异常。有三种不同的类型:

实际引发的异常

break语句

return声明

break和return语句在Procs上下文中使用时都具有特殊意义。

用例子详细说明这三个方面。

正常例外情况

实际的异常会爬上堆栈,调用终结器直到找到异常处理程序。

这些都是大家熟悉的正常例外情况。

return是从块内调用的。您可能期望x*4从块返回,但它是从封闭函数(词法范围)返回的。

 

返回x*4将从f而不是从块返回。

代码打印

2: 8

而不是

1: 8

2:42

breaks

与返回类似,breaks允许从封闭函数返回,但方式略有不同。

这是这里最复杂的例子。

 

top调用循环函数并将块传递给它。块只是引擎盖下的另一个函数;在这里作为__anonymous_block单独呈现。

Runtime为循环创建一个新的堆栈帧,并将其放在调用堆栈上。

循环调用传递的块(__anonymous_block)。

Runtime为__anonymous_block创建新的堆栈帧,并将其放在堆栈上。

__anonymous_block递增i,检查是否相等,然后返回到循环,没有什么特别的。

Runtime从调用堆栈中删除__anonymous_block堆栈帧。

循环堆栈帧保留在调用堆栈上,while true的下一次迭代再次调用__anonymous_block。

Runtime为__anonymous_block创建新的堆栈帧,并将其放在堆栈上。

__anonymous_block递增i,检查是否相等,并调用break。

break启动堆栈展开并从封闭函数(循环)返回。请参见虚线。

循环返回,从而在为true时绕过无休止循环。

break构造实际上等效于以下代码:

实施

上面描述的所有语言构造(块中的异常、返回和中断)的行为都类似:它们展开堆栈(在向上的过程中调用终结器)并在某个定义明确的点停止。

它们在原始mruby运行时中的实现略有不同。尽管如此,还是将它们全部作为异常来实现,返回和中断是特殊的异常:需要携带一个值并存储关于在哪里停止解除过程的信息。

考虑以下示例:

 

救援和确保后的障碍物称为着陆垫。

此示例有两种着陆台:捕获(救援)和清理(确保)。捕获是“有条件的”着陆台:只有当异常类型与其类型匹配时,才会执行捕获。注意最后一个救援:它没有附加任何类型,所以它只会捕获任何异常。

相反,清理是无条件的——它们将始终运行,但也会将异常转发到调用堆栈上的下一个函数。

这个例子中的另一个重要细节是第二个救援:它使用函数参数作为类型。也就是说,着陆台的类型只有在运行时才知道,它可以是任何东西。

例如,在C++中,所有的catch类型都必须预先知道,并且编译器会发出特殊的运行时类型信息(RTTI)。再说一遍,IMO,它应该是编译时间类型信息,但它是C++…

因此,Ruby虚拟机总是进入每个着陆台。对于捕获,它首先(在运行时!)检查异常类型是否与平台的类型匹配,如果匹配,则将异常标记为捕获,并继续执行平台。

如果异常类型不匹配,则会立即重新抛出异常,以便下一个着陆台可以尝试接住它。

MLIR

描述一下如何在MLIR级别建模异常的,但这需要更多的时间,原因有几个:

由于异常的工作方式(即,一些寄存器必须溢出堆栈),最初立即构建SSA的方法不起作用,所以方言发生了一些变化,需要对它们进行一些清理

目前对它们建模的方式更像是一种破解,只是因为有某些约定,所以它还不是一个可靠的模型

添加了JIT支持(对Kernel.eval),并需要在那里进行一些调整,以使异常在实时评估中起作用

参考文献链接

https://lowlevelbits.org/compiling-ruby-part-5/

posted @ 2024-04-04 04:36  吴建明wujianming  阅读(4)  评论(0编辑  收藏  举报