Windows 95 System Programming SECRENTS学习笔记(六)

类别:编程语言 点击:0 评论:0 推荐:

线程控制函数

Win32 API提供了几个函数,用来修改和查询线程的执行状态。在底层,线程可以读些另一个线程的寄存器(假设它有其他线程的handle)。在高层方面,有些Win32 API允许你暂停/开始其他线程的执行。让我们看看这些线程控制函数。

 

GetThreadContext和IGetThreadContext

GetThreadContext使线程有能力获得另一个线程的寄存器的副本。任何时候,线程要么是正在执行、要么就是出于挂起状态。即使线程处于挂起状态,其寄存器值还是保存在一个名为thread context的结构中。GetThreadContext函数让你能够读取一个被挂起的线程的thread context。

 

GetThreadContext函数其实只是做参数的检验工作。它检查传入的指针是否指向足够容纳CONTEXT结构的内存块。如果答案是肯定的,程序代码就跳到内部函数IGetThreadContext中。

 

IGetThreadContext首先转换hThread参数为一个thread database指针,然后调用x_ThreadContext_CopyRegs,把输入的寄存器组存放到ring3的CONTEXT结构中。除了复制寄存器内容到ring3级的CONTEXT结构,IGetThreadContext也调用VWIN32.VXD取得那些寄存器的ring0版本。至于寄存器为什么要有ring0和ring3两种,不甚清楚。

 

在将CONTEXT结构填充完毕之后,GetThreadContext检查CS以及标志寄存器的值,看看是否合法。标志寄存器的检查很简单,只是确定目前不出于V86模式。

 

x_ThreadContext_CopyRegs

    这个函数把被选中的寄存器内容,从某个CONTEXT结构复制到另一个CONTEXT结构。CONTEXT结构定义与WINNT.H。

补充:

在Visual Studio.NET 2003自带的SDK的WINNT.H中,分别包含针对AMD、ALPHA、x86、MIPS、PPC(Power PC)、和IA64六种CPU的CONTEXT结构定义。在针对x86的CONTEXT结构中,第一个成员是DWORD ContextFlags,该标志用来指出CONTEXT中的那些寄存器应该被复制。ContextFlags可能的值如下:

CONTEXT_EXTENDED_REGISTERS  // cpu specific extensions

CONTEXT_DEBUG_REGISTERS  // 调试寄存器 0-3,6,7

CONTEXT_FLOATING_POINT    // 算术协处理器387的状态

CONTEXT_SEGMENTS          // DS, ES, FS, GS

CONTEXT_INTEGER            // AX, BX, CX, DX, SI, DI

CONTEXT_CONTROL           // SS:SP, CS:IP, FLAGS, BP

 

x_ThreadContext_CopyRegs函数十分直接了当。它忠实地检查来源端的ContextFlags标志(针对x86),根据其指示复制对应的寄存器值到目的端的CONTEXT中。程序代码中没有什么令人惊奇的发现。

 

SetThreadContext和ISetThreadContext

SetThreadContext使线程有能力改变另一个线程的寄存器。任何时候,这个函数与GetThreadContext形成互补。

 

SuspendThread和VWIN32_SuspendThread

SuspendThread会影响线程的挂起次数。如果挂起次数不是0,ring0线程调度器绝不可能让该线程执行。SuspendThread函数代码其实只不过是我所谓的VWIN32_SuspendThread内部函数的外包装而已。VWIN32_SuspendThread等待获取一个thread database指针,所以在调用其之前,SuspendThread首先把hThread转换为有用的指针,如果VMWIN32_SuspendThread成功,SuspendThread会在thread database的1BCh位置增加挂起次数。

 

然而,重要的是,当线程调度器决定哪一个线程可以执行,挂起次数并不是决定因素。倒是,VWIN32在TDBX结构中保存了真正的挂起次数。VWIN32_SuspendThread其实也只不过是底层函数的一个外包装而已,它通过一个未公开的VxDCall函数调用VWIN32.VXD。

 

ResumeThread

这个函数和ResumeThread有互补作用。它会将某个线程的挂起状态减1(不论是在ring0的TBDX结构中,或是在ring3的thread database 1BCh位置中)。当挂起状态将为0时,线程就有资格被调度器视为可能的执行对象。

 

ResumeThread首先把hThread参数转换为一个Thread Database指针,然后检查ring3的挂起次数,确定不是0(如果是0,ResumeThread就什么也不做)。接下来,调用一个VWIN32.VXD的服务函数,将挂起次数减1。如果VWIN32.VXD的服务函数能够成功的将ring0的挂起次数减1,那么ResumeThread就将ring3的挂起次数也减1。

 

结构化异常处理(Structured Execption Handling)

结构化异常处理(Structured Exception Handling, 简称SEH)在现代操作系统,如OS/2、Windows NT、Windows9x等,是一个被大肆宣传并且常常被误解的主题。大部分谈到他的书籍和文章都把它放在编译器层面来说。编译器利用一些如:__try、__except、__finally、catch、throw等保留字,把凌乱的操作系统基础界面包装起来。

 

当一个异常情况(例如page fault)发生,CPU会立刻把控制权转给ring0的异常处理函数,后者的地址存放在中断描述表(interrupt descriptor table)中。ring0处理函数才能够决定该如何善后。如果这是一个系统知道如何处理的异常情况,ring0代码就作必要的措施,然后让指令继续下去。这些异常情况基本上对ring3代码以及system DLLs是不可见的,而且此处我们也不关心它们。

 

此处我们关心的是,万一系统不知道如何处理这个异常情况。怎么办?旧的操作系统的典型反应就是把引起异常情况的进程砍掉。虽然“将任何引起不可预期的错误的程序结束掉”的哲学无可挑剔,但它毕竟没有包容性。比较好的做法是通过应用程序(或其他应用程序)让它们自己决定如何处理。

 

32位的OS如OS/2和NT引入了一个比较有弹性的处理方法—结构化异常处理(SEH)。在处理多线程以及利用C++ catch/throw机制等方面,SHE的表现比以前的方法好得多。对于C++异常情况,应用程序本身可以引发一个和CPU异常情况截然不同的异常情况。假设C++的new运算符失败,它会抛出一个表示内存不足的异常情况。32位操作系统的SHE机制有足够弹性,以相同的代码一并处理语言的异常和硬件的异常。

 

在继续前进之前,我要再次强调我要谈的是SHE在操作系统层面的运作情况。

 

在一个拥有SHE的系统中,每个线程有它自己的SEH链内含设定好的异常处理函数。当一个异常情况发生,操作系统走访该SEH链,并调用其中适当的异常处理函数。这样的动作一直持续到某个异常函数传回代码,表示它要处理这个异常情况位置。这就是第一阶段:找到正主。如果这些函数无一能处理此异常情况,系统就会出面,把闹事的进程砍掉。我们不关心后面这种情节,因为操作系统把进程砍掉是很简单的事情。

 

当你获得了一个函数用来处理异常情况,第二阶段就是重新一遍走访SHE链。未公开函数RtlUnwind函数为我们做这样的事,它被那个“决定处理此异常情况的处理函数”所调用。当RtlUnwind一一触发那些SEH链的中函数,系统会交给它们一个标志。这个标志告诉函数说线程的堆栈目前正被“unwound”(译注)。将堆栈“Unwinding”,是“把程序状态恢复到异常处理函数被安装时的状态”的一种方法。不只在__except区块中重新恢复执行,系统还会给与每个“被安装,但是不处理此异常情况”的函数一个机会清理自己。给与这个机会之后,重要的事情如“调用堆栈中的C++对象的析构函数”就可以在一种有纪律的情况下完成。

 

译注:

所谓“unwind”,就是当一个异常情况发生时,将堆栈中的对象析构。这是ANSI的标准要求。据我所知,OWL支持完整的“Stack Unwinding”,而早期的MFC并不包含自动的stack unwinding。新版的MFC表现如何不得而知。

 

理论够多了,Windows 95真正使用的结构和方式到底是什么呢?先前我介绍TIB时说过,FS:[0]总是这向当前线程的SEH链的链头,异常处理函数所组成的链是一个EXCEPTIONREGISTRATIONRECORD链表。这个长长的名称来自于OS2/2.0的BSEXCEPT.H文档。为了某些理由,微软似乎企图对一般人隐藏SEH在操作系统层面的资料。EXCEPTIONREGISTRATIONRECORED结构看起来像这样:

DWORD prev_structure   // A pointer to the previously

// installed EXCEPTIONREGISTRATIONRECORED

DWORD ExceptionHandler // Address of the exception handler function

链表的最后是以-1(prev_structure)表示结束。

 

正常情况下,程序会按照需要从堆栈中挖出空间来制造EXCEPTIONREGISTRATIONRECORD。在C/C++程序中,每个EXCEPTIONREGISTRATIONRECORD对应一个__try/__except块。当程序进入__try块时,编译器就在堆栈中产生一个新的EXCEPTIONREGISTRATIONRECORD并把它放到SEH链的开始处。离开__except块后,编译器设定FS:[0]指向链表中下一个EXCEPTIONREGISTRATIONRECORD。下图显示了这些被串联起来的数据。

记住,上述8位结构只是操作系统的最小需求而已。没有什么可以阻止编译器在堆栈中产生更大的结构并且把EXCEPTIONREGISTRATIONRECORD放在结构首位。编译器从上述结构中获得的其他成员也可以提供足够的信息,使单独一个异常处理函数适用于所有的__try块。微软和Borland的编译器都使用EXCEPTIONREGISTRATIONRECORD的扩充结构。

 

说到异常处理函数,它到底是什么样子?再一次,微软似乎想隐藏某些资料,但至少Win32提供了一个函数原型。在EXCPT.H中,你可以看到这样的原型:

EXCEPTION_DISPOSITION  __cdecl _except_handler(

      struct   _EXCEPTION_RECORD *  ExceptionRecord,

      void *  EstablisherFrame,

      struct   _CONTEXT* ContextRecord,

      void *  DispatcherContext

)

乍看之下,它仿佛太过复杂了。返回值EXCEPTION_DISPOSITION其实只不过是个enum,告诉系统说此函数如何被用来处理异常情况:

typedef enum _EXCEPTION_DISPOSITION {

    ExceptionContinueExecute,

    ExceptionContinueSearch,

    ExceptionNestedException,

    ExceptionCollidedUnwind

} EXCEPTION_DISPOSITION;

最后两项很少会遇到。至于第一项ExceptionContinueExecute是告诉系统说异常处理函数已经处理该异常情况,并打算让执行继续下去。ExceptionContinueSearch则是告诉系统说异常处理例程不打算处理此异常情况,系统应该继续走访EXECEPTIONREGISTRATIONRECORD链表,直到某个异常处理例程返回ExceptionContinueExecute。

 

把_except_handler函数原型重写一遍,看起来就可以接受的多了:

int _except_handler (

    PEXCEPTION_RECORD  ExceptionRecord,

    PVOID EstablisherFrame,

    PCONTEXT ContextRecord,

    PVOID DispatcherContext );

我们发现,一个异常处理函数需要四个指针参数,指向异常情况以及机器状态等数据结构。这个函数返回一个整数,告诉系统它是否把异常处理好了。EXCEPTION_RECORD结构内含异常代码,以及其他东西。WINNT.H对此有些说明。CONTEXT结构内含异常发生时的寄存器内容,WINNT.H对此也有说明。EstablisherFrame参数内含一个指针,指向堆栈---EXCEPTIONREGISTRATIONRECORD结构的设定处。DispathcerContext参数则似乎没有用到。

 

稍早我说过,异常处理函数会被调用两次。第一次是系统寻找适当处理函数的时候。第二次是为了系统要“unwinding”,而处理函数被认为会进行必要的清理工作(像是调用堆栈对象的析构函数等等)。这两次调用之间的差别在哪里?ExceptionRecord结构(被第一个参数所指)内含一个ExceptionFlags标志,如果EH_UNWINDDING(0x2)或EH_EXIT_UNWIND(0x4)标志并未设定,那么就是前述第一种情况;如果两个标志之一设立,那么就是第二种情况。

补充:

    关于为什么异常处理函数会被调用两次,在《Windows下32位汇编程序设计》一书中有详细地介绍,根本上讲是为了进行清理工作,恢复系统状态到异常发生时。

本文地址:http://com.8s8s.com/it/it23680.htm