轻视WinXPe异常系统健壮性将命悬一线
【IT168 专稿】众所周知,嵌入式系统强调的是系统健壮性。但当尝试关闭、启动Windows XP Embedded或在Windows XP Embedded运行应用程序时,可能会遇到类似的错误消息:A fatal exception XY has occurred at xxxx:xxxxxxxx。类似的代码常常出现在如遇到对非法指令的访问,访问了无效的数据或代码,操作的权限级别无效等。虽然多数情况下异常是可恢复的,但有时也必须重新启动系统或关机,具体取决于异常的严重性。
异常处理对于Windows XP Embedded程序开发是极其重要的一环,异常处理的好坏直接关系到系统的健壮性和稳定度。因此,了解和熟悉Windows XP Embedded的异常处理机制对于开发人员来说是必不可少的。近期,我在一个Windows XP Embedded嵌入式项目中就体验到轻视异常处理的惨痛教训。因为异常没有处理好,后果是严重影响系统的健壮性。本文分享在此项目过程中对异常处理的一些经验总结。
1. 什么是Windows XPe异常
(1)什么是异常
在Windows XPe运行时常常会出现一些非正常的现象,这种情况称为运行错误。根据其性质可以分为错误和异常。一般来说,最常见的错误有程序进入死循环,内存泄漏等。这种情况下运行的程序本身无法解决,只能通过其它程序干预。而异常是由于CPU执行了某些指令引起的,是程序执行时遇到的非正常情况或意外行为。这种情况不像错误类那样,通常在程序运行时可以解决,由异常代码调整程序运行方向使程序仍可继续运行直至正常结束。
异常和中断的区别是中断可在任何时候发生,与CPU正在执行什么指令无关。中断主要由I/O设备、处理器时钟或定时器等硬件引发,可以被允许或取消。一般这些情况都可以引发异常:代码或调用的代码(如共享库)中有错误,操作系统资源不可用,公共语言运行库遇到意外情况(如无法验证代码)等等。还有常见的有数组下标越界,算法溢出,除数为零,无效参数等,内核也将系统服务视为异常。
当意外行为在Windows XPe中发生时,系统将创建一个异常对象并把它抛出给运行时系统(Runtime system),然后运行时系统跳入另一个动作,就是要找到一些代码来处理这个异常。运行时系统会向后搜寻调用堆栈,从错误发生的函数,一直到找到一个包括合适的异常处理器(Exception Handler)的函数。异常处理器的选择被叫做:捕获异常(Catch the exception)。
(2)什么是异常调用程序
Windows XPe异常处理器是处理中断的主要组件之一,异常调用处理也是用户程序最基本的一种执行流程,它的性能直接影响着整个系统的可靠性。简单地说,异常调用是把执行权重新由应用程序还给操作系统,因此操作系统内核需要捕捉所有的异常,当操作系统捕获到异常时,也就重新获得CPU。因此,异常调用程序就是能够让系统在出现异常的情况下恢复过来的程序。
在Windows XP Embedded体系结构上,异常的实现原理和Windows XP一样。当应用程序进行系统调用时,它直接调用的是CoreDLL.DLL中的一个(Wrapper)函数,CoreDLL.DLL会被Windows XPe的所有进程加载。当CoreDLL.DLL发起一个异常时,也叫做软件中断。内核会响应这个软件中断,这时应用程序进程就会被挂起,接下来内核会根据不同的异常中断系统调用,找到具体实现该系统调用的进程(也被叫做PSL,全称是Protected Server Library)。此进程可能是系统内核,也可能不是。如果不是系统内核,那么执行就再次跳转,把执行转到具体实现异常处理的进程去执行。最后,当异常调用处理结束后应用程序再从CoreDLL.DLL的调用处返回,然后继续执行。
(3)Windows XPe的异常类型
在Windows XPe平台按优先级可分为:复位(Reset)、数据访问中止(Data abort)、快速中断请求(FIQ)、外部中断请求(IRQ)、指令预取中止(Prefech abort)、软件中断(Software interrupt SWI)、未定义的指令(Undefined instruction)等。其中, Reset仅在复位时发生,其它几种都是在系统运行时发生。
2.Windows XPe异常处理流程
Windows XPe有两种运行模式:内核模式和用户模式,并且允许一个运行于用户模式的应用程序随时切换为内核模式或切换回来。因此,Windows XPe的异常处理也和桌面Windows 相似,有两种不同的处理流程。
(1)内核模式异常处理流程
内核是 Windows XPe的内部核心,它负责调度和同步线程、处理异常和中断、加载应用程序和治理虚拟内存。系统内核包括内存管理、进程管理以及异常处理。当中断到来时,内核根据IRQ来调用相应的中断服务例程(ISR)。先判断KiDebugRoutine是否为空,不为空就将Context、陷阱帧、异常记录、异常帧、发生异常的模式等压入栈并将控制交给KiDebugRoutine。若KiDebugRoutine为空或者KiDebugRoutine未处理异常,则将Context结构和异常记录ExceptionRecord压栈,并调用内核模式的RtlDispatchException在内核堆栈中查找基于帧的异常处理例程。
RtlDispatchException调用RtlpGetRegistrationHead获取当前线程异常处理链表指针,并调用RtlpGetStackLimits获取当前线程堆栈底和顶。然后开始由异常处理链表指针遍历链表查找异常处理例程,这里和用户态有一点不同是既没有顶层异常处理例程(TOP LEVEL SEH),也没有默认异常处理例程。然后对每个当前异常处理链表指针检查判断堆栈是否有效及堆栈是否是DPC堆栈。
处理后RtlDispatchException再判断异常处理例程的返回值,若为ExceptionContinueExecution,则返回TRUE到上一层,否则调用RtlRaiseException进入到KiDispatchException的第二次机会处理异常;若为ExceptionContinueSearch则继续查找异常处理例程;若为ExceptionNestedException嵌套异常,则保留当前异常处理链表指针为内层异常处理链表并继续查找异常处理例程。简单说就是,当异常发生于内核模式,会给予内核调试器第一次机会和第二次机会处理异常。
(2)用户模式下陷阱帧异常处理流程
在进行用户态异常的分派前,KiDispatchException先判断异常是否来自用户模式,是的话将Context.ContextFlags 上CONEXT_FLOATING_POINT,这意味着对来自用户模式的异常总是尝试分派浮点状态,这样可以允许异常处理程序或调试器检查和修改协处理器的状态。然后从陷阱帧中取出寄存器值填入Context结构,并判断是否是断点异常(int 0x3和int 0x2d),如果是的话先将Context.Eip减一使它指向int 0x3指令,然后根据不同模式而采取不同处理过程。当异常被处理后就将设置好陷阱帧并返回到陷阱处理程序,在Iret返回发生异常的地方继续执行。
例如,当用户模式下发生异常后,CPU记录当前各寄存器状态并在内核堆栈中建立陷阱帧TrapFrame,然后将控制交给对应异常的陷阱处理程序。当陷阱处理程序能处理异常时,比如缺页时通过调页程序MmAccessFault,将页换入物理内存后通过iret返回发生异常的地方。但大多数情况下是无法处理异常的,则此时先是调用CommonDispatchException在内核堆栈中建立异常记录ExceptionRecord和异常帧ExceptionFrame。然后,调用KiDispatchException进行异常的分派,这个函数是Windows XPe下异常处理的核心函数,负责异常的分派处理。
3.异常处理的几点经验总结
对于许多嵌入式系统而言,要消除所有异常几乎是不可能的,因为嵌入式系统可能是由各种应用程序集成在一起,从而引入未知且无限的交互组合。虽然Windows XPe内核具有内存管理功能,可以检查出应用程序造成的系统异常,抑制由于应用不正常直接破坏系统的危险性。但为了加强Windows XPe的稳定性,程序设计时必须还应考虑到可能发生的异常事件并做出相应的处理。
(1)缩小异常检测范围,启动干净计算机环境
因为引起致命异常错误的情况各不相同,所以解决问题的第一步是缩小范围。为此,可尝试“启动干净”计算机。干净启动故障排除是指尽可能减少由于计算机环境而出现的问题的方法,因为许多异常问题都是由相互冲突的驱动程序、终止并驻留程序 (TSR) 以及其它在计算机启动时加载的设置而引起的。
(2)将异常处理代码和正常流程代码分开
在传统的程序中,错误侦测代码、报告和处理代码经常是象令人迷惑的意大利面条式的混在一起。在Windows XPe系统有一种优雅的解决方案,就是主流程和处理异常情况的代码分开。例如,不要把重要的异常信息放在message中,每个线程需要一个单独的try/catch模块,否则将会丢失异常从而导致非常难处理的问题的出现。还有不要忘记应该经常性的记录Exception.ToString(),而不仅仅是Exception.Message;书写catch(Exception ex)时,尽量描述清楚OutOfMemoryException异常被抛出时代码该如何处理等。
(3)将异常分类,以便快速定位异常
在处理异常的时候,我们可以把异常划分成不同类别或组。例如,某一组别异常中每一个都是表示数组操作的异常:如索引超出数组的范围,要插入的元素是错误的类型,要查找的元素不在数组中等,这样就可以设置某一些函数将处理所有这类的异常,而其它一些函数将处理特殊异常。
总之,最好的方法是在进行系统设计就把异常处理融合在系统中,若嵌入式系统一旦实现,就很难添加异常处理功能。因此从嵌入式项目一开始就应该着手进行异常处理,投入大精力把异常处理的策略融合到嵌入式产品中,增强产品的健壮性。