tiamo
VIP专家组
VIP专家组
  • 注册日期2002-02-26
  • 最后登录2018-01-09
  • 粉丝17
  • 关注4
  • 积分50分
  • 威望142点
  • 贡献值1点
  • 好评度40点
  • 原创分2分
  • 专家分15分
  • 原创先锋奖
  • 社区居民
阅读:2974回复:5

how a c++ compiler implements exception handling

楼主#
更多 发布于:2004-07-13 17:05
有人应该知道有这么一个同名的文章介绍vc 6怎么实现c++异常的文章.....在这里小弟不才.借用下这个名字,介绍vc.net 2003怎么实现这个技术.

先说这些代码的由来,首先呢,你得写一个c++得程序,然后选择静态链接,然后用ida 反汇编就ok,用 vc 本身作调试器就ok,softice这种家伙,也没有用得必要了.
当然你也许能发现eh.obj这样的文件,其实代码就在那几个obj里面.

要想继续看下去,你得了解windows得seh技术,得熟练掌握32位汇编,因为这里并没有什么源代码给你看,全部是反汇编得结果...自然对c++异常的语法,以及c++异常规范,你必须要有足够的了解才行.

因为我ida反汇编以后加上注释的那几个idb文件已经不见了,只是剩下了自己用汇编写的代码,所以这里出现的代码都是nasm格式的汇编代码,同时搭配使用c32.mac这个文件,这个文件我有修改增加其宏的功能,名字都很简单明了,希望那些宏不会对大家的阅读产生障碍.

然后呢,要说说一些基本的概念.
你应该要知道__stdcall,__cdecl,__thiscall这些的调用与被调用规则,应该要知道在一个c函数里面哪些寄存器的值是要保护的,哪些寄存器是不用保护的.应该要明白mov fs:[0],eax是在干什么,你要明白windows的seh是怎么完成的.有了足够的准备以后往下看吧..

seh是windows平台上的一种异常处理方式,通过预先安装一个处理函数,到发生异常的时候跳转到处理函数里面的方式完成异常处理,这里不要把__try,__except等同到seh,他们只是一种简单的处理模型,并不代表了seh本身,seh本身只是在异常发生的时候跳转到一个函数而已.

当我们写下throw 0;这样的语句的时候,编译器会为我们产生下面的这些代码

mov [ebp-10],0
lea eax,[ebp-10]
push __TI1H
push eax
call __CxxThrowException@8

上面这些代码调用了一个叫__CxxThrowException@8的函数
看它的名字就知道它应该是一个使用stdcall调用法则,有两个参数的函数.它的原型
void __stdcall _CxxThrowException(void *pObject,_s_ThrowInfo const* pThrowInfo)

第一个参数就是throw 后面的那个object的地址,第二个参数是一个叫throwInfo的结构,它的定义如下(能在vc的watch窗口里面看到,你输入(_s_ThrowInfo*)0就能看到它的成员定义了)

; throw info
struc _s_ThrowInfo
.attributes : resd 1 ; properites
.pmfnUnwind : resd 1 ; destructor for thrown object
.pForwardCompat : resd 1 ; compat handler address
.pCatchableTypeArray : resd 1 ; catchable type array
endstruc

这个结构定义是使用nasm的语法,resd表示它是dword大小的,后面的1表示一个dword.

上面看到的那个__TI1H就是这个结构.

关于它的几个成员.我后面都有注释,唯一要说的就是最后一个成员,它表示这个throw出来的object它能转换成哪些其他的object以便被catch语句捕获,你应该要知道catch语句捕获的原则.

_CxxThrowException函数是一个外部的函数,它应该由c++的lib提供,它在设置好一些工作以后应该调用os提供的RaiseException的函数,然后os取得控制权,交到应用程序安装的seh handler上面,
那么vc为我们安装的那个函数是什么呢?

它会是一个诸如__ehhandler$_main:这样的函数
它简单的mov eax,__TI1H+48h
然后jmp到一个叫___CxxFrameHandler的函数
这个函数原型如下
int __CxxFrameHandler(struct EHExceptionRecord * pExcept,struct EHRegistrationNode * pRN,
;  struct _CONTEXT * pContext,void * pDC)

这个函数使用什么样子的调用法则是无关紧要的,因为它要么不返回,要么os会另外的设置esp,所以你使用cdcel还是stdcall都没有关系,唯一要保证的就是它的名字,它也应该要由c++的lib提供(当然我假定你是这个lib的提供者).

它使用的几个参数,首先是eax,它是一个_s_FuncInfo的指针,然后是一个EEHExceptionRecord的指针,它其实跟seh使用的ExceptionRecord差不多,只是vc在它里面加入了些其他的成员,剩下的参数也同它一样,你应该要知道这些大都是seh传递给你的参数,你也应该要知道,他们会在你调用RaiseException的时候设置好.
先给出这些结构的定义.
;exception record
struc EHExceptionRecord
.ExceptionCode : resd 1 ; exception code
.ExceptionFlags : resd 1 ; exception flags
.ExceptionRecord : resd 1 ; exception record
.ExceptionAddress : resd 1 ; exception address
.NumberParameters : resd 1 ; number of param
.magicNumber : resd 1 ; magic number
.pExceptionObject : resd 1 ; exception object pointer
.pThrowInfo : resd 1 ; throw info pointer
endstruc

;function info
struc _s_FuncInfo
.magicNumber : resd 1 ; magic number
.maxState : resd 1 ; max state in the function
.pUnwindMap : resd 1 ; unwind function list
.nTryBlocks : resd 1 ; how many try blocks
.pTryBlockMap : resd 1 ; try block list
endstruc

;registration node
struc EHRegistrationNode
.pNext : resd 1 ; prev exception registration node
.frameHandler : resd 1 ; this frame handler
.state : resd 1 ; current state
endstruc

上面的几个结构呢.首先要说的是ExceptionRecord,你应该要知道它里面由一个Parameters的数组,vc使用了里面的3个成员,分别赋予了不同的意思.这个很容易理解.

然后是RegisterationNode,vc多加入了一个叫state的域,如果你对__try,__except很熟悉的话,你会会心一笑,他其实跟tryLevel差不多,用来表示抛出这个异常时,程序运行到什么地方了.

至于function info,他表示的是你当前处于的这个函数的信息,他收集了发生异常的时候必须要完成的操作所需要的信息.

我希望你看到这里的时候还没有头晕,我选择先把这些东西一古老全抖出来然后再解释他的机制的方式,希望你还挺得住.

到这里,我要描述具体得实现思路了.

vc实现得c++异常是一种编译时行为,并不能归结到运行时行为,他通过编译器收集足够的信息来完成异常处理.

对于每个函数,vc收集一份他里面的try语句的位置信息,catch语句能catch住的object type信息,以及每个可能发生异常的情况下要完成的object destructor调用的信息,这些信息共同的构成了上面的function info.

同时vc在throw语句执行的时候,抓取到(也是在编译时完成的)那个object的地址,收集到这个obj能转换的类型(catch判断的时候有用),然后移交控制权,同时vc还在运行时候记录当前执行到的语句的位置,这个数据会用来得到当前语句属于哪个try,当前这个地方要调用哪些destructor.

看个具体的例子
假设我有定义A,B两个class
void test()
{
    // 这里设置当前位置为-1,表示不处于任何一个try语句里面
    try
    {
        // 这里设置当前位置0,表示进入了第一个try里面
        A testA;
        // 这里设置当前位置1,表示如果在这里出现了问题,要调用类A的destructor
        MayThrowException1;
        try
        {
            // 这里设置当前位置2,表示进入第二个try
            B testB;
            // 这里设置为3,要调用B的destructor
            MayThrowException2;
            // 这里会插入 B类的ddestructor调用,同时设置当前位置为1,表示退出了第二个try
        }
        catch(...)
        {
            // 这里也会有修改到当前位置的操作,因为在catch里面也会定义c++对象
        }
        // 这里同样插入A的destructor调用,设置当前位置-1
    }
    catch(...)
    {
    }
}


这个例子程序非常的简单,大家可以体会下这个当前位置的值的变换情况,他不仅仅是要表示try的位置,也要表示你必须要调用的destructor的信息.你也许要问这个当前位置保存到什么地方的?就在那个EHRegisterationNode里面,state就是用来保存这个的,你应该要知道他的位置就是[ebp-4],这个部分你要熟悉seh才能明白为什么是[ebp-4]

假想这么一个情况,上面的函数发生了一个c++异常,进入到异常处理函数里面,函数通过查看EHRegisterationNode->state的值,得到了当前程序运行的位置,先通过function info得到了当前所在的try block,这个是通过function info里面的try block list完成的,
他其实是一个数组,每个成员又是一个结构定义如下
;try block entry
struc _s_TryBlockMapEntry
.tryLow : resd 1 ; begin state
.tryHi : resd 1 ; end state
.catchHi : resd 1 ; catch end state
.nCatches : resd 1 ; how many catches
.pHandlerArray : resd 1 ; cathe block entry list
endstruc

tryHi跟tryLow定义了try语句所跨越的位置,比如上面的第一个try
的范围就是0到3,第二个是2到3,在list的array里面排列的顺序是从里到外的,所以,经过一些比较以后就能确定当前位于哪个try里面
知道了这个以后,从上面的_s_TryBlockMapEntry结构里面能得到跟这个try关联的catch语句信息,他们又构成了一个数组,通过比较那个throw object的类型跟catch语句的类型就能找到合适的catch语句跳转进去,如果没有找到,那么就应该回到上一个tryblock继续查找,这个任务的实现是靠编译器合理的布局try block map entry数组完成的,实际的工作只是需要遍历这个数组一一判断state是否落在tryLow还是tryHi里面,如果不是就继续下一项,如果是就查找catch语句,执行.

对于catch语句,vc也生成了一份信息,定义如下
; catch block entry
struc _s_HandlerType
.adjectives : resd 1 ; properites
.pType : resd 1 ; type_info pointer
.dispCatchObj : resd 1 ; offset from ebp
.addressOfHandler : resd 1 ; handler address
endstruc

最主要的就是那个pType成员,他是一个type_info指针,你应该要知道这个是个什么东西,addressOfHandler就是catch语句开始的指针.

现在我找到了异常发生的时候的程序运行位置,也知道了我程序写下的catch语句的信息,这个时候我就要开始一一比较我所throw出来的obj跟catch语句能catch的是否一致了.

这个部分比较复杂,涉及c++很多的语法
比如一个const的不能被非const的引用所catch等等
这些的判断就要借助_s_HandlerType.adjectives属性来判断了,vc收集好catch语句要求的obj的信息,const,volatile等等属性,你应该要知道这些属性并不能通过type_info来获取,你也应该知道非const的指针是能被const指针catch的等等语法.如果你明白了这些就能明白adjectives用来干什么了.

catch语句这边的信息已经足够了.
那throw出来的object呢,他能转换成什么?这个就落在了_s_ThrowInfo.pCatchableTypeArray上面了,是一个结构定义如下
; catchable type array
struc _s_CatchableTypeArray
.nCatchableTypes : resd 1 ; how many types
.arrayOfCatchableTypes : resd 1 ; types list
endstruc

最后一个成员是一个数组,成员定义如下
; catchable type
struc _s_CatchableType
.properites : resd 1 ; properites
.pType : resd 1 ; type_info pointer
.mdisp : resd 1 ; PMD
.pdisp : resd 1
.vdisp : resd 1
.sizeOrOffset : resd 1 ; size or offset
.copyFunction : resd 1 ; copy constructor
endstruc

看到这个properites你就应该想到他是和catch的adjectives配合使用的,yes,you are right!
同样你也看到了type_info,中间的几个变量你也许要觉得诡异了,干什么用的,这个先放放,先只要知道这个是用来作this指针调整的就够了,然后是一个sizeOrOffset,标记object的大小,然后是一个copy constructor,这个,呃,你应该知道catch(A a)这样的语句是有一个copy动作的.

然后要提到的就是local object的distructor调用,这个是通过_s_FuncInfo.pUnwindMap完成的,他指向一个unwindmap的数组,定义如下
; unwind map
struc _s_UnwindMapEntry
.toState : resd 1 ; linked field
.action : resd 1 ; unwind function
endstruc

vc 把每个要调用destructor的local object都收集起来,放到一个list里面,然后用toState链接起来,通过当前state作为下标索引这个list,然后调用action函数(这个action并不是destructor本身,因为action是一个无参数的,而destructor是要有this参数的,在action里面会有mov ecx,xxx,call xxx),然后用toState成员再作下标索引,直到toState变成-1为止.这样完成Local unwind,你当然也要知道global unwind其实是通过os转而调用你对应的local unwind完成的.

大致的流程就这么多,晕了没?呵呵.

接下来要解决的就是刚刚提到的this指针调整的问题,关于这个
我有一个原创,发表在金点上面的(我是金点的程序员,呵呵)
http://member.netease.com/~qinj/gpgame/docs/program/objectlayer.htm

再详细的分析就要看你自己的了,如果再贴汇编代码,再一行行的分析,呵呵,会死人的.

研究这个的初衷是想实现一个kernel级的c++环境,后来才发现原来为了c++环境去作c++环境在kernel模式有些得不尝试,所以放弃了,跟踪过driverstudio得c++异常实现,很不幸,里面有个bug,他并没有调用object得copy constructor,也没有作this指针得调整,所以他实现得c++异常其实是错误的,当然你用他的那套c++库,还是不会有什么问题的,如果你把他当作一个真正的c++环境,那你要小心了....

附上一个源代码,我用nasm实现的c++异常,源代码会产生一个lib文件,这个lib只是实现了c++的异常,你想要使用他并且通过连接的话,还得自己定义new delete,同时要定义type_info类.

声明,这个代码只是展示c++的异常实现方式,并不打算当作一个c++的运行库使用,如果你想获得kernel模式的全方位c++支持,请使用google,外国人有一个全c++环境支持的运行库(并不是driver studio提供的那个).....代码本身的问题我能尽量的解答....但是我对这个代码使用过程中出现的问题一律不回答.谢谢合作..

p.s.我们是不是让这个板块变成真正的windows源代码讨论板.

[编辑 -  7/13/04 by  tiamo]

最新喜欢:

TOMG2004TOMG20...
tiamo
VIP专家组
VIP专家组
  • 注册日期2002-02-26
  • 最后登录2018-01-09
  • 粉丝17
  • 关注4
  • 积分50分
  • 威望142点
  • 贡献值1点
  • 好评度40点
  • 原创分2分
  • 专家分15分
  • 原创先锋奖
  • 社区居民
沙发#
发布于:2004-07-13 17:06
忘记发附件了
附件名称/大小 下载次数 最后更新
2004-07-13_cppexcpt.rar (318KB)  68
wowocock
VIP专家组
VIP专家组
  • 注册日期2002-04-08
  • 最后登录2016-01-09
  • 粉丝16
  • 关注2
  • 积分601分
  • 威望1651点
  • 贡献值1点
  • 好评度1227点
  • 原创分1分
  • 专家分0分
板凳#
发布于:2004-07-13 17:48
好,收藏......
花开了,然后又会凋零,星星是璀璨的,可那光芒也会消失。在这样 一瞬间,人降生了,笑者,哭着,战斗,伤害,喜悦,悲伤憎恶,爱。一切都只是刹那间的邂逅,而最后都要归入死亡的永眠
wowocock
VIP专家组
VIP专家组
  • 注册日期2002-04-08
  • 最后登录2016-01-09
  • 粉丝16
  • 关注2
  • 积分601分
  • 威望1651点
  • 贡献值1点
  • 好评度1227点
  • 原创分1分
  • 专家分0分
地板#
发布于:2004-07-13 18:08
外国人有一个全c++环境支持的运行库,哪里有给个下载地址???
最近刚开始真正学C++......
花开了,然后又会凋零,星星是璀璨的,可那光芒也会消失。在这样 一瞬间,人降生了,笑者,哭着,战斗,伤害,喜悦,悲伤憎恶,爱。一切都只是刹那间的邂逅,而最后都要归入死亡的永眠
arthurtu
驱动巨牛
驱动巨牛
  • 注册日期2001-11-08
  • 最后登录2020-12-19
  • 粉丝0
  • 关注0
  • 积分26分
  • 威望161点
  • 贡献值0点
  • 好评度35点
  • 原创分0分
  • 专家分0分
  • 社区居民
地下室#
发布于:2004-07-13 20:01
顶一下 :D
tiamo
VIP专家组
VIP专家组
  • 注册日期2002-02-26
  • 最后登录2018-01-09
  • 粉丝17
  • 关注4
  • 积分50分
  • 威望142点
  • 贡献值1点
  • 好评度40点
  • 原创分2分
  • 专家分15分
  • 原创先锋奖
  • 社区居民
5楼#
发布于:2004-07-14 17:13
那个lib我也不记得自己在什么地方看到的了...
当时看了看就删除了
没有保留
因为那个时候想自己实现一整套的环境
所以就没有打算用别人的....呵呵
游客

返回顶部