上一篇文章Windows对异常的管理详细介绍了Windows中对异常的分发和处理过程。 我们知道,Windows会给每个异常最多两轮被处理的机会:
- 第一轮:先尝试发给调试器,如果调试器没有处理,则遍历并依次调用异常处理器。如果一个处理器返回
EXCEPTION_CONTINUE_EXECUTION ,则表示它已经处理了异常,并让系统恢复执行因为异常而中断的代码;如果返回EXCEPTION_CONTINUE_SEARCH ,则表示它不能处理该异常,并让系统继续寻找其他的异常处理器。 - 第二轮:再次发给调试器,如果还没有处理,则通过KeBugCheckEx触发蓝屏。
未处理异常
未处理异常:系统遍历了所有的异常处理器都处理不了的异常。
系统只有在第一轮分发时,才会把异常分发给用户代码注册的异常处理器,第二轮分发时并不会这么做。因此,对于SEH或VEH异常处理器来说,只有一轮处理异常的机会。如果在第一轮异常分发的过程中一个异常没有被处理,那么它便称为未处理异常,进入到第二轮分发的异常都属于未处理异常。
根据程序的运行模式,把发生在驱动程序等内核态模块中的未处理异常称为内核态的未处理异常,把发生在应用程序中的未处理异常称为用户态的未处理异常。
内核态的未处理异常
Windows对它的处理很简单:
- 若有内核调试器存在:系统(KiExceptionDispatch)会给调试器第二轮处理机会;
- 若调试器没有处理该异常或者根本没有内核调试器:系统便会调用KeBugCheckEx发起蓝屏机制,报告错误并停止整个系统,其停止码为
KMODE_EXCEPTION_NOT_HANDLED 。
Windows这么做的理由是,它把内核态执行的代码看作是系统信任的代码,这些代码应该是经过缜密设计和认真测试过的,因此,一旦在信任代码中发生未处理异常,那么一定是发生了事先没有估计到的严重问题,蓝屏终止可让系统以可控的方式停止工作,防止其继续运行造成更大的损失。
用户态的未处理异常
对于用户态的未处理异常,Windows使用系统登记的默认异常处理器来处理。 Windows为应用程序的每个线程都设置了默认的SEH异常处理器,此外编译器在编译时插入的启动函数通常也会注册一个SEH处理器。对于使用C运行库的程序,C运行库包含了基于信号的异常处理器机制。 当应用程序内的代码没有处理异常时,系统会使用这些默认的异常处理器来处理异常。
BaseProcessStart中的SEH处理器
首先介绍一下Windows启动一个程序的大体过程: 不论是通过双击程序文件、键入程序命令,还是在程序中调用CreateProcess 来启动一个程序,其内部过程都是类似的。
- 打开要执行的程序映像文件,创建section对象用于将文件映射到内存中。
- 建立进程运行所需的各种数据结构(
EPROCESS 、KPROCESS 及PEB )和地址空间。 - 调用
NtCreateThread ,创建处于挂起状态的初始线程,将用户态的初始地址存储在ETHREAD 结构中。 - 通知Windows子系统注册新的进程。
- 开始执行初始线程。
- 在新进程的上下文中对进程做最后的初始化工作。
第3 点决定了初始线程的起始地址,也就是新线程开始在用户态正式运行时的起始地址。Windows程序的PE文件中登记了程序的入口地址,即IMAGE_OPTIONAL_HEADER->AddressOfEntryPoint 字段。但是在创建Windows子系统进程时,系统通常并不把这个地址用作新线程的起始地址,而是把起始地址指向kernel32.dll 中的进程启动函数BaseProcessStart 。
原因就是要注册一个默认的SEH处理器,它是初始化线程中注册的第一个异常处理器,但它是最后得到处理机会的。只有当应用程序自己设计的代码没有处理异常时,这个默认的SEH处理器才会得到处理机会。
编译器插入的SEH处理器
BaseProcessStart的参数lpStartAddress 指向的入口地址并不是main 或WinMain 函数的地址,因为在执行这些函数前,还有很多准备工作要做,比如初始化C运行库、初始化全局变量、初始化C运行库所使用的堆、准备命令行参数等。为了做这些准备工作,编译器通常会把自身提供的一个启动函数登记为程序的入口,让系统先执行这个启动函数,这个启动函数内部再调用用户编写的main或WinMain函数。 通常是:mainCRTStartup 、wmainCRTStartup 、WinMainCRTStartup 、wWinMainCRTStartup 。
我们看一下这些启动函数的源码实现,太长了不好截图,就直接粘过来了。 可以发现在它里面也注册了一个SEH处理器,这是应用程序初始线程的第二个异常处理器。
#ifdef _WINMAIN_
#ifdef WPRFLAG
int wWinMainCRTStartup(
#else
int WinMainCRTStartup(
#endif
#else
#ifdef WPRFLAG
int wmainCRTStartup(
#else
int mainCRTStartup(
#endif
#endif
void)
{
int initret;
int mainret;
OSVERSIONINFOA *posvi;
int managedapp;
#ifdef _WINMAIN_
_TUCHAR *lpszCommandLine;
STARTUPINFO StartupInfo;
#endif
posvi = (OSVERSIONINFOA *)_alloca(sizeof(OSVERSIONINFOA));
posvi->dwOSVersionInfoSize = sizeof(OSVERSIONINFOA);
(void)GetVersionExA(posvi);
_osplatform = posvi->dwPlatformId;
_winmajor = posvi->dwMajorVersion;
_winminor = posvi->dwMinorVersion;
_osver = (posvi->dwBuildNumber) & 0x07fff;
if ( _osplatform != VER_PLATFORM_WIN32_NT )
_osver |= 0x08000;
_winver = (_winmajor << 8) + _winminor;
managedapp = check_managed_app();
#ifdef _MT
if ( !_heap_init(1) )
#else
if ( !_heap_init(0) )
#endif
fast_error_exit(_RT_HEAPINIT);
#ifdef _MT
if( !_mtinit() )
fast_error_exit(_RT_THREAD);
#endif
#ifdef _RTC
_RTC_Initialize();
#endif
__try {
if ( _ioinit() < 0 )
_amsg_exit(_RT_LOWIOINIT);
#ifdef WPRFLAG
_wcmdln = (wchar_t *)__crtGetCommandLineW();
_wenvptr = (wchar_t *)__crtGetEnvironmentStringsW();
if ( _wsetargv() < 0 )
_amsg_exit(_RT_SPACEARG);
if ( _wsetenvp() < 0 )
_amsg_exit(_RT_SPACEENV);
#else
_acmdln = (char *)GetCommandLineA();
_aenvptr = (char *)__crtGetEnvironmentStringsA();
if ( _setargv() < 0 )
_amsg_exit(_RT_SPACEARG);
if ( _setenvp() < 0 )
_amsg_exit(_RT_SPACEENV);
#endif
initret = _cinit();
if (initret != 0)
_amsg_exit(initret);
#ifdef _WINMAIN_
StartupInfo.dwFlags = 0;
GetStartupInfo( &StartupInfo );
#ifdef WPRFLAG
lpszCommandLine = _wwincmdln();
mainret = wWinMain(
#else
lpszCommandLine = _wincmdln();
mainret = WinMain(
#endif
GetModuleHandleA(NULL),
NULL,
lpszCommandLine,
StartupInfo.dwFlags & STARTF_USESHOWWINDOW
? StartupInfo.wShowWindow
: SW_SHOWDEFAULT
);
#else
#ifdef WPRFLAG
__winitenv = _wenviron;
mainret = wmain(__argc, __wargv, _wenviron);
#else
__initenv = _environ;
mainret = main(__argc, __argv, _environ);
#endif
#endif
if ( !managedapp )
exit(mainret);
_cexit();
}
__except ( _XcptFilter(GetExceptionCode(), GetExceptionInformation()) )
{
mainret = GetExceptionCode();
if ( !managedapp )
_exit(mainret);
_c_exit();
}
return mainret;
}
基于信号的异常处理
在编译器入口函数的SEH处理器的__except中,过滤表达式的内容是调用_XcptFilter 函数。它内部调用了UnhandledExceptionFilter 。 这里涉及到的是C运行库中基于信号的异常处理器,主要是为了兼容来自UNIX系统的软件才存在的,对于Win32程序不起什么作用,我们暂不做详细介绍了。
BaseThreadStart中的SEH处理器
对于初始线程之外的其他线程,其用户态的起始地址是系统提供的另一个位于kernel32.dll 中的函数BaseThreadStart ,它内部也注册了一个SEH处理器。
UnhandledExceptionFilter
我们总结这几个默认的SEH处理器的过滤表达式,发现它们最终都调用了UnhandledExceptionFilter 函数。
函数 | 过滤表达式 | 内部调用 |
---|
BaseProcessStartup | BaseExceptionFilter | UnhandledExceptionFilter | mainCRTStartup | _XcptFilter | UnhandledExceptionFilter | BaseThreadStartup | BaseThreadExceptionFilter | UnhandledExceptionFilter |
UnhandledExceptionFilter函数才是真正的大BOSS!!!它的源码也很长,在看源码之前,先介绍几个概念。
JIT调试
JIT调试:在应用程序出现严重错误后而启动的紧急调试。 因为JIT调试建立时,被调试的应用程序内已经发生了严重的错误。通常都无法再恢复正常运行,所以JIT调试又被称为事后调试(Postmortem Debugging)。
JIT调试的主要目的是分析和定位错误原因,或者收集和记录错误发生时的现场数据供事后分析。很多调试器都可以作为JIT调试器来使用,Dr.Watson是Windows系统中默认的JIT调试器。
关于JIT调试器的配置信息被保存在注册表的如下键中: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug AeDebug(AE指Application Error)键下通常包括三个键值:Auto 、Debugger 、UserDebugger-HotKey 。
- Auto:决定是否自动启动JIT调试器。也就是当有未处理异常发生时,是先询问用户还是直接启动JIT调试器。它的有效值只有
"0" 和"1" 两种,如果该值为"1" 且Debugger 非空,那么系统就会直接启动JIT调试器;否则系统会先显示应用程序错误 对话框,等用户选择调试选项后,再启动JIT调试器。 - Debugger:用来定义启动JIT调试器的命令行。如果该项存在,那么在应用程序错误对话框内就会包含
调试 或取消 按钮,供用户选择是否调试发生错误的程序;如果不存在或为空,那么弹出的对话框中就不包含调试或取消按钮。 - UserDebuggerHotKey:用来定义中断到调试器的热键(默认为F12)。
应用程序错误对话框
当Windows检测到应用程序内发生了未处理异常或其他严重错误时,系统的策略是将其终止。在终止前,系统通常会弹出一个对话框来通知用户这个程序即将被关闭,这个对话框称为应用程序错误对话框(Application Fault Dialog)或GPF错误框(General Protection Fault)。
源码剖析
有了这两个概念之后,我们来看看UnhandledExceptionFilter 函数的源码实现,详情见注释:
LONG
WINAPI
UnhandledExceptionFilter(IN PEXCEPTION_POINTERS ExceptionInfo)
{
static UNICODE_STRING AeDebugKey =
RTL_CONSTANT_STRING(L"\\Registry\\Machine\\" REGSTR_PATH_AEDEBUG);
static BOOLEAN IsSecondChance = FALSE;
NTSTATUS Status;
PEXCEPTION_RECORD ExceptionRecord = ExceptionInfo->ExceptionRecord;
LPTOP_LEVEL_EXCEPTION_FILTER RealFilter;
LONG RetValue
HANDLE DebugPort = NULL;
ULONG_PTR ErrorParameters[4];
ULONG DebugResponse, ErrorResponse;
HANDLE KeyHandle;
OBJECT_ATTRIBUTES ObjectAttributes;
UNICODE_STRING ValueString;
ULONG Length;
UCHAR Buffer[sizeof(KEY_VALUE_PARTIAL_INFORMATION) + MAX_PATH * sizeof(WCHAR)];
PKEY_VALUE_PARTIAL_INFORMATION PartialInfo = (PVOID)Buffer;
BOOLEAN AeDebugAuto = FALSE;
PWCHAR AeDebugPath = NULL;
WCHAR AeDebugCmdLine[MAX_PATH];
BOOL Success;
HRESULT hr;
ULONG PrependLength;
HANDLE hDebugEvent;
HANDLE WaitHandles[2];
STARTUPINFOW StartupInfo;
PROCESS_INFORMATION ProcessInfo;
if (ExceptionRecord->ExceptionFlags & EXCEPTION_NESTED_CALL)
{
NtTerminateProcess(NtCurrentProcess(), ExceptionRecord->ExceptionCode);
return EXCEPTION_EXECUTE_HANDLER;
}
if ((ExceptionRecord->ExceptionCode == STATUS_ACCESS_VIOLATION) &&
(ExceptionRecord->NumberParameters >= 2))
{
switch (ExceptionRecord->ExceptionInformation[0])
{
case EXCEPTION_WRITE_FAULT:
{
RetValue = BasepCheckForReadOnlyResource(
(PVOID)ExceptionRecord->ExceptionInformation[1]);
if (RetValue == EXCEPTION_CONTINUE_EXECUTION)
return EXCEPTION_CONTINUE_EXECUTION;
break;
}
case EXCEPTION_EXECUTE_FAULT:
break;
}
}
Status = NtQueryInformationProcess(NtCurrentProcess(),
ProcessDebugPort,
&DebugPort,
sizeof(DebugPort),
NULL);
if (NT_SUCCESS(Status) && DebugPort)
{
DPRINT("Passing exception to debugger\n");
return EXCEPTION_CONTINUE_SEARCH;
}
RealFilter = RtlDecodePointer(GlobalTopLevelExceptionFilter);
if (RealFilter)
{
RetValue = RealFilter(ExceptionInfo);
if (RetValue != EXCEPTION_CONTINUE_SEARCH)
return RetValue;
}
PrintStackTrace(ExceptionInfo);
if ((GetErrorMode() & SEM_NOGPFAULTERRORBOX) ||
(RtlGetThreadErrorMode() & RTL_SEM_NOGPFAULTERRORBOX))
{
return EXCEPTION_EXECUTE_HANDLER;
}
ErrorParameters[0] = (ULONG_PTR)ExceptionRecord->ExceptionCode;
ErrorParameters[1] = (ULONG_PTR)ExceptionRecord->ExceptionAddress;
if (ExceptionRecord->ExceptionCode == STATUS_IN_PAGE_ERROR)
{
ErrorParameters[2] = ExceptionRecord->ExceptionInformation[2];
}
else
{
ErrorParameters[2] = ExceptionRecord->ExceptionInformation[0];
}
ErrorParameters[3] = ExceptionRecord->ExceptionInformation[1];
DebugResponse = OptionOk;
AeDebugAuto = FALSE;
InitializeObjectAttributes(&ObjectAttributes,
&AeDebugKey,
OBJ_CASE_INSENSITIVE,
NULL,
NULL);
Status = NtOpenKey(&KeyHandle, KEY_QUERY_VALUE, &ObjectAttributes);
if (NT_SUCCESS(Status))
{
RtlInitUnicodeString(&ValueString, REGSTR_VAL_AEDEBUG_AUTO);
Status = NtQueryValueKey(KeyHandle,
&ValueString,
KeyValuePartialInformation,
PartialInfo,
sizeof(Buffer),
&Length);
if (NT_SUCCESS(Status) && (PartialInfo->Type == REG_SZ))
{
AeDebugAuto = (*(PWCHAR)PartialInfo->Data == L'1');
}
else
{
AeDebugAuto = FALSE;
}
RtlInitUnicodeString(&ValueString, REGSTR_VAL_AEDEBUG_DEBUGGER);
Status = NtQueryValueKey(KeyHandle,
&ValueString,
KeyValuePartialInformation,
PartialInfo,
sizeof(Buffer),
&Length);
if (NT_SUCCESS(Status) && (PartialInfo->Type == REG_SZ))
{
AeDebugPath = (PWCHAR)PartialInfo->Data;
while ( *AeDebugPath &&
((*AeDebugPath == L' ') ||
(*AeDebugPath == L'\t')) )
{
++AeDebugPath;
}
if (*AeDebugPath)
{
DebugResponse = OptionOkCancel;
}
else
{
AeDebugPath = NULL;
}
}
else
{
AeDebugPath = NULL;
}
NtClose(KeyHandle);
}
TODO: Start a ReactOS Fault Reporter (unimplemented!)
if (!(AeDebugPath && AeDebugAuto))
{
Status = NtRaiseHardError(STATUS_UNHANDLED_EXCEPTION | HARDERROR_OVERRIDE_ERRORMODE,
4,
0,
ErrorParameters,
(!IsSecondChance ? DebugResponse : OptionOk),
&ErrorResponse);
}
else
{
Status = STATUS_SUCCESS;
ErrorResponse = (AeDebugPath ? ResponseCancel : ResponseOk);
}
if (!NT_SUCCESS(Status) || (ErrorResponse != ResponseCancel) || IsSecondChance)
goto Quit;
if (BaseRunningInServerProcess)
{
IsSecondChance = TRUE;
goto Quit;
}
InitializeObjectAttributes(&ObjectAttributes,
NULL,
OBJ_INHERIT,
NULL,
NULL);
Status = NtCreateEvent(&hDebugEvent,
EVENT_ALL_ACCESS,
&ObjectAttributes,
NotificationEvent,
FALSE);
if (!NT_SUCCESS(Status))
hDebugEvent = NULL;
Success = FALSE;
Length = wcslen(AeDebugPath) + 2*10 + 1;
if ((*AeDebugPath != L'"') &&
(RtlDetermineDosPathNameType_U(AeDebugPath) == RtlPathTypeRelative))
{
PrependLength = wcslen(SharedUserData->NtSystemRoot) + 10
if (PrependLength + Length <= ARRAYSIZE(AeDebugCmdLine))
{
hr = StringCchPrintfW(AeDebugCmdLine,
PrependLength + 1,
L"%s\\System32\\",
SharedUserData->NtSystemRoot);
Success = SUCCEEDED(hr);
}
}
else
{
PrependLength = 0;
if (Length <= ARRAYSIZE(AeDebugCmdLine))
Success = TRUE;
}
if (Success)
{
hr = StringCchPrintfW(&AeDebugCmdLine[PrependLength],
Length,
AeDebugPath,
HandleToUlong(NtCurrentTeb()->ClientId.UniqueProcess),
hDebugEvent);
Success = SUCCEEDED(hr);
}
if (Success)
{
DPRINT1("\nStarting debugger: '%S'\n", AeDebugCmdLine);
RtlZeroMemory(&StartupInfo, sizeof(StartupInfo));
RtlZeroMemory(&ProcessInfo, sizeof(ProcessInfo));
StartupInfo.cb = sizeof(StartupInfo);
StartupInfo.lpDesktop = L"WinSta0\\Default";
Success = CreateProcessW(NULL,
AeDebugCmdLine,
NULL, NULL,
TRUE, 0,
NULL, NULL,
&StartupInfo, &ProcessInfo);
}
if (Success)
{
WaitHandles[0] = hDebugEvent;
WaitHandles[1] = ProcessInfo.hProcess;
do
{
Status = NtWaitForMultipleObjects(ARRAYSIZE(WaitHandles),
WaitHandles,
WaitAny,
TRUE, NULL);
} while ((Status == STATUS_ALERTED) || (Status == STATUS_USER_APC));
if (Status == STATUS_WAIT_1)
{
Status = NtQueryInformationProcess(NtCurrentProcess(),
ProcessDebugPort,
&DebugPort,
sizeof(DebugPort),
NULL);
if (!NT_SUCCESS(Status) || !DebugPort)
{
IsSecondChance = TRUE;
}
}
CloseHandle(ProcessInfo.hThread);
CloseHandle(ProcessInfo.hProcess);
if (hDebugEvent)
NtClose(hDebugEvent);
return EXCEPTION_CONTINUE_SEARCH;
}
if (hDebugEvent)
NtClose(hDebugEvent);
IsSecondChance = TRUE;
Quit:
if (IsSecondChance)
NtTerminateProcess(NtCurrentProcess(), ExceptionRecord->ExceptionCode);
return EXCEPTION_EXECUTE_HANDLER;
}
|