快捷搜索:  as  test  1111  test aNd 8=8  test++aNd+8=8  as++aNd+8=8  as aNd 8=8

齐博国际:兼容内核之二十四:Windows的结构化异常处理(第一部分I)



布局化非常处置惩罚(Structured Exception Handling),简称SEH,是Windows操作系统的一个紧张组成部分。

在ReactOS内核的源代码中,分外是在实现系统调用的代码中,读者已经看到很多类似于这样的代码:

if(MaximumSize != NULL && PreviousMode != KernelMode)

{

_SEH_TRY

{

ProbeForRead(MaximumSize, sizeof(LARGE_INTEGER), sizeof(ULONG));

/* make a copy on the stack */

SafeMaximumSize = *MaximumSize;

MaximumSize = &SafeMaximumSize;

}

_SEH_HANDLE

{

Status = _SEH_GetExceptionCode();

}

_SEH_END;

if(!NT_SUCCESS(Status))

{

return Status;

}

}

这段代码取自NtCreateSection(),其参数之一是指针MaximumSize。系统调用一样平常都是从用户空间调用的,是以PreviousMode一样平常不是KernelMode。以是,只要指针MaximumSize不是NULL,就要从它所指的地方从用户空间把数值复制到内核空间。那为什么不直接把它的数值作为参数通报,而要这样绕一下呢?这是由于它的类型为LARGE_INTEGER,而作为参数通报的只能是32位(或以下)的通俗整数。

然而,从用户空间复制数据到内核空间(或反过来)恰好是轻易误事出事的。这是由于,用户法度榜样的质量相对而言是没有包管的,这个指针所指向的地址(所在的页面)大概根本就没有映射,或者大概不容许读,那样就会发生与页面映射和造访有关的非常(Exception)。

不过倒也并非只要发生非常就有问题,例如如果页面已经映射、也容许读,然则所在页面已经换出(Swap-Out),那就会发生缺页非常;而缺页非常着实不是“非常”而是“正常”,内核从磁盘上换入(Swap-In)目标页面,就可以从非常处置惩罚法度榜样返回、并继承运行了,就像发生了一次中断一样。此时CPU将从新履行发生非常的指令,这一次一样平常就能正常完成了。以是,(物理上的)非常之是否真的(逻辑上)“非常”,还得看详细的缘故原由。只要没有分外加以阐明,本文中所讲的非常都是指真正意义上的非常。

对付非常的处置惩罚,内核一样平常会供给默认的要领,例如“杀掉落”当提高程,让其一逝世百了,这样至少不会迫害其余进程。然则假如详细的法度榜样预期在某一段代码中有可能发生某几种特定的非常,并乐意为之供给办理、解救之道,那当然是更合理、更优雅的要领。举例言之,要是用户法度榜样中有除法运算,CPU在碰着除数为0的时刻就会发生非常,此时默认的处置惩罚要领一样平常是中止该用户法度榜样的运行,由于不知该如何让它继承下去了。然而齐博国际这可能发生在已经继续谋略了几十个小时今后,离成功大概只有一步之遥了,让它就这样退出运行不免难免丧掉太大年夜。假如法度榜样的设计职员事先预计到有这样的可能,大概会选择在这种环境下弹出一个对话框,提示应用者改变几个参数,然后以新的前提继承运算;或者至少问一下用户,是否把发生问题时的“现场”信息经由过程邮件发送给法度榜样的设计者。显然,这是更好的办理规划。问题在于若何来实现,若作甚法度榜样的设计者供给这样做的手段。

简而言之,便是要为法度榜样的设计者供给一种手段,使得假使在履行某一段代码的历程中发生了特定种类的非常就履行另一些指定的代码。事实上,这恰是微软的“布局化非常处置惩罚(Structured Exception Handling)”、即SEH机制要办理的问题之一。后面读者将会看到,SEH要办理两类问题,这是此中之一。

在上列的代码片断中,在_SEH_TRY{}里面是要加以“保护”的代码,即用户预计可能会在履行中发生非常的代码;而_SEH_HANDLE{}里面便是当发生非常时必要履行的代码;着末的_SEH_END则阐明与SEH有关的代码到此为止,从此今后的代码规复常态。这样,假如在履行_SEH_TRY{}里面受保护代码的历程中发生了某些非常,CPU就转入_SEH_HANDLE{};而若顺利履行完_SEH_TRY{}里面的代码,那就跳过_SEH_HANDLE{}直接到达_SEH_END。

留意在_SEH_TRY{}里面可能会调用其余函数,被调用函数的代码虽然形式上不在_SEH_TRY{}里面,然则它的本次被调用履行却同样是在_SEH_TRY{}所指定齐博国际的保护范围之内。在本文中,由_SEH_TRY{}所划定的范围称为一个“SEH保护域”,也称“SEH框架”,由于在履行这些代码时这体现为客栈上的一个框架。以是在本文中“SEH域”和“SEH框架”是同义词。说是“保护域”,着实也可以说是“捕捉域”,便是说这是一个必要“捕捉”住非常的域(以是在C++说话顶用“catch”表示捕捉到非常之后要履行的代码)。留意SEH域和函数是相互自力的两个观点。同一个函数,这一次是从_SEH_TRY{}里面调用,它的履行就在SEH域中;下一次不是从_SEH_TRY{}里面调用,就不在这个SEH域中了。以是一个函数(的履行)是否在SEH域里面是个动态的观点。不过,SEH域老是存在于某个函数的内部,而弗成能游离在函数之外,就像C语句只能存在于函数之内一样。

在实际利用中,SEH域还可以嵌套,便是在一个SEH域的内部又经由过程_SEH_TRY{}开辟了第二个SEH域。例如,前者大概是针对页面非常的SEH域,而在这里面又有一部分代码可能会引起“除数为0”的非常,以是又得将其保护起来,形成一个嵌在外层保护域里面的内层保护域。

显然,多个SEH框架嵌套就形成了一个SEH框架栈。SEH框架栈既可以只是实质的,也可以既是实质的、又是形式的。比方齐博国际说,一个SEH域的内部调用了一个函数,而在这个函数中又有一个SEH域,那么这两个SEH域(框架)的嵌套是实质的,但却不是形式的,由于从代码上不能一清二楚看出这样的嵌套关系,这种嵌套关系是运行起来才形成的。然则,假如在第一个SEH域的_SEH_TRY{}内部直接又有一个_SEH_TRY{},那么这两个SEH域的嵌套关系就既是实质的、又是形式的。在本文中,前者所形成的SEH框架栈称为“实质”SEH框架栈、或“全局”SEH框架栈,后者所形成的则称为“形式”SEH框架栈、或“局部”SEH框架栈。之以是如斯,是由于0.3.0版ReactOS的代码中对付SEH机制的实现有了一些更改。不过,在0.3.0版ReactOS的代码中并未见到应用形式嵌套的SEH域。

转头看前面_SEH_TRY{}里面的代码。这里受保护的有三个语句,先看对ProbeForRead()的调用。ProbeForRead()是个内核函数,这里也是在内核中调用,以是对这个函数的调用本身并没有问题。

VOID STDCALL

ProbeForRead (IN CONST VOID *Address, IN ULONG Length, IN ULONG Alignment)

{

ASSERT(Alignment == 1 || Alignment == 2 || Alignment == 4 || Alignment == 8);

if (Length == 0)

return;

if (((ULONG_PTR)Address & (Alignment - 1)) != 0)

{

ExRaiseStatus (STATUS_DATATYPE_MISALIGNMENT);

}

else if ((ULONG_PTR)Address + Length - 1  (ULONG_PTR)MmUserProbeAddress)

{

E齐博国际xRaiseStatus (STATUS_ACCESS_VIOLATION);

}

}

其目的只是反省参数的合理性,而并不真的去造访用户空间。假如用户空间数据所在的地址不与给定命据类型(在这里是ULONG)的界限对齐,或者所在的位置纰谬、长度分歧理,那就要经由过程ExRaiseStatus()以软件措施模拟非常。这是为什么呢?由于在正常的环境下这是弗成能发生的,既然发生了就必然是出了问题,按理说最好是CPU在碰着这种环境时能引起一次非常,然则386布局的CPU不会(从486开始就会了,这便是17号非常“Alignment Check”),以是就只好经由过程软件手段来模拟一次“软非常”。留意这里软非常的类型为STATUS_DATATYPE_MISALIGNMENT和STATUS_ACCESS_VIOLATION,前者表示与数据类型的界限纰谬齐,后者表示越界造访,这相称于硬非常的非常号,然则富厚得多。

就前述的SEH域而言,由此而引起的效果与硬件非常相同,CPU也会转入_SEH_HANDLE{}里面。

认识C++的读者可能会遐想到throw语句,实际上也确凿是同一回事。

假如ProbeForRead()没有反省出什么问题,前面的第二个语句是“SafeMaximumSize = *MaximumSize”,这是从用户空间读取数据写入系统空间。这里写入系统空间不会有问题,然则读用户空间可能会有问题,假如指针MaximumSize所指的页面无映射就会发生非常。以是要把它放在_SEH_TRY{}里面。

第三个语句是“MaximumSize = &SafeMaximumSize”,这是对指针MaximumSize进行赋值。作为调用参数,这个变量本来在用户空间客栈上,CPU因系统调用进入内核今后把它复制到了系统空间客栈上。因而这个赋值操作应该不会引起非常,本可以放在外貌,然则放在_SEH_TRY{}里面也无弗成。以是,并不凡是放在_SEH_TRY{}里面的都必须是可能引起非常的语句。对付不会引起非常的语句,放在_SEH_TRY{}里面或外貌都是一样。

再看安排在发生非常时加以履行的代码、即_SEH_HANDLE{}里齐博国际面的代码。在这里只有一个语句,便是对_SEH_GetExceptionCode()的调用。顾名思义,这便是获取详细非常的代码,例如STATUS_DATATYPE_MISALIGNMENT、STATUS_ACCESS_VIOLATION等等。然后将获取的代码赋值给变量Status,这就完事了。再往下便是_SEH_END及其后面的if语句了。当然,这里面也可以有不止一个、以致很多的语句。

留意变量Status蓝本已经初始化成STATUS_SUCCESS,而_SEH_TRY{}里面的代码都不会改变它的值;以是只要“!NT_SUCCESS(Status)”为真就必然已经发生过非常,是以这个系统调用就掉足返回了,而且所返回的便是所发生非常的代码。而根据所返回的值判断本次系统调用是否成功,以及采取什么步伐,那便是用户软件的事了。

这里还要阐明一下,并不是所有的非常都邑落入这_SEH_HANDLE{}里面。发生非常时,首先是由内核底层的非常处置惩罚法度榜样“认领”和处置惩罚,例如缺页非常就会被其认领并处置惩罚,处置惩罚完就返回了。纵然是不归其认领处置惩罚的非常,也还得看当时是否正在经由过程调试对象(debugger)调试法度榜样,假如是就交由debugger处置惩罚。只有不受这二者拦截的非常才会落入_SEH_HANDLE{}。后面读者将看到,每个SEH域都可以经由过程一个“过滤函数”反省本次非常的类型,已抉择是否认领。假如存在嵌套的SEH域,则首先要由嵌套在最内层(最底层)的SEH域先作过滤,抉择不予认领才会交给上一层SEH域。以是,只有不被拦截、认领,并经由过程了层层过滤的非常才真正进入本SEH域的_SEH_HANDLE{}。

上面所引的是内核中的代码,用户空间的代码同样也可以使用SEH所供给的功能和机制,着实C++说话中的try{..}catch{…}终极也是使用SEH实现的。

免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。

您可能还会对下面的文章感兴趣: