原文地址:https://mentorembedded.github.io/cxx-abi/exceptions.pdf
本节描述了编译器生成的数据,使运行时能够找到关于在异常发生时所采取行动的合适信息。
概览
从当前PC查找异常处理信息的过程总结在下图:
所有的表都在“代码”空间。由typeinfo指针指向的类型由一个GP相对偏移确定。
系统回滚表
这些都描述在《64-Bit Runtime Architecture andSoftware Conventions for IA-64》[2]。C++异常处理最重要的域是回滚表项的“start”域。调用点被保存为到该程序片段开头的相对偏移。注意一个程序可能被分解为多个程序片段。
如果一个程序被分解并导致多个程序片段,着陆场(landing pad)可以位于任何可能的片段中,甚至可能有一个特定用于着陆场的片段,这通常对应不常执行的代码。不过,目前没有一个程序片段对应多个着陆场片段的特殊配置(例如,“热”与“冷”着陆场)。
这只能通过为每个这样的片段复制回滚表项及LSDA来实现。另一个考虑的方案,其中一个比特将表示一个着陆场是相对于程序片段还是着陆场片段,但获得的好处远不能弥补空间的损失。
语言特定数据区域
语言特定数据区域(LSDA)包含指向相关数据的指针,一组调用点以及一组活动记录。每个来自C++代码(名义上一个函数)的程序片段有自己的LSDA。LSDA的几个部分使用LEB128压缩方案,这在“解码异常记录”一节描述。
LSDA头
LSDA头包含应用在一个程序片段的域。目前,定义了两个域:
· 着陆场开始点指针。这是一个到该程序片段着陆场代码开头的自相对偏移。在调用点表中的着陆场域相对于这个指针。0值表示LSDA是空的。低4位比特保留。值0000意味着有一个类型表指针。值0001意味着没有类型表指针。在本文档的余下部分,这个地址称为LPStart。
· 类型表指针。这是一个到类型表(捕捉条款与异常描述类型)的自相对偏移,在“捕捉条款”一节描述。如果着陆场偏移的低4位的值是0001,这个字不存在。在本文档的余下部分,这个地址称为TTBase。
调用点表
调用点表是一组可能在该程序片段中抛出一个异常(包括C++的throw语句)的调用点。它紧跟着LSDA头。每个项表示,对于一个给定的调用,第一个对应的活动记录与对应的着陆场。
这个表的开头是字节数,它保存为一个LEB128压缩的无符号整数。随后紧跟着记录。它们以调用点地址升序排序。每个记录指出:
· 调用点的位置,
· 着陆场的位置,
· 该调用点的第一个活动记录。
调用点记录域:
调用点 相对于之前调用点的偏移,以16字节块为单位[1]。第一个调用点相对于程序片段的开头计数。
着陆场 以16字节块为单位的,相对于LPStart地址的偏移。
活动记录 相对于活动表起始,第一个相关活动记录的偏移。这个值偏差1(1表明活动表的起始),而0表明没有活动存在。
着陆场表的所有域使用LEB128编码压缩(在“解码异常记录”一节描述)。
在调用点表中一个缺失的项表明一个调用不应该抛出异常。这样的调用包括:
· 在清理代码中析构函数的调用。C++语义禁止这些调用抛出异常。
· 对标准库中已知不会抛出异常的固有函数的调用(sin,memcpy)。
如果对一个给定的调用,该例程找不到调用点项,它将调用terminate()。
活动表
在LSDA中活动表跟在调用点表之后。每个记录是两个类型之一:
· 捕捉条款,在“捕捉条款”一节描述。
· 异常规范,在“异常规范”一节描述。
这两个记录类型有相同的格式,仅有很小的差别。它们由“switch value”域来区分:捕捉条款有正的切换值,而异常规范有负的切换值。值0表示一个catch-all条款。
活动记录域:
类型过滤器: 由运行时用来匹配抛出异常的类型与捕捉条款的类型或异常规范的类型。
活动记录: 到下一个活动的自相关的有符号字节数位移,0表示没有下一个活动记录。
所有的域使用LEB128编码进行压缩(在“解码异常记录”一节描述)。活动表的结构由C++前端确定,但受制于内联与其他优化。代码生成负责分配实际的切换值与“下一个记录”偏移。
捕捉条款
跟随同一个try的捕捉条款的代码类似于一个switch语句。捕捉条款活动记录通知运行时关于一个捕捉条款的类型及相关的切换值。
注意在以一个不同的类型抛出一个异常时,运行时可能应用某些转换(参考《ISO C++Final Draft International Standard》15.3.3节[except.handler]中的可接受转换)。因此,例如类型信息的指针不能直接作为切换值使用。
活动记录域:
过滤值: 正数,从1开始。捕捉条款__typeinfo类型的类型表的索引。1是TTBase之前的第一个字,2是第二个字,以此类推。由运行时用来检查抛出的异常类型是否匹配捕捉条款类型。后端生成检查这个值的switch语句。
Next: 有符号偏移,从这个域开始到下一个链接的活动记录的字节数,如果没有为0。
所有的域使用LEB128编码进行压缩(在“解码异常记录”一节描述)。
由next域确定的活动记录次序是捕捉条款在源代码中出现的次序,它们必须保持一致。C++语言允许在同一函数中两个捕捉条款涵盖相关的类型(比如基类与派生类)。因此,改变捕捉条款的顺序将改变程序的语义。
运行时活动:如果抛出异常类型匹配捕捉条款类型,该活动记录的切换值将被运行时传递给着陆场的“switch选择符”实参。
前端:前端产生一个援引这个活动记录的XHJP操作符。
后端:后端将分配切换值。如果两个XHJP操作符可以从同一个着陆场到达,那么它们不能共享任何切换值,除了表示完全相同的类型。XHJP操作符将被转换为switch语句,如果switch选择符的值匹配这个活动记录的切换值,它将跳转到该捕捉条款代码。
异常规范
异常规范的一个违例由运行时通过将switch选择符的值设置为负数来表示。着陆场中的代码检查该switch选择符值是否为负,如果是,调用__cxa_unexpected例程。否则,该异常被传播出去。
活动记录域:
C++类型列表:负数,从-1开始,它是一个以空字符结尾的类型索引在类型表中的字节偏移数。对-1,该列表将在TTBase+1,对-2在TTBase+2,以此类推。由运行时用来匹配抛出异常类型与“throw”列表中指定的类型。后端生成一个switch语句来检查这个特定的值。
Next: 有符号偏移,从这个域开始到下一个链接的活动记录的字节数,如果没有为0。
所有的域使用LEB128编码进行压缩(在“解码异常记录”一节描述)。
异常规范的行为非常类似于捕捉条款:当抛出异常违反了该异常规则,回滚遍1表示找到一个处理句柄,回滚遍2在生成代码中将控制权交给一个句柄。
运行时活动:异常处理库将检查抛出异常是否在可能异常类型的列表中。如果不是,它将着陆场的“switch选择符”实参设置为所指示的负值。
前端生成代码:一个异常规范句柄的生成代码将检查switch选择符的值是否为一个合适的负值。如果不是,这个异常被传播出去。
S1:
// Corresponding to an XHJP statement
switch(switchSelector)
{
case NEGATIVE_SWITCH_VALUE:
gotoH1
}
X1:
[RESX]
H1:
__cxa_unexpected();
注意在以一个异常规范内联一个函数后,某些代码可能实际使用在调用函数中的switch选择符值,如果匹配活动记录中指定的负值失败,控制权落到“default” 出口。
类型表
类型表是一个到描述C++类型的__type_info对象的非压缩GP相对偏移的数组。捕捉条款记录中的过滤值是这个数组的索引。这个类型由后端产生。
例如,在包含catch (A),catch(B)及catch(c)的一段代码里,该表可能包含:
· TTBase前的第一个字中A的__type_info,对应过滤值1。
· TTBase前的第二个字中B的__type_info,对应过滤值2。
· TTBase前的第三个字中C的__type_info,对应过滤值3。
异常规范表
这个表包含异常规范中使用的类型列表。这些列表是类型表压缩索的引以空字符结尾的序列。这个表由后端生成。
例如,给定在前一节中的类型描述,我们可以使用以下字节对使用throw (A,B)与throw(C)的两个函数编码:
0,
1,2, 0 // throw (A, B)
3,0 // throw (C)
在一个活动记录里,异常被编码为从表开头偏移的负数。Throw (A, B)将具有过滤值-1,而throw(C)则有过滤值-4。
注意类型索引可能超过一个字节(它们是LEB128编码的)。
解码异常记录
正如关于活动记录与回滚表章节指出的,异常表中几乎所有的域都以压缩格式保存以节省空间。使用的格式是Little-EndianBase 128(LEB128)。这是与DWARF目标模型格式相同的压缩方案。
要解码LEB128:
· 收集一连串设置了高比特位的字节,它们后跟一个没有设置高比特位的字节。(最高位为0表示一个LEB128值结尾的标记)。
· 丢弃每个字节的最高位。剩下N个7比特字节。
· 从这些字节以小端的次序(最后字节为最高位)构成一个7N比特二进制数。
· 如果这个值是有符号的,以二进制补码来解析之,最高位为符号位。
要编码LEB128:
· 将值分解为7比特的组合,从最低位开始(小端序)。
· 如果值是无符号的,将最后的组零扩展为整7比特。如果值是有符号的,将最后的值符号扩展为整7比特。
· 丢弃所有组的前导零(译注:即第7位),但如果值为0,至少保留第一组(最低位)。如果值是有符号的,丢弃所有组中重复的前导1(符号扩展),但确保保留至少一个符号位(参考下面-128的例子)。
· 将最后组以外所有组的最高位置为1;将最后组的最高位置为0。
例子(标记位粗体,符号位加下划线):
LEB128-编码值 二进制值 值(有符号)
00000000 0000000 0
00111111 0111111 63
01111111 1111111 127(-1)
10000000 00000001 00000010000000 128
10000001 00000001 00000010000001 129
10000000 01111111 11111110000000 16256 (-128)
10001000 00001100 00011000001000 1544
10000000 01000000 10000000000000 8192 (-4096)
10001010 10000101 00000011 000001100001010001010 49802
回滚库接口
本节定义作为通用C++ ABI导出的回滚库接口。
回滚库接口至少包括以下例程:
_Unwind_RaiseException,
_Unwind_Resume,
_Unwind_DeleteException,
_Unwind_GetGR,
_Unwind_SetGR,
_Unwind_GetIP,
_Unwind_SetIP,
_Unwind_GetRegionStart,
_Unwind_GetLanguageSpecificData,
_Unwind_ForcedUnwind
另外,对接洽一个调用运行时及上面的例程,定义了两个数据类型(_Unwind_Context与_Unwind_Exception)。所有的例程与接口的行为就像定义了extern“C”。特别的,这些名字是没有修饰的。
最后,一个语言与供应商特定的personality例程将由编译器保存在要求异常处理的栈帧的回滚描述符中。这个personality例程由回滚器调用来处理语言特定的任务,比如识别处理一个特定异常的帧。
设计讨论
回滚栈有两个主要的原因:
· 异常,正如支持它们的语言所定义的(比如C++)
· “强制的”回滚(比如由longjump或线程终止导致的)。
这里描述的接口尝试保持相似。不过,有一个主要的差别。
· 在抛出一个异常的情形下,在异常传播的同时栈被回滚,但期望每个运行时知道是否捕捉该异常,亦或放过它。这个任务委托给personality例程,它被假定对任意异常类型,不管是“原生”还是“外来”,行为正确。下面给出了“行为正确”的某些指引。
· 另一个方面,一个外部力量在“强制回滚”的过程中推动回滚。例如,这可以是longjump例程。这个外部力量,而不是每个personality例程,知道何时停止回滚。没有向personality例程给出一个关于是否进一步回滚的选择的事实由EH_FORCE_UNWIND_FLAG标记表示。
为了调和这些差异,提出了两个不同的例程。_Unwind_RaiseException在personality例程的控制下执行回滚。另一方面,_Unwind_ForceUnwind执行回滚,但给予“外部力量”拦截对personality例如调用的机会。这通过使用一个代理personality例程来完成,这个例程拦截对personality例程的调用,让外部力量取代personality例程的默认值。
[1] IA64架构指出单个调用将在一个给定块里执行。多个调用可能放在单个块里(例如,在一个像a? b(): c()的表达式),仅当它们可以共享同一个着陆场时。