虽然我所说的不足以让你写一个自己的操作系统层面的异常处理机制,但是以足够让你了解SEH是如何运作的。在本章的示例程序SHOWSEH程序中,会利用__try块来设立异常处理链表。设立好后,该程序将遍历SEH链表并打印出每一个节点的内容。
SHOWSEH的输出结果如下图所示。我要你注意几点。第一,请注意“next rec”一栏总是递增的,那是编译器用来放置EXCEPTIONREGISTRATIONRECORD的堆栈区域。最前面四笔数据反映出了SHOWSEH.C中的__try段落。第二,请注意前四笔资料有一个不变的ESP值。输出结果的最后一行则显示SHOWSEH.C之中每一个函数的ESP值。
最后一件值得注意的事情是,最前面5笔字了得handler地址都一样,地址都位于SHOWSEH的代码区域(而非数据区域)中,显示出编译器所产生的代码对于每一个__try块都使用相同的异常处理函数。刚才我说过了,前四笔是SHOWSEH.C的四个_try形成的。至于第5个则是在调用main之前由运行期函数库安装的。这些异常例程的地址统统都是__except_handler3 – Visual C++的运行期函数库中的一个函数。最后一个异常处理函数是默认的系统处理函数,位于KERNEL32.DLL中。
结构化异常处理与参数确认
Windows 95 SEH的一个重要的用途就是提供快速且容易的方法,检验API函数的参数。基本概念是:假设参数是正确的,然后执行一系列测试。然而在执行这些测试之前,程序代码首先得在SEH链表头部加上一个新的异常处理函数。如果参数是合法的,就不会有什么事情出发,而新添的那个异常处理函数也可以拿掉。这些只需要花费极小的执行时间。
如果参数是不正确的,就会引起CPU异常情况,于是我们安装的异常处理函数就会挺身处理。这个函数告诉操作系统,继续进行线程的执行,即使是在困难的处境下(可能引发API函数的失败)。让我们看看一些Win32 API函数的虚拟代码。本书对其他API的描述中,我对于SEH在参数检验的角色上都只是轻描淡写。对于随后介绍的GetCurrentDirectoryA,我将展示其虚拟代码,以展示操作系统如何使用SEH。
除了展示SEH的概念,GetCurrentDirecotryA函数的虚拟代码也详细示范了什么是一个典型的参数确认层。拥有参数确认层的函数被分割为两个部分。输出(exported)地址上的那一部分只是做参数检验的一小段代码而已。如果参数正确,那一段代码就会跳到真正的大角色去(位于同一模块中)。在Windows 3.1种,参数检验层之外的那个真正的函数本身,其函数名称前面多一个T。例如,本章稍早谈到的GetProceAddress是曾经看到的,它被分为两个部分:
GetProcessAddress stub
Validate the produre name string paramter
JMP IgetProcessAddress
IGetProcessAddress
Meat of the code that looks up a function address
为了让事情比较一致化,我也采用了Windows 3.1的原则(函数前面加个 T)。
GetCurrentDirectoryA
GetCurrentDirectoryA就是个典型的参数检验层。它一开始在堆栈之中产生一个EXCEPTIONREGISTRATIONRECORD(利用push指令)。这个结构中的prev_structure成员内容,通过push FS:[0]被放到堆栈之中。EXCEPTIONREGISTRATIONRECORD指定的异常处理函数是一个我命名为x_invalid_param_2_handler的函数。函数如果有两个参数,并且有一个参数检验层,就会在其检验过程中使用这个x_invalid_param_2_handler函数。
把EXCEPTIONREGISTRATIONRECORD压进堆栈之后,程序代码让一个指针指向FS:[0]所指的结构。这使得刚刚说的新的EXCEPTIONREGISTRATIONRECORD成为异常处理函数链中的第一个。此后,函数代码就可以安全接触lpszCurDir参数而不必管它是否合法了。这个例子中的参数检验动作包括lpszCurDir所指的内存是否可用、是否可写、长度是否足够等等。
一旦参数检验过程发生异常情况,x_invalid_param_2_handler就会获得控制权。这个函数将在下一节描述。如果参数是正确的,执行路径不会偏离。GetCurrentDirectorA最后会把异常处理函数一走。pop FS:[0]会恢复异常处理函数链的原始头部,add EPS 4则会抹去异常处理函数的地址。
假设每一件事情都做对了(也就是没有异常发生),GetCurrrentDirectoryA的最后一个动作就是跳到IGetCurrentDirectoryA去。令人惊讶的是,KERNEL32似乎并不适用存储在environment database中的current directory pointer,它进入Win16 Task Database并使用VWIN32的int 21h中断机制,调用int 21h的第19h号功能(Get Current Directory)和第7147号功能(用于处理长文件名)。
GetCurrentDirectoryA函数的虚拟代码:
// Paramters:
// DWORD cchCurDir
// LPTSTR lpszCurDir
// Set up structured exception handling frame. We do this by creating
// the exception record on the stack. An exception record looks like this
//
// prev_structure; // Pointer to previous record
// ExceptionHandler // Address of call on an exception
//
push offset x_invalid_param_2_handler // offset of handler
push FS:[0] // Head of list in FS:0
move FS:[0], esp // Point FS:0 at record we just built.
if ( lpszCurDir && cchCurDir )
{
LPSTR lpszEndPtr = lpszCurrDir + cchCurDir + 1;
LPSTR lpszTemp;
*lpszEndPtr += 0; // Harmlessly write the last byte in the buffer. If a fault occures, the
// exception handler will be invoked.
if ( lpszEndPtr != lpszCurDir )
{
lpszTemp = lpszCurDir;
// Go through each page between the start and end of the buffer and touch it.
// if a fault occurs, the exception handler will be invoked.
While ( lpszTemp < lpszEndPtr )
{
*lpszTemp += 0; // Harmlessly write to the page
lpszTemp += 0x1000; // Advance pointer to next page
}
}
}
// If we got here.,everything went well. Clear off the exception record from the statck
// and restore the previous head pointer.
pop FS:[0] // Put the previous head of the list into FS:0
add exp, 4 // Throw away the exception handler address we pushed.
goto IgetCurrentDirectoryA
x_invalid_param_handler
这个函数是“参数不合法”这种异常情况的典型终点站。这个函数并不是被直接调用。KERNEL32中有10个程序,每个小程序交给x_invalid_param_handler不同的参数。拥有10个小程序的原因是,x_invalid_param_handler必须负责移除那个“拥有不合法参数的函数”在堆栈中的所有参数。
x_invalid_param_handler函数必须执行三个主要动作。第一是在调试窗口中输出“Invalid parameter passed to: XXXXXX”信息。第二,函数调用RtlUnwind,负责将所有“在参数检验代码安装之后才能安装的异常处理函数”清理好。第三,也是最重要的一点,调用ReturnFailureCode函数(这是我命名的)。后者计算正确的失败代码,准备交给原函数(例如,GetCurrentDirectoryA),然后跳到原函数的退出程序(exit prologue)中。
所有这些背后的故事只是使得一个获得不合法参数的Win32函数执行失败而已。如果你在Windows 95调试版下,你将可以获得不合法参数的诊断信息。
x_invalid_param_X_param函数的虚拟代码:
x_invliad_param_1_param proc
x_invalid_param_handler( 0x04 );
x_invalid_param_2_params proc
// Parameters:
// struct _EXCEPTION_RECORD * ExceptionRecord
// void * EstablisherFrame,
// struct _CONTEXT* ContextRecord,
// void * DispatcherContext
x_invalid_param_handler( 0x08 )
x_invalid_param_3_params proc
x_invalid_param_handler( 0x0c )
x_invalid_param_4_params proc
x_invalid_param_handler( 0x10 )
x_invalid_param_5_params proc
x_invalid_param_handler( 0x14 )
x_invalid_param_6_params proc
x_invalid_param_handler( 0x18 )
x_invalid_param_7_params proc
x_invalid_param_handler( 0x1c )
x_invalid_param_8_params proc
x_invalid_param_handler( 0x20 )
x_invalid_param_9_params proc
x_invalid_param_handler( 0x24 )
x_invalid_param_special_proc
x_invalid_param_handler( 0x80000000 );
x_invalid_param_handler函数的虚拟代码:
// Parameters:
// DWORD cbParam
// DWORD caller_retAddr
// struct _EXCEPTION_RECORD * ExceptionRecord
// void* EstablisherFrame
// struct _CONTEXT* ContextRecord
// void* DispatcherContext
//
// Locals:
// DWORD faultEBX
// DWORD faultEBP
// DWORD faultESI
// DWORD faultEDI
// DWORD fSomeFlag
// if the unwinding flags are’t set (TRUE the first time through), then handle the exception
if ( 0 == (pExcRec->ExceptionFlags & (EH_UNWINDING | EN_EXIT_UNWIND)) )
{
if ( cbParams == 0 )
{
fSomeFlag = -1;
if ( !(pEstablisherFrame->8 & 0x100) )
fSomeFlag = 0;
}
else
fSomeFlag = 0;
dprint( “Invalid parameter passed to:\n” );
// Send the EIP out via the debugger INT 41h interface.
x_INT41_DS_print( “%pLNS”, pContext->EIP );
dprint( “ (%04x:%08x)\n”, pContext->SegCS, pContext->EIP );
push EBX, ESI, EDI // Preserve across the RTLUnwind call.
RtlUnwind( pEstablisherFrame, FFC00BAD, 0, 0 );
pop EDI,ESI,EBX
SetLastaError( ERROR_INVALID_PARAMETER );
// Restores EBX, ESI, EDI, ESP, and returns to original code.
return ReturnFailureCode( cbParams,
fSomeFlag,
pEstablisher,
faultEBX,
faultESI,
faultEDI,
faultEBP );
}
return XCPT_CONTINUE_SEARCH;
Thread Local Storage(线程本地存储空间)
TLS是一个很好的Win32特性,让多线程程序设计更容易一些。TLS是一种机制,通过它,程序可以拥有全局变量,但出于“每个线程各不相同”的状态。也就是说,进程中的所有线程都可以拥有全局变量,但这些全局变量针对特定的线程才有意义。例如,你可能有一个多线程程序,每一个线程都对不同的文件(因此,每个线程使用不同的文件handle)。这种情况下,把每个线程所使用的文件handle存储在TLS中,将会十分方便。当线程需要知道所使用的handle,它可以从TLS获取。重点在于:线程用来取得文件handle的那一段代码在任何情况下都是相同的,而从TLS中取出的文件handle却各不相同。非常巧妙,不是吗?既有全局变量的便利,却又分属于不同的线程。
当然,你可以使用链表,让一个文件handle与一个Thread ID产生关系。让每一个线程拥有一个节点。用此来模拟TLS。当线程需要知道它使用哪个文件handle,它可以从链表中寻找文件handle。你当然可以把文件handle保存在局部变量(位于线程自己的堆栈中)。但是却因此要把这个handle在函数与函数之间传来传去。那多痛苦。TLS可以利用简单的alloc/set/get/free函数消除这些问题。
虽然TLS很方便,但它并不是毫无限制。在Windows NT和Windows 95之中,有64个DWORD slots供每个线程使用。这意味着每个线程最多可以使用64个“对各线程有不同意义”的DWORDs。为了在每个线程中保留一个slot,程序应该调用TlsAlloc。每次调用TlsAlloc就会返回一个可被所有线程使用的索引值。这个索引值常常被存储在全局变量中。当线程需要一个slot写入数据,它使用TlsSetValue,传入一个TLS索引和一笔数据。稍后当线程要取出此值,它调用TlsGetValue,再次传入一个TLS索引。最后,程序调用TlsFree并交待一个TLS索引,将slot释放掉。这么一来当然也就让slot不再能够被任何线程使用,因为TLS索引值在各线程之间是共通的。
译注:
如果你想明白上一段话的意义和TLS在系统层面上的实际影响,请详细越多稍后的四个TLS函数的详细说明。
虽然TLS可以存放单一数值,如文件handle,更常见的用途是存放指针,指向线程的私有数据。有许多情况,多线程程序需要存储一堆数据,而它们又都是与各线程相关的。许多程序员对此的做法是把这些变量包装为一个C结构体,然后把该结构体的指针保存在TLS中。当新的线程诞生,程序就分配一些内存给结构体使用,并且把指针存放在为线程保留下来的TLS中。一旦线程结束,程序代码就释放所有分配来的区块。
这种程序风格的最佳示范就是第10章的APISPY32。APISPY32.DLL需要保持一个堆栈,用来返回它所拦截的函数地址(我在这里使用古典的计算机科学术语—堆栈(Stack),事实上我指的是一个结构链表,以及一个堆栈指针)。由于被拦截的程序可能有许多线程,APISPY32.DLL必须针对每个线程保留各自的返回地址。
如果每个线程有64个slots用来存放线程自己的数据,这些空间从哪儿来?稍早我曾说过,每个Thread Database有64个DWORDs给TLS使用。当你以TLS函数设定或取出数据,事实上你真正面对的就是那64个DWORDs。没有任何公开文件告诉我们可以存取其他线程的TLS。让我们更详细的看看这些TLS函数。
TlsAlloc
由于TLS只提供最多64个slots给每个线程使用(在Windows 2000下,每个线程可使用1000个以上的slots),所以必须有某种方法追踪哪一个slot已被使用。KERNEL32使用两个DWORDs(总共64位)来记录哪一个slot是可用的、哪一个slot已经被使用。这两个DWORDs可想象成为一个64位数组,如果64位中的某位被设立,就表示它对应的TLS slot已被使用。
这64位的TLS slot数组存放在Process Database中(可能你会猜想在Thread Database中,不,不是这样)。记住,当你分配一个TLS slot,这个slot可以在进程所拥有的任何线程中被该索引值引用到。64位的TLS slot数组存放在Process Database的0x88和0x8C两个DWORD。虽然下面的TlsAlloc函数虚拟代码可能看起来有些复杂,事实上并不如此。其中所做的只不过是扫描64位数组中的每个位,看看有没有哪一个是0。如果找到,就把它改为1,并返回其在数组中的位置(作为索引值使用)。因此,如果第5个位是0,TlsAlloc就把它改为1并返回4(索引值从0开始)。
TlsAlloc函数的虚拟代码:
// Locals:
// DWORD i
// PDWORD pTlsInUseBits
// DWORD newFlag
x_LogSomeKernelFunction( function number for TlsAlloc );
i = 0;
_EntrySysLevel( x_TlsMutex );
pTlsInUseBits = &ppCurrentProcess->tlsInUseBits1;
// Poition pTlsInUseBits so that it points at the first of the two tls bit DWORDs that has
// a free bit available
while ( *pTlsInUseBits == 0xFFFFFFFF && ( i < 2 ) )
{
i++;
pTlsInUseBits++; // Point at next DWORD of tlsInUseBits.
}
if ( i < 2 ) // if a free bit-slot wsas found, it is 0 or 1
{
i *= 32; // “i” starts at either 0 or 1, so the end result is
// either 0 or 32. There are 32 “inUse” bits in each
// of the TlsInUseBits DWORDs
newFlag = 1;
if ( *pTlsInUseBits & newFlag )
{
// Blast through the bits in this DWORD until we find one that’s 0 (available).
// Keep incrementing “i” so that when we’re done, it’s a TLS index.
do
{
i++;
newFlags << 1;
}while ( *pTlsInUseBits & newFlag )
}
*pTlsInUseBits |= newFlag; // Turn on the newly allocated bit to indicate that the
// corresponding TLS index is in use.
}
else
{
// If we get here, all the TLS indices were in use. Return -1 and set the last error code.
i = TLS_OUT_OF_INDEXES; // 0Xffffffff
InternalSetLastError( ERROR_NO_MORE_ITEMS );
}
_LeaveSysLevel( x_TlsMutex );
return i;
补充:
上述关于TlsAlloc的描述,通过观察该函数在Windows 2000中的行为与上述虚拟代码的描述是相同的。
TlsSetValue
TlsSetValue可以把数据放入先前分配的TLS slot中。两个参数分别是TLS slot索引值以及准备写入的数据内容。函数首先检查数组索引是否合法(小于64)。Windows 95的早期版本还会检查此索引是否的确被分配了,但是在beta3版本中就只作上述的最简单检查。如果索引值的确小于64,TlsSetValue就把你指定的数据放入64 个DWORDs所组成的数组(位于当前的Thread Database)的适当位置中。
除此之外,TlsSetValue还更新第二个64 DWORDs数组。这个数组内含EIP值,TlsSetValue上一次就是在那里被调用的。很明显,这些EIP是为了调试用的。微软并没有提供什么方法让应用程序取用这块数据。
TlsSetValue函数的虚拟代码:
// Parameters:
// DWORD dwTlsIndex;
// LPVOID lpvTlsValue;
// Locals:
// PTHREAD_DATABASE ptdb
// The thread database starts 0x10 bytes before the TIB pointed to by the FS
// register. Make a pointer to the thread database
ptdb = FS:[ptibSel] – 0x10;
if ( dwTlsIndex < TLS_MINIMUM_AVAILABLE(64) )
{
ptdb->TLSArray[ dwTlsIndex ] = *lpvTlsValue;
// Grab return EIP off the stack and store in the oters TLS array that runs
ptdb->LastTlsSetValueEIP[ dwTlsIndex ] = [EBP+04];
return TURE;
}
else
{
ptdb->GetLastErrorCode = ERROR_INVALID_PARAMETER;
return 0;
}
TlsGetValue
这个函数似乎总是TlsSetValue的一面镜子,最大的差异是它取出数据而非保存数据。和TlsSetValue一样,这个函数也是先检查TLS索引值合法与否。如果是,TlsGetValue就使用这个索引值找到64 DWORDs数组(位于Thread Database中)的对应数据项,并将其内容返回。
TlsGetValue函数的虚拟代码:
// Parameters:
// DWORD dwTlsIndex
// Locals:
// PTHREAD_DATABASE ptdb
// The Thread database starts 0x10 bytes before the TIB pointed to by the FS register.
// Make a pointer to the thread database.
ptdb = FS:[ptibSelf] – 0x10;
if ( dwTlsIndex < TLS_MINIMUM_AVAILABLE(64) )
{
// Set last error value to 0
ptdb->GetLastErrorCode = ERROR_SUCESS;
return ptdb->TLSArray[ dwTlsIndex ];
}
else // The TLS index passed in wa >= TLS_MINIMUM_AVAILABLE.
{
ptdb->GetLastErrorCode = ERROR_INVALID_PARAMETER;
return 0;
}
补充:
在Windows 2000中,TlsSetValue/TlsGetValue并不对dwTlsIndex进行合法性检查。
TlsFree
这个函数将TlsAlloc和TlsSetValue的努力全部抹掉。TlsFree先检查你交给它的索引值是否的确被分配过。如果是,它将对应的64位TLS slot的相应位关闭(将其置0)。然后,为了避免那个已经不再合法的内容被使用,TlsFree遍历进程的每个线程,把0放到刚被释放的那个TLS slot上头。于是呢,如果某个TLS索引后来又被重新分配,所有用到该索引的线程就保证会取回一个0值,除非它们再调用TlsSetValue。
TlsFree函数的虚拟代码:
// Parameters:
// DWORD dwTlsIndex
// Locals:
// DWORD retValue
// PDWORD pTlsInUseBits;
// PTHREAD_DATABASE ptdb;
// PK32OBJECTLISTENTRY pK32Object
x_LogSomeKernelFunction( function number for TlsFree );
_EntrySysLevel( pKrn32Mutex );
_EntrySysLevel( x_TlsMutex );
point pTlsInUseBits to either ppCurrentProcess->tlsInUseBits1
or ppCurrentProcess->tlsInUseBits2 as appropriate.
if ( dwTlsIndex < TLS_MINIMUM_AVAILABLE )
{
DWORD turnOffFlag;
// Create a DWORD with the appropriate flag set that represents the TLS index to be freed.
turnOffFlag = 1 << ( dwTlsIndex & 0x1F );
// If that bit is already turned off in the process database’s tlsInUseBits filed.
// The TLS index isn’t allocated. This is a bad thing, so go report an error.
if ( 0 == turnOffFlag & *pTlsInUseBits )
{
goto error;
}
// Turn off the correct bit in the tlsInUseBits field of the process database
*pTlsInUseBits = ~turnOffFlag;
// Now walk through each of the threads of the process, putting the value 0
// into the DWORD assigned to the TLS index we’re freeing.
pK32Object = x_GetNextObjectInList(ppCurrentProcess->ThreadList, 0);
while ( pK32Object )
{
ptdb = pK32Object->pObject;
ptdb->TLSArray[ dwTlsIndex ] = 0;
ptdb->AnotherTlsArray[ dwTlsIndex ] = 0;
pK32Object = x_GetNextObjectInList( ppCurrentProcess->ThreadList,1);
}
retValue = 1;
}
else
{
error:
retValue = 0;
InternealSetLastError( ERROR_INVALID_PARAMETER );
}
done:
_LevelSysLevel( x_TlsMutex );
_LevelSysLevel( pKrn32Mutex );
return retValue;
线程的杂项函数
本节描述的各个函数无法归类于前面各个主题中。虽然如此,但它们都是十分重要的函数,可以强调“KERNEL32.DLL使用Thread Database”这一事实。
GetLastError
GetLastError是一个机制,应用程序可以利用它决定为什么某个系统调用会失败。当Windows 95函数失败,它可以选择性的在当前线程中设定“最后错误代码(last error code)”,指示失败的原因。WINERROR.H中定义有许多错误代码。GetLastError有点像C函数库中的error变量。
除了系统函数,应用程序也可以参与这场派对,并且调用SetLastError。那些错误代码最好是和系统定义的错误代码不同。
GetLastError函数动作很简单。在确定的确有查询对象(某个线程)之后,它就返回该线程的Thread Database的GetLastErrorCode成员内容。如果没有查询对象(即某个线程),此函数就返回KERNEL32全局变量中的值。该全局变量出现在下面的虚拟代码中(我没有给它命名)。
GetLastError函数的虚拟代码:
if ( ppCurrentThread )
return ppCurrentThread->GetLastErrorCode
else
return x_LastErrorIfNoCurrentThread; // A Global Variable in KERNEL32.DLL
SetLastError
SetLastError也非常简单。如果目前存在有线程,这个函数就设定其Thread Database中的GetLastErrorCode成员。
SetLastError函数的虚拟代码:
// Locals:
// DWORD fdwError
if ( ppCurrentThread )
ppCUrrentThread->GetLastErrorCode = fdwError
GetExitCodeThread和IGetExitCodeThread
GetExitCodeThread返回由hThread所指定的线程的退出状态。线程退出状态应纪录在Thread Database的TerminationStatus成员中。正常执行情况下,其值为0x103(STILL_ACTIVE)。
GetExitCodeThread只不过是参数检查而已,在检查传进来的指针合法之后,它就调用IGetExitCodeThread。
IGetExitCodeThread首先进入一个“必须完成(must complete)”的段落中,然后把hThread转化为Thread Database指针。在取出TerminationStatus成员后,它离开那个“必须完成”的段落。
补充:
TLS的应用是非常广泛的,C/C++运行期库就使用了TLS。下图就是Windows 2000中TLS的内部管理结构:
用于管理TLS的内部管理结构
Mrcosoft保证至少TLS_MINIMUM_AVAILABLE个slot是可用的。在WINNT.H中TLS_MINIMUM_AVAILABLE被定义为64。Windows 2000将其扩展为允许有1000个以上的TLS的slot存在。对于任何应用来说,这个数量足够了。