The following diagram shows a high-level picture how exceptions work on 32-bit Windows:
If something bad happens, e.g. an invalid memory access, the hardware exception is caught by the kernel, which resumes user level execution in ntdll!KiUserExceptionDispatcher
. This function is hooked by Valgrind, so the exception can be analyzed etc. The rest of the dispatcher is then ran on the emulated CPU (necessary mostly because RtlDispatchException()
can call application code), which is done by longjumping back to the scheduler with updated EIP
and stack.
Then, if a viable exception handler has been found, NtContinue
syscall is issued and caught by Valgrind, which copies the updated context values to the VEX context structures and executes the syscall with a dummy CONTEXT
structure in order to call it, but effectively just return to the syscall PRE-wrapper, which sets SsComplere
and SfDontWriteResult
and the scheduler resumes at the updated location.
If no useful exception handler could be found, the syscall issued is NtRaiseException
, with the FirstChance
argument set to FALSE
. This is intercepted by Valgrind and the process is terminated, as that would happen if the syscall was passed to the kernel anyway. (To be precise, UnhandledExceptionFilter
would be called. Maybe we should call it too if the application uses its own routine?)
The client code can call RaiseException()
to throw an exception at any time. This is used by MSVS C++ exceptions, internally by OutputDebugString()
etc. In this case, the process is exactly the same, except the exception does not originate from generated code but by stopping emulation and calling a syscall NtRaiseException
.
The syscall is let through, so after kernel does its job, the execution gets to the hooked KiUserExceptionDispatcher
again.
Although Valgrind itself does not use exceptions and will not throw any through the RaiseException()
API, there is a possibility of crash in V/tool code because of bugs. Such exceptions will take the same route as hardware exceptions originating from client code, with the difference that VG_(in_generated_code)
will be false in the KiUserExceptionDispatcher
hook. So this case can be easily discerned and a helpful report about bug in Valgrind shown to the user instead. Then the process is terminated.
SetUnhandledExceptionFilter()
is also called early during startup to catch similarly any problems during initialization.
TBD
There are two entry points in ntdll.dll, which the kernel uses when dealing with exceptions -- KiUserExceptionDispatcher
and KiRaiseUserExceptionDispatcher
. The latter is used only when kernel wants to raise an exception and essentially just calls RtlRaiseException()
, which is not very interesting. KiUserExceptionDispatcher
is much more useful, as it is the function called when an exception has occurred and needs to be handled.
This section describes how Valgrind for Windows intercepts KiUserExceptionDispatcher
. In general:
ntdll!!KiUserExceptionDispatcher
code to call an assembler stub.Related files:
Original ntdll!KiUserExceptionDispatcher | Hooked ntdll!KiUserExceptionDispatcher |
---|---|
0000 8b4c2404 mov ecx, \[esp+4] 0004 8b1c24 mov ebx, \[esp] 0007 51 push ecx 0008 53 push ebx 0009 e8xxxxxxxx call RtlDispatchException 000e 0ac0 or al, al 0010 740c je .001e 0012 5b pop ebx 0013 59 pop ecx 0014 6a00 push 0 0016 51 push ecx 0017 e8xxxxxxxx call NtContinue 001c eb0b jmps .0029 001e 5b pop ebx 001f 59 pop ecx 0020 6a00 push 0 0022 51 push ecx 0023 53 push ebx 0024 e8xxxxxxxx call NtRaiseException 0029 83c4ec add esp, -0x14 002c 890424 mov \[esp], eax 002f c744240401000000 mov dword ptr \[esp+4], 1 0037 895c2408 mov \[esp+8], ebx 003b c744241000000000 mov \[esp+0x10], 0 0043 54 push esp 0044 e8xxxxxxxx call RtlRaiseException 0049 c20800 retn 8 | 0000 ff15xxxxxxxx call d,\[user_exception_dispatcher_ptr\] 0006 90 nop 0007 51 push ecx 0008 53 push ebx 0009 e8xxxxxxxx call RtlDispatchException 000e 0ac0 or al, al 0010 740c je .001e 0012 5b pop ebx 0013 59 pop ecx 0014 6a00 push 0 0016 51 push ecx 0017 e8xxxxxxxx call NtContinue 001c eb0b jmps .0029 001e 5b pop ebx 001f 59 pop ecx 0020 6a00 push 0 0022 51 push ecx 0023 53 push ebx 0024 e8xxxxxxxx call NtRaiseException 0029 83c4ec add esp, -0x14 002c 890424 mov \[esp], eax 002f c744240401000000 mov dword ptr \[esp+4], 1 0037 895c2408 mov \[esp+8], ebx 003b c744241000000000 mov \[esp+0x10], 0 0043 54 push esp 0044 e8xxxxxxxx call RtlRaiseException 0049 c20800 retn 8 |
Stack layout at the entry time of KiUserExceptionDispatcher | Stack layout at the entry time of VG_(win_sc_user_exception_dispatch) |
---|---|
ESP+0x00 EXCEPTION_RECORD* ExceptionRecord +0x04 CONTEXT* ContextRecord +0x08 EXCEPTION_RECORD +0x58 CONTEXT ... ... | ESP+0x00 return address (into VG_(catch_user_exception_dispatcher)) +0x04 real sizeof(CONTEXT) +0x08 return address (into original KiUserExceptionDispatcher) +0x0c EXCEPTION_RECORD* ExceptionRecord +0x10 CONTEXT* ContextRecord +0x14 EXCEPTION_RECORD +0x54 CONTEXT ... ... |
The function is exactly the same, except for one "cld" instruction at the start. For safety reasons, the hook call is patched after this instruction (in the end, it makes the hooking code simpler too).
Original ntdll!KiUserExceptionDispatcher | Hooked ntdll!KiUserExceptionDispatcher |
---|---|
0000 fc cld 0001 8b4c2404 mov ecx, \[esp+4] 0005 8b1c24 mov ebx, \[esp] 0008 51 push ecx 0009 53 push ebx 000a e8xxxxxxxx call RtlDispatchException ... ... ... | 0000 fc cld 0001 ff15xxxxxxxx call d,\[user_exception_dispatcher_ptr] 0007 90 nop 0008 51 push ecx 0009 53 push ebx 000a e8xxxxxxxx call RtlDispatchException ... ... ... |
TBD