This is the first part of the .NET Internals Cookbook series. For your convenience you can find other parts in the table of contents in Part 0 – Table of contents
Table of Contents
1. What happens when you throw something which does not inherit from System.Exception? Was it different in .NET 1?
First, you should ask „how to do that”? Beside C# there are other languages running on CLR. One of them is C++/CLI which is a managed version of C++ language. In C++ you can throw anything — integer, string, byte etc. If you try doing that in C++/CLI you are effectively throwing something which does not derive from System.Exception
.
The object you throw is wrapped in System.Runtime.CompilerServices.RuntimeWrappedException so you can still catch it with normal catch(Exception e) {}
block.
However, before .NET 2 it was different. The thrown object was not wrapped so you couldn’t catch it in this way. You had to use untyped version of catch block in the form of catch {}
. Because of that, you could see code like this:
1 2 3 4 5 6 7 8 9 10 11 12 |
try { // } catch(Exception e) { // } catch { // } |
After migrating to .NET 2 this code doesn’t compile anymore because the last catch clause will never be executed. You can either modify the code or restore old behavior using RuntimeCompatibilityAttribute.
2. How to swallow ThreadAbortException?
You can catch it easily with an exception block but if you don’t reset it it gets rethrown automatically. To reset it, you need to call Thread.ResetAbort() method.
This means that calling Thread.Abort()
guarantees nothing – the target thread can catch and swallow the exception, effectively stopping the thread from terminating.
But that’s not all. ThreadAbortException
cannot be thrown in finally
block which means that you have no guarantee that you even stop the thread from doing its current task. This pattern is pretty common with CER where the try
block is empty and everything happens in finally
.
So how does the Thread.Abort
works under the hood? There are few steps:
- Suspend OS thread.
- Set metadata bit indicating that abort was requested.
- Add APC to the queue.
- Resume the thread.
At this point the thread works again and when it gets to alertable state it executes the APC and in turn checks the flag. But what happens if the thread doesn’t get to the alertable state? In that case .NET can hijack the thread and modify its IP register directly. See this post for some more details.
Not to mention that Thread.Abort
is not available in .NET Core.
3. How to catch AccessViolationException and similar? How was it working in .NET 1?
You can use HandleProcessCorruptedStateExceptionsAttribute attribute to mark methods where you want to handle those exceptions. This is not a simple thing so you probably should avoid it but it is possible. See this article for more details.
Interesting thing is that this exception was not available before .NET 2. What’s more, in .NET 2 you could just catch it with catch
block. You can still restore old behavior by using legacyNullReferenceExceptionPolicy
and legacyCorruptedStateExceptionPolicy
attributes. Also, behavior depends on the compiling environment, not execution one so if you run older binary using latest CLR you still get older behavior.
4. Is it possible that finally block is not executed? Is it possible that only some of them are not executed?
For both parts the answer is YES.
First, if someone kills the application then finally
is not executed at all, but that is obvious. What is not so obvious is that it also happens when you terminate application by calling Environment.FailFast() method. But there are other cases when it is not executed — StackOverflowException
, unhandled exceptions etc.
The answer for the second part is even more tricky. Let’s grab this code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
void Method1() { try { Method2(); } finally { Console.WriteLine("Method 1"); } } void Method2() { try { Method3(); } finally { Console.WriteLine("Method 2"); } } void Method3(){ // Do something nasty } |
Is it possible that we see Method 1
but not Method 2
in the console? The answer is yes. In case of access violation exceptions the normal blocks are not executed. If you mark Method1
with HandleProcessCorruptedStateExceptionsAttribute you can get the described effect. See this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
using System; using System.Runtime.ExceptionServices; using System.Runtime.InteropServices; public class Program { public static void Main() { Method1(); } [HandleProcessCorruptedStateExceptionsAttribute] static void Method1() { try { Method2(); } finally { Console.WriteLine("Method 1"); } } static void Method2() { try { Method3(); } finally { Console.WriteLine("Method 2"); } } static void Method3() { try { Marshal.Copy(new byte[] { 42 }, 0, (IntPtr)1000, 1); } catch (AccessViolationException) { // Never happens! } } } |
The output is:
1 2 3 4 5 6 7 8 |
Unhandled Exception: System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt. at System.Runtime.InteropServices.Marshal.CopyToNative(Object source, Int32 startIndex, IntPtr destination, Int32 length) at System.Runtime.InteropServices.Marshal.Copy(Byte[] source, Int32 startIndex, IntPtr destination, Int32 length) at Program.Method3() in C:\Users\afish\source\repos\ConsoleApp1\Program.cs:line 41 at Program.Method2() in C:\Users\afish\source\repos\ConsoleApp1\Program.cs:line 29 at Program.Method1() in C:\Users\afish\source\repos\ConsoleApp1\Program.cs:line 17 at Program.Main() in C:\Users\afish\source\repos\ConsoleApp1\Program.cs:line 9 Method 1 |
5. What is fault?
There are 3 exception handling blocks in C#: try, catch and finally. IL supports additional one: fault. This block is executed always if there was an exception. You cannot use it directly from C#, however, there are tricks to get it emitted by the compiler.
Compile this code with ILasm to see the effect:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
// Metadata version: v4.0.30319 .assembly extern mscorlib { .publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4.. .ver 4:0:0:0 } .assembly 'bacccde7-828a-4616-94e8-da4332f65171' { .hash algorithm 0x00008004 .ver 0:0:0:0 } .module 'bacccde7-828a-4616-94e8-da4332f65171.dll' // MVID: {63E2991D-2F64-4FE2-8CF3-0A1E1F6A1FD1} .imagebase 0x10000000 .file alignment 0x00000200 .stackreserve 0x00100000 .subsystem 0x0003 // WINDOWS_CUI .corflags 0x00000001 // ILONLY // Image base: 0x01280000 // =============== CLASS MEMBERS DECLARATION =================== .class public auto ansi beforefieldinit Program extends [mscorlib]System.Object { .method public hidebysig static void Main() cil managed { // .entrypoint .maxstack 1 .language '{3F5162F8-07C6-11D3-9053-00C04FA302A1}', '{994B45C4-E6E9-11D2-903F-00C04FA302A1}', '{5A869D0B-6611-11D3-BD2A-0000F80849BD}' .line 6,6 : 2,3 '' IL_0000: nop .line 7,7 : 6,7 '' .try { IL_0001: nop .line 9,9 : 3,4 '' IL_0002: nop IL_0003: leave.s IL_0013 .line 9,9 : 11,12 '' } // end .try fault { IL_0005: nop .line 10,10 : 4,52 '' IL_0006: ldstr "This will not be executed." IL_000b: call void [mscorlib]System.Console::WriteLine(string) IL_0010: nop .line 11,11 : 3,4 '' IL_0011: nop IL_0012: endfinally .line 16707566,16707566 : 0,0 '' } // end handler IL_0013: nop .line 13,13 : 6,7 '' .try { IL_0014: nop .line 14,14 : 4,42 '' IL_0015: ldstr "Some exception" IL_001a: newobj instance void [mscorlib]System.Exception::.ctor(string) IL_001f: throw .line 15,15 : 11,12 '' } // end .try fault { IL_0020: nop .line 16,16 : 4,44 '' IL_0021: ldstr "You will see this." IL_0026: call void [mscorlib]System.Console::WriteLine(string) IL_002b: nop .line 17,17 : 3,4 '' IL_002c: nop IL_002d: endfinally } // end handler } // end of method Program::Main .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { // .maxstack 8 IL_0000: ldarg.0 IL_0001: call instance void [mscorlib]System.Object::.ctor() IL_0006: nop IL_0007: ret } // end of method Program::.ctor } // end of class Program // ============================================================= // |
6. What is an exception filter? Is it just a syntax sugar?
Instead of catching the exception and then checking it contents to see if we should handle it (and then rethrowing), we can check the exception before catching it. The difference is that exception filter is executed in the first pass of the two pass exception mechanism. See Part 2 — Handling and rethrowing exceptions in C# to see the differences when it comes to the debugging scenarios.
Take this code:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
using System; public class Program { public static void Main() { try { } catch(Exception e) when (e.Message == "Message"){ } } } |
If you decompile it you will get this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
.try { IL_0001: nop .line 9,9 : 3,4 '' IL_0002: nop IL_0003: leave.s IL_002e .line 16707566,16707566 : 0,0 '' } // end .try filter { IL_0005: isinst [mscorlib]System.Exception IL_000a: dup IL_000b: brtrue.s IL_0011 IL_000d: pop IL_000e: ldc.i4.0 IL_000f: br.s IL_0027 IL_0011: stloc.0 .line 10,10 : 22,51 '' IL_0012: ldloc.0 IL_0013: callvirt instance string [mscorlib]System.Exception::get_Message() IL_0018: ldstr "Message" IL_001d: call bool [mscorlib]System.String::op_Equality(string, string) IL_0022: stloc.1 .line 16707566,16707566 : 0,0 '' IL_0023: ldloc.1 IL_0024: ldc.i4.0 IL_0025: cgt.un IL_0027: endfilter .line 16707566,16707566 : 0,0 '' } // end filter { // handler IL_0029: pop .line 10,10 : 51,52 '' IL_002a: nop .line 11,11 : 3,4 '' IL_002b: nop IL_002c: leave.s IL_002e .line 12,12 : 2,3 '' } // end handler |
You can clearly see that it uses filter under the hood, not some logic inside the catch
handler.