This post will discuss the following topics:
- How try/catch semantic is reached in .NET?
- How Application.ThreadException Event is dispatched?
- How AppDomain.UnhandledException Event is dispatched?
1. try/catch
In .NET, exception handling is really easier than its counterpart in antique C++. You don’t need to know more in detail about the under the hood to code your application as strength capability of exception handling. Microsoft, as usual, makes .NET hide a lot of details behind. But, sometimes, we really need to know the underneath to find the reason, "Why we can’t reach our goal when coding like this?". Especially for my question in last post that I come up with recently. OK, write a simple Windows Form application to throw an exception surrounded in a try/catch, in a button click event handler, launch windbg and look into it…
[For .NET application, we should use (sxe -c "" clrn) to get notified when CLR is loaded, then sos.dll (Son Of Strike)]
Use "!name2ee WinForm!WinForm.Form1.btnCLRExcept_Click" to get MethodDesc address of Method and then "!dumpil 00315a10" to get MSIL of the function. You will see try/catch is implemented by .try/.catch MSIL directives.
ilAddr = 00d22288
IL_0000: nop
.try
{
IL_0001: nop
IL_0002: ldarg.0
IL_0003: call WinForm.Form1::test // test will throw a NullReferenceException.
IL_0008: nop
IL_0009: nop
IL_000a: leave.s IL_001c
} // end .try
.catch
{
IL_000c: pop
IL_000d: nop
IL_000e: ldstr "asdfasfasfss"
IL_0013: call System.Windows.Forms.MessageBox::Show
IL_0018: pop
IL_0019: nop
IL_001a: leave.s IL_001c
} // end .catch
IL_001c: nop
IL_001d: ret
Then use "!U 00315a10" to get JITed native code.
Normal JIT generated code
WinForm.Form1.btnCLRExcept_Click(System.Object, System.EventArgs)
Begin 003903c0, size 53
003903c0 55 push ebp
003903c1 8bec mov ebp,esp
003903c3 57 push edi
003903c4 56 push esi
003903c5 53 push ebx
003903c6 83ec1c sub esp,1Ch
003903c9 33c0 xor eax,eax
003903cb 8945e8 mov dword ptr [ebp-18h],eax
003903ce 894dd8 mov dword ptr [ebp-28h],ecx
003903d1 8955dc mov dword ptr [ebp-24h],edx
003903d4 833d082e310000 cmp dword ptr ds:[312E08h],0
003903db 7405 je WinForm!WinForm.Form1.btnCLRExcept_Click(System.Object, System.EventArgs)+0x22 (003903e2)
003903dd e8657fd979 call mscorwks!JIT_DbgIsJustMyCode (7a128347)
003903e2 90 nop
003903e3 90 nop
003903e4 8b4dd8 mov ecx,dword ptr [ebp-28h]
003903e7 e814bdf8ff call WinForm.Form1.test() (0031c100)
003903ec 90 nop
003903ed 90 nop
003903ee 90 nop
003903ef eb16 jmp WinForm!WinForm.Form1.btnCLRExcept_Click(System.Object, System.EventArgs)+0x47 (00390407)
003903f1 90 nop
003903f2 90 nop
003903f3 8b0ddc6beb02 mov ecx,dword ptr ds:[2EB6BDCh] ("asdfasfasfss")
003903f9 e83a55ef7a call System_Windows_Forms_ni!System.Windows.Forms.MessageBox.Show(System.String) (7b285938)
003903fe 90 nop
003903ff 90 nop
00390400 e8b08db779 call mscorwks!JIT_EndCatch (79f091b5)
00390405 eb00 jmp WinForm!WinForm.Form1.btnCLRExcept_Click(System.Object, System.EventArgs)+0x47 (00390407)
00390407 90 nop
00390408 90 nop
00390409 8d65f4 lea esp,[ebp-0Ch]
0039040c 5b pop ebx
0039040d 5e pop esi
0039040e 5f pop edi
0039040f 5d pop ebp
00390410 c20400 ret 4
Apparently, .NET doesn’t use stack-frame-based exception handling mechanism. You will not see the code populating on FS:[0] chain. Set breakpoints at ntdll!KiUserExceptionDispatcher (the entry point from kernel mode to user mode, where exception dispatching starts to work), then click the button to throw out the exception. Use "!exchain" to list the exception handlers registered on the thread where exception originated from.
0:000> !exchain
0022ecf8: mscorwks!_except_handler4+0 (79fc15dc)
0022edc0: mscorwks!GetManagedNameForTypeInfo+1cce3 (79f0a66c)
0022efdc: mscorwks!FastNExportExceptHandler+0 (7a095373)
0022f07c: USER32!_except_handler4+0 (76cc51ba)
0022f0e0: USER32!_except_handler4+0 (76cc51ba)
0022f2cc: mscorwks!COMPlusFrameHandler+0 (79f07fee)
0022f320: mscorwks!_except_handler4+0 (79fc15dc)
0022f5f8: mscorwks!_except_handler4+0 (79fc15dc)
0022f864: mscorwks!GetManagedNameForTypeInfo+97f0 (7a3237f8)
0022fd34: mscorwks!GetManagedNameForTypeInfo+7476 (7a320496)
0022fd80: mscorwks!_except_handler4+0 (79fc15dc)
0022fdcc: mscorwks!GetManagedNameForTypeInfo+acc (7a316288)
0022fe24: ntdll!_except_handler4+0 (76e69834)
Then set breakpoints for these exception handlers. Keep an eye on mscorwks!COMPlusFrameHandler (This is the right place that implements try/catch semantics), it will accept to handle the exception we throw. As we all know, there will be two passes over this exception handler chain: the first pass for searching for exception handler that will process the exception, and the second for unwinding (Stacks below the frames, where exception handler found in first pass locates in, will be removed, that’s so-call stack unwinding, usually doing clean up work in that phase). Exception handlers in this chain usually own the same code processing logical, which can be divided into two part: filter expression, and exception handler. They use filter expression to give out feedbacks to OS during the first pass, if they will handle the exception, and use the handler block after the second pass where catch block will execute. Note: stack unwinding is triggered proactively by exception handler at the end of the first pass. This is true for both C++ and .NET CLR’s SEH implementation. For details, please refer to Matt Pietrek: A Crash Course on the Depths of Win32™ Structured Exception Handling, _except_handler3 Pseudocode.
After tracing the order of function calls several times, eventually, we will find catch block will be called during the second pass (by inferring), and after stack walking (CLR will walk the stack to collection information, that’s where Exception.StackTrace property pick up the value). For details, please refer to the article above, RtlUnwind Pseudocode. When stopping in the entry of RtlUnwind, the callstack is as below:
0:000> k
ChildEBP RetAddr
0022e6e4 79f0891f ntdll!RtlUnwind
0022e708 79f085cc mscorwks!CallRtlUnwind+0x18
0022e824 79f081d6 mscorwks!CPFH_RealFirstPassHandler+0x50c
0022e864 79f080a7 mscorwks!CPFH_RealFirstPassHandler+0x68c
0022e888 76eb9b99 mscorwks!COMPlusFrameHandler+0x15a
0022e8ac 76eb9b6b ntdll!ExecuteHandler2+0x26
0022e95c 76eb99f7 ntdll!ExecuteHandler+0x24
0022e95c 76bc42eb ntdll!KiUserExceptionDispatcher+0xf
After stack unwinding, execution control will be turned back to mscorwks!COMPlusFrameHandler, and catch block will execute. Here, I just want to point out two interesting API, that accomplish the final work of unwinding: NtContinue and RtlpCaptureContext. RtlpCaptureContext will get the context of the stack frame above the current one, where RtlpCaptureContext is called, while NtContinue will do the drity job, setting CPU context, and start to execute at the IP indicated by the returned context of RtlpCaptureContext. So, NtContinue will not return to RtlUnwind (skip it), but to mscorwks!CallRtlUnwind. Function CallRtlUnwind doesn’t do many thing, it will restore some register, eax, edi, esi, ebx, then return execution control to mscorwks!CPFH_RealFirstPassHandler+0x50c, which will call another insteresting function mscorwks!COMPlusAfterUnwind, and further call mscorwks!UnwindFrames, where managed stack walking begins:
mscorwks!UnwindFrames:
79f07d04 55 push ebp
79f07d05 8bec mov ebp,esp
79f07d07 56 push esi
79f07d08 57 push edi
79f07d09 a174d43a7a mov eax,dword ptr [mscorwks!PerfCounters::m_pPrivatePerf (7a3ad474)]
79f07d0e 83a0c400000000 and dword ptr [eax+0C4h],0
79f07d15 8b7d08 mov edi,dword ptr [ebp+8]
79f07d18 8db778010000 lea esi,[edi+178h]
79f07d1e 8bce mov ecx,esi
79f07d20 e8fbf7ffff call mscorwks!ThreadExceptionState::IsExceptionInProgress (79f07520)
79f07d25 85c0 test eax,eax
79f07d27 740a je mscorwks!UnwindFrames+0x2f (79f07d33)
79f07d29 8bce mov ecx,esi
79f07d2b e8f9f7ffff call mscorwks!ThreadExceptionState::GetFlags (79f07529)
79f07d30 830804 or dword ptr [eax],4
79f07d33 e8f89bf6ff call mscorwks!CORDebuggerAttached (79e71930)
79f07d38 84c0 test al,al
79f07d3a 0f851e680f00 jne mscorwks!UnwindFrames+0x38 (79ffe55e)
79f07d40 8b450c mov eax,dword ptr [ebp+0Ch]
79f07d43 ff7028 push dword ptr [eax+28h]
79f07d46 33c9 xor ecx,ecx
79f07d48 3908 cmp dword ptr [eax],ecx
79f07d4a 0f95c1 setne cl
79f07d4d 83c904 or ecx,4
79f07d50 51 push ecx
79f07d51 50 push eax
79f07d52 68647df079 push offset mscorwks!COMPlusUnwindCallback (79f07d64)
79f07d57 8bcf mov ecx,edi
79f07d59 e809cef7ff call mscorwks!Thread::StackWalkFrames (79e84b67)
79f07d5e 5f pop edi
79f07d5f 5e pop esi
79f07d60 5d pop ebp
79f07d61 c20800 ret 8
You will be confusing, why it unwinds again just at the end of the stack unwinding? In fact, this time, it’s for managed stack unwinding. mscorwks!UnwindFrames will call mscorwks!Thread::StackWalkFrames and set a callback function mscorwks!COMPlusUnwindCallback as its parameter. So, every time a stack frame is enumerated, COMPlusUnwindCallback will be called. Actually, mscorwks!Thread::StackWalkFrames will further call mscorwks!Thread::StackWalkFramesEx to do the real job:
0022e130 79e84b26 mscorwks!COMPlusUnwindCallback
0022e144 79e84962 mscorwks!Thread::MakeStackwalkerCallback+0x15
0022e32c 79e84bf2 mscorwks!Thread::StackWalkFramesEx+0x396
0022e65c 79f07d5e mscorwks!Thread::StackWalkFrames+0xb8
0022e67c 79f089cc mscorwks!UnwindFrames+0x62
0022e70c 79f085db mscorwks!COMPlusAfterUnwind+0x97
0022e824 79f081d6 mscorwks!CPFH_RealFirstPassHandler+0x51b
0022e864 79f080a7 mscorwks!CPFH_RealFirstPassHandler+0x68c
0022e888 76eb9b99 mscorwks!COMPlusFrameHandler+0x15a
0022e8ac 76eb9b6b ntdll!ExecuteHandler2+0x26
0022e95c 76eb99f7 ntdll!ExecuteHandler+0x24
0022e95c 76bc42eb ntdll!KiUserExceptionDispatcher+0xf
In function mscorwks!COMPlusUnwindCallback, we can see a lot of function calls that are collecting the frame information, that’s where value Exception.StackTrace picked from. (And if you are careful, you should realize that’s the reason of a default behavior for CLR exception stack trace. Only stacks between where the exception is thrown and where it is caught, are collected.) The following stack frames will be unwound during this phase:
0022e064 79f08df5 mscorwks!EEJitManager::ResumeAtJitEH+0x12
0022e170 79e84b26 mscorwks!COMPlusUnwindCallback+0x7c3
0022e184 79e84962 mscorwks!Thread::MakeStackwalkerCallback+0x15
0022e368 79e84bf2 mscorwks!Thread::StackWalkFramesEx+0x396
0022e698 79f07d5e mscorwks!Thread::StackWalkFrames+0xb8
0022e6b8 79f089cc mscorwks!UnwindFrames+0x62
0022e748 79f085db mscorwks!COMPlusAfterUnwind+0x97
0022e860 79f081d6 mscorwks!CPFH_RealFirstPassHandler+0x51b
0022e8a0 79f080a7 mscorwks!CPFH_RealFirstPassHandler+0x68c
0022e8c4 76eb9b99 mscorwks!COMPlusFrameHandler+0x15a
0022e8e8 76eb9b6b ntdll!ExecuteHandler2+0x26
0022e998 76eb99f7 ntdll!ExecuteHandler+0x24
0022e998 76bc42eb ntdll!KiUserExceptionDispatcher+0xf
0022ece4 79f071ac KERNEL32!RaiseException+0x58
0022ed44 79f0a629 mscorwks!RaiseTheExceptionInternalOnly+0x2a8
0022ee08 0039045d mscorwks!JIT_Throw+0xfc
0022ee44 003903ec WinForm!WinForm.Form1.test()+0x35
At the end of Stack walking, COMPlusUnwindCallback will call mscorwks!EEJitManager::ResumeAtJitEH to return the control to the catch block:
mscorwks!MNativeJitManager::ResumeAtJitEH:
79f08e90 55 push ebp
79f08e91 8bec mov ebp,esp
79f08e93 56 push esi
79f08e94 8b7508 mov esi,dword ptr [ebp+8]
79f08e97 8b8620010000 mov eax,dword ptr [esi+120h]
79f08e9d 8b11 mov edx,dword ptr [ecx]
79f08e9f 6a00 push 0
79f08ea1 50 push eax
79f08ea2 ff521c call dword ptr [edx+1Ch] ds:0023:79fc79ec={mscorwks!EEJitManager::JitTokenToStartAddress (79ef37b4)}
79f08ea5 ff7518 push dword ptr [ebp+18h]
79f08ea8 ff7514 push dword ptr [ebp+14h]
79f08eab ff7510 push dword ptr [ebp+10h]
79f08eae ff750c push dword ptr [ebp+0Ch]
79f08eb1 50 push eax
79f08eb2 56 push esi
79f08eb3 e805000000 call mscorwks!ResumeAtJitEH (79f08ebd)
79f08eb8 5e pop esi
79f08eb9 5d pop ebp
79f08eba c21400 ret 14h
Before calling a global function mscorwks!ResumeAtJitEH, CLR will call JitTokenToStartAddress to get the address of catch block. It’s apparently using Metadata Token. Then mscorwks!ResumeAtJitEHHelper will use a JMP instruction to return the control to catch block:
mscorwks!ResumeAtJitEHHelper:
79f090c7 8b542404 mov edx,dword ptr [esp+4] ss:0023:0022df9c=0022dfc4
79f090cb 8b02 mov eax,dword ptr [edx]
79f090cd 8b5a04 mov ebx,dword ptr [edx+4]
79f090d0 8b7210 mov esi,dword ptr [edx+10h]
79f090d3 8b7a14 mov edi,dword ptr [edx+14h]
79f090d6 8b6a18 mov ebp,dword ptr [edx+18h]
79f090d9 8b621c mov esp,dword ptr [edx+1Ch]
79f090dc ff6220 jmp dword ptr [edx+20h]
In the prologue of mscorwks!ResumeAtJitEHHelper, it just set the register context, [edx+20h] contains the target address 003903f1, which located in body of WinForm!WinForm.Form1.btnCLRExcept_Click. So application continues to execute start from catch block, and just like nothing happened before.
Conclusion:
The details of CLR Exception is more complex than I’ve described above. For example, SEH Exception mapping. But to make the macroscopic workflow clear is already invaluable. Unfortunately, I still cannot figure out the question I post in the MSDN forum. Maybe I need to carefully check the assembler at another time or more readable source code of SSCLI. Hm~~ CLR gives perquisite to CLR Exception.
2. Application.ThreadException Event
It’s actually a try/catch around the message loop logical in Application.Run, when an exception is raised, OnThread will be called in catch to start calling delegate chain registered on ThreadException Event. This can be got easily with using Reflection tool. It’s not unhandled exception when this event is dispatched.
3. AppDomain.UnhandledException Event
This is also intuitive, it is implemented by means of traditional UnhandledExceptionFilter. Set breakpoint at the handler of this event, and when it is hit, see the callstack.
That’s all for today, I’ve been a little bit OT^^ Home…
Thanks Stephen for his time. We take a long time to go through those sick assembler code.
Austin