异常处理表

原文地址: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()的表达式),仅当它们可以共享同一个着陆场时。

时间: 2024-11-13 15:50:11

异常处理表的相关文章

C#异常处理表、类、SQL

表SQL /****** Object: Table [dbo].[IError] Script Date: 09/05/2012 17:00:41 ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO SET ANSI_PADDING OFF GO CREATE TABLE [dbo].[IError]( [ErrorModuleID] [varchar](500) NOT NULL, [ErrorClassName] [varcha

深入理解Java虚拟机笔记---属性表集合

在Class文件,字段表,方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息.与Class文件中其它的数据项目要求的顺序.长度和内容不同,属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性.为了能正确地解析Class文件,<Java虚拟机规范(第二版)>中预定义了9荐虚拟机实现应当能识别的属性,具体如下表所示: 对于每个属性,它的名称需要

JVM-class文件完全解析-属性表集合

属性表集合 在前面魔数,次版本号,主板本号,常量池入口,常量池,访问标志,类索引,父类索引,接口索引集合,字段表集合,方法表集合,那么接下来就是属性表集合了.   在class文件,字段表,方法表都可以携带自己的属性表集合(像前面方法表的时候就用到"code"这个属性表)以用于描述某些场景专有的信息. 虚拟机中预定义的属性: 属性名称 使用位置 含义 Code 方法表 Java代码编译成的字节码指令 ConstantValue 字段表 final关键字定义的常量池 Deprecated 类,方法

Jbpm表结构说明

JBPM_ACTION action记录表JBPM_DECISIONCONDITIONS 结果条件表JBPM_DELEGATION 委托表JBPM_EVENT 事件表 处理进入或者离开事件JBPM_EXCEPTIONHANDLER 异常处理表JBPM_ID_GROUP 用户组表JBPM_ID_MEMBERSHIP 用户成员表 表现用户和组之间的多对多关系JBPM_ID_PERMISSIONS 用户权限表JBPM_ID_USER 用户表JBPM_MODULEDEFINITION 模块定义表JBPM

ARM异常处理(IRQ中断处理)总结1

ARM A8的异常处理表如下可以看到vector table的基地址是不固定的但是所有异常的偏移地址是固定的.这张表也为了体现这个偏移量,还有要从硬件角度理解的是在CPU设计的时候这些异常就已经定义好了在发生相应的异常时候CPU就自动的转到在异常向量表里的地址处去执行这个过程是不需要软件干预的因此就简单了我们只要把出现相应的异常时候处理过程的函数名(函数名就是这个函数体的指针)放在相应的地址里边就可以执行了举例说明.以三星S5PV210为例,我们根据官方的Application Node可以查到

Java 虚拟机

# Java 虚拟机 ## Java 虚拟机概述和基本概念 ### 类加载子系统.方法区.Java堆.直接内存.Java栈.本地方法栈.垃圾回收系统.PC寄存器.执行引擎 + 类加载子系统:负责从文件系统或者网络中加载 Class 信息,加载的信息存放在一块称之为方法区的内存空间. + 方法区:就是存放类信息,常量信息,常量池信息,包括字符串面量和数字常量等. + Java 堆:在 Java 虚拟机启动的时候建立 Java 堆,他是 Java 程序最主要的内存工作区域,几乎所有的对象实例都存放到

Dump内存中的Assembly到磁盘

发现ConfuserEx这个开源的.NET加密程序用的人也非常多,尤其是老外.屡屡遇到这东西,很头疼,主要是是没有现成的脱壳程序,需要手动脱壳,虽说难度不大,但它更新的速度挺快,一直在变化,总是给人一种追着才能赶上的感觉,真是不爽.话说来,不知国内是真没有大牛,还是大牛都藏着掖着,那些脱壳的程序都是老外的作品,身在天朝又访问不了GG,只能自己动手了.其实本文的目的是根据内存中的源程序集Rebuild一个新的程序集,之所以不是Dump是因为还要把被混淆过的名称和字符串信息还原,显然Dump不能满足

java的GC与内存泄漏

从诞生至今,20多年过去,Java至今仍是使用最为广泛的语言.这仰赖于Java提供的各种技术和特性,让开发人员能优雅的编写高效的程序.今天我们就来说说Java的一项基本但非常重要的技术内存管理 了解C语言的同学都知道,在C语言中内存的开辟和释放都是由我们自己来管理的,每一个new操作都要对于一个delete操作,否则就会参数内存泄漏和溢出的问题,导致非常槽糕的后果.但在Java开发过程中,则完全不需要担心这个问题.因为jvm提供了自动内存管理的机制.内存管理的工作由jvm帮我们完成.这样我们就不

JVM的基本结构及其各部分详解(二)

3.2 栈帧组成之操作数栈 操作数栈是栈帧的主要内容之一,它主要用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间. 操作数栈也是一个先进后出的数据结构,只支持入栈和出栈两种操作,许多java字节码指令都需要通过操作数栈进行参数传递.比如add指令,它就会在操作数栈中弹出两个整数并进行加法计算,计算结果会被入栈,如图:显示了iadd前后操作数栈的变化. 3.3 帧数据区 除了局部变量表和操作数栈,java栈帧还需要一些数据来支持常量池的解析.正常方法返回和异常处理等.大部分jav