Things behind exception handling in .NET

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

Recent, I…

It is really time escaping. It’s been three weeks since my last post, so I just realize that I have to get down to my space, and write something somehow tedious to keep tracking on what I did in the recent period.
 
Last week, I held up a training on ASP.NET Compilation and Deployment for our team, the experience of which made me become aware of that, it’s not easy to be a teacher. To make me understood is much more difficult than to make me understand. I don’t think my presentation is good enough, as the topic is boring and long, but the feedback is somehow good. After all, some team members benefited from my training^^
 
For CER.NET Support project, after it came to avaiabe to Product, most of time in my job, I served as more Support Engineer than a developer. Luckily, not so many issues found since then (of course, I’m confident of my push ^^), except some little one that Production Team complain about the Callstack parsing looks not good. With launching Windbg, and fast going through those minidumps, checking the output from symsrv, examing the regular expression implementations for CER Frontend, these little problems can be solved quickly.
 
For my new task, totally focusing on .NET support XML-based Stacks, as it were, a challenge, still in a investigation phase. The launch of this project can be casted back to the early of last year. Because of original implementation of writing out minidump by Vertical Production, and the shortcoming that Marshal.GetExceptionPointers() in .NET Framework when called in a try/catch block, and SetUnhandledExceptionFilter is ligated, Production Team used XML instead of .dmp to store crash information. Even though .dmp is alongside within CER report, but it’s nonetheless useless. This is completely depressed. When using XML, the information it inludes is too little for developers to use for debugging.
 
Last week, I posted a thread in MSDN forum, eagering to got the answer, but found nothing useful. Just got a word from NoBugz [a Microsoft MVP], "You are asking questions that probably only Chris Brumme can answer." Hmm~~Put the URL here in case anyone can help me to figure out: http://forums.msdn.microsoft.com/en-US/clr/thread/321f8960-ccbb-49d0-b285-3d2bbf3d20d9
 
After some discussion with Stephen, Seema, and Michael, in this phase of the project, we will keep on using XML, but we need to push out to Product with the Bucketing, De-Obfuscation functionalities and so forth. And what’s most important is to standardize the format of XML-based Stacks. I have to communicate with Vault Team, ProductStream Team, Topobase Team, and Impression Team (.NET Production) to conform/agree to this standard. Next week, I will prepare for the initial draft of Tech Spec, so as to deliver to Production Team ASAP and then get the feedback as much as possible.

URL of newer articles by Matt Pietrek has been updated

For more information, please refer to the last post. With the help of Matt, those newer stuffs come to be available now. Thanks, Matt^^
 
Content
    • x64 Primer: Everything You Need To Know To Start Programming 64-Bit Windows Systems
    • { End Bracket }: Joining the Team
    • Threading: Break Free of Code Deadlocks in Critical Sections Under Windows
      Windows Server 2003: Discover Improved System Info, New Kernel, Debugging, Security, and UI APIs
    • Under the Hood: Link-time Code Generation
    • Inside Windows: An In-Depth Look into the Win32 Portable Executable File Format, Part 2
    • Under the Hood: Improved Error Reporting with DBGHELP 5.1 APIs
    • Inside Windows: An In-Depth Look into the Win32 Portable Executable File Format
    • Under the Hood: The .NET Profiling API and the DNProfiler Tool
    • Under the Hood: TypeRefViewer Utility Shows TypeRefs and MemberRefs in One Convenient GUI
    • Under the Hood: New Vectored Exception Handling in Windows XP
    • Under the Hood: IA-64 Registers, Part 2
    • Under the Hood: IA-64 Registers
    • Under the Hood: Displaying Metadata in .NET EXEs with MetaViewer
    • Under the Hood: Reduce EXE and DLL Size with LIBCTINY.LIB
    • Under the Hood: Programming for 64-bit Windows
    • Avoiding DLL Hell: Introducing Application Metadata in the Microsoft .NET Framework
    • Under the Hood: A Tale of Real-world Debugging
    • Under the Hood: Happy 10th Anniversary, Windows
    • Under the Hood: Optimizing DLL Load Time Performance
Hope this can help,
Austin
 
Matt Pietrek has co-written several books on Windows system-level programming as well as the Under the Hood column for MSDN Magazine. Previously he was a primary architect for the NuMega/Compuware BoundsChecker series of products. He now works on the Visual Studio team at Microsoft.

I got sound from Matt Pietrek, amazing~~~

Yes, Matt Pietrek, whom your guys should know (if you know the NuMega Labs of Compuware Corporation, where BoundsChecker comes from. Now, he is working for Microsoft). Out of question, he’s my idol. ^^ It’s amazing that I got email reply from him, though, I used to suspect if such a great-level master will take notice of a help just abut something trivial from me. But, I got sound from Matt Pietrek, amazing~~~
 
====================================================
(Don’t spread out Matt’s Email below.)
 
Austin,
I’ve emailed someone I used to know at MSDN magazine. I myself have no idea why some of my old content is online, while other, newer stuff isn’t.
If I found out anything , I’ll let you know.
 
Matt
====================================================
From: Austin Dai [mailto:daikof622@msn.com]
Sent: Tuesday, July 01, 2008 6:35 AM
To: MSDNMag – Under the Hood
Subject: Could you please help…?
 
Hi Matt,
 
I’m one of your MSDN magazine reader from Shanghai, China. Your articles are always so great that help to wade me through those tough programming technologies with those under the hood explaination. Particularly, your aticles on PE and SEH do help me lot.
 
Recently, I am studying deeply into .NET CLR, and find a great aticle on Microsoft’s web site, title of which is "Avoiding DLL Hell: Introducing Application Metadata in the Microsoft .NET Framework". But unluckily, Microsoft’s web site just provides a title without a valid link, and I tried my best to search with google/yahoo/baidu, still failed with an out-of-date web link.
 
So, could you please help to get a copy of this article to me? I will appreciate ^_^
 
Hope this Email address can still make me contact with you.
Thanks in advance,
austin.dai@autodesk.com