ExceptionHandling
Table of Contents
- 1 Introduction
- 2 Example Assembly
- 3 Compile, Disassemble and Execute the Assembly
- 4 MonoContext struct
- 5 Code Generation for ‘throw’
- 6 Throwing Exceptions from Managed Code
- 7 Fault Recovery and Restoring Context
- 8 Code Generation for Try-Catch-Finally blocks
- 9 Exception Handler Internals
- 10 Call Filter
- 11 References
1 Introduction
(This an Initial Draft)
In this article, I attempt to explore how Exception Handling (EH for
convenience) is implemented in the Mono Runtime. This I do by
considering a simple example assembly. As is customary, the
exploration would be limited to only those aspects of Exception
Handling that kicks in for the example under consideration.
Specifically we will be examining -
-
Native code generation for
- try-catch and try-finally blocks SEH blocks 1
- throw CIL instruction
-
Exception Handling
- Fault Handler Identification
- Fault Handling and Recovery
2 Example Assembly
Consider the example down below.
The method Test.ThrowException () unconditionally throws an
exception.
The method Test.Main () invokes ThrowException () within a protected
block. The protected block has a catch handler that swallows the
thrown exception and a finally handler that waits on user input
before exiting.
The property Test.Path and enum Test.MARKER are defined more for
convenience. The specific convenience they offer is -
-
The enum values of Test.Marker are ‘magical’. These magical
values would help us easily identify try, catch, finally blocks
in the generated native code. - The property Test.Path traces and logs the control flow.
-
Last but not the least, they help us have non-empty
try-catch-finally blocks.// file ExceptionHandling.cs using System; class Test { enum MARKER { TRY_CATCH_FINALLY_BEGIN = 0x1111, TRY_BEGIN = 0x2222, TRY_END = 0x3333, CATCH_BEGIN = 0x4444, CATCH_END = 0x5555, FINALLY_BEGIN = 0x6666, FINALLY_END = 0x7777, TRY_CATCH_FINALLY_END = 0x8888 } private static MARKER Path { set { Console.WriteLine (value); } } private static void ThrowException () { throw new Exception (""); } static void Main() { Path = MARKER.TRY_CATCH_FINALLY_BEGIN; try { Path = MARKER.TRY_BEGIN; ThrowException (); Path = MARKER.TRY_END; } catch (Exception e) { Path = MARKER.CATCH_BEGIN; Path = MARKER.CATCH_END; } finally { Path = MARKER.FINALLY_BEGIN; Path = MARKER.FINALLY_END; Console.ReadLine (); } Path = MARKER.TRY_CATCH_FINALLY_END; } }
3 Compile, Disassemble and Execute the Assembly
$ mcs ExceptionHandling.cs ExceptionHandling.cs(43): warning CS0162: Unreachable code detected ExceptionHandling.cs(38): warning CS0168: The variable `e' is declared but never used
The dump down below shows the disassembly of methods -
ThrowException () and Main () in the generated CIL executable.
Pay specific attention to the highlighted portions.
$ ildasm ExceptionHandling.exe
// Disassembly of Test.ThrowException () and Test.Main ()
.method private static hidebysig void ThrowException() cil managed
{
// Start of method header: 2104
.maxstack 8
?L2105:
ldsfld class System.String [mscorlib]System.String::Empty
newobj instance void [mscorlib]System.Exception::.ctor(class System.String)
throw
}
.method private static hidebysig void Main() cil managed
{
// Start of method header: 2110
.entrypoint
.maxstack 7
.locals init (class [mscorlib]System.Exception)
?L211c:
ldc.i4 4369
call void Test::set_Path(valuetype Test/MARKER)
--------------------+
?L2126: -------+ |
ldc.i4 8738 | |
call void Test::set_Path(valuetype Test/MARKER) | |
call void Test::ThrowException() | |
ldc.i4 13107 | SEHBlock1 |
call void Test::set_Path(valuetype Test/MARKER) | Try-Catch |
leave ?L2179 | Block |
-------+ |
| |
?L2144: | |
stloc.0 | |
ldc.i4 17476 | |
call void Test::set_Path(valuetype Test/MARKER) | | SEHBlock2
ldc.i4 21845 | |
call void Test::set_Path(valuetype Test/MARKER) | | Try-Finally
leave ?L2179 -------+ | Block
|
--------------------+
|
?L215e: |
ldc.i4 26214 |
call void Test::set_Path(valuetype Test/MARKER) |
ldc.i4 30583 |
call void Test::set_Path(valuetype Test/MARKER) |
call class System.String [mscorlib]System.Console: ReadLine() |
pop |
endfinally |
----------------------
?L2179:
ldc.i4 34952
call void Test::set_Path(valuetype Test/MARKER)
ret
.try ?L2126 to ?L2144 catch [mscorlib]System.Exception handler ?L2144 to ?L215e
.try ?L2126 to ?L215e finally handler ?L215e to ?L2179
}
$ mono ExceptionHandling.exe TRY_CATCH_FINALLY_BEGIN TRY_BEGIN CATCH_BEGIN CATCH_END FINALLY_BEGIN FINALLY_END TRY_CATCH_FINALLY_END
4 MonoContext struct
Before we proceed ahead let’s see what a MonoContext structure is.
typedef struct {
guint32 eax;
guint32 ebx;
guint32 ecx;
guint32 edx;
guint32 ebp;
guint32 esp;
guint32 esi;
guint32 edi;
guint32 eip;
} MonoContext;
There is nothing surprising in the definition here. It merely
represents the machine state at a given point in time.
5 Code Generation for ‘throw’
Let’s now examine the native code generated by the JIT compiler for
ThrowException ().
In mono_method_to_ir (), CEE_THROW is mapped in to OP_THROW and
OP_NOT_REACHED intermediate sequence.
In mono_arch_output_basic_block (), OP_THROW is translated into a push
of the exception object and a native call to
mono_arch_throw_exception ().
Native code for ThrowException ()
@ 0xb797b010 - @ 0xb797b038:
push ebp
mov ebp,esp
sub esp,0x8
mov eax,0x28fe0
sub esp,0x8
push 0x2bcd0
push 0x200002d
call 0x80b6cb0 // mono_create_corlib_exception_1 ()
add esp,0x10
----+
push eax // push exception object | 'throw'
call 0xb7bbccf8 // mono_arch_throw_exception () |
----+
leave
ret
6 Throwing Exceptions from Managed Code
In the previous section we saw that a ‘throw’ in CIL is mapped to a
native method mono_arch_throw_exception ().
mono_arch_throw_exception () is an icall and maps to following code
sequence.
The routine merely saves the context information on to the stack and
invokes throw_exception (). Note that the EIP of the dumped context
is within the scope of the function that threw the exception.
It is the throw_exception () that kicks off the exception handling
path.
Native code for mono_arch_throw_exception () @ 0xb7bbccf8 - 0xb7bbcd0: push esp push DWORD PTR [esp+4] // point of throw push DWORD PTR [esp+12] // exception object push ebp push edi push esi push ebx push edx push ecx push eax call 0x8107e00 // throw_exception () int3
// Snippet code from throw_exception ()
static void
throw_exception (unsigned long eax, unsigned long ecx, unsigned long edx,
unsigned long ebx, unsigned long esi, unsigned long edi,
unsigned long ebp, MonoObject *exc, unsigned long eip,
unsigned long esp, gboolean rethrow)
{
static void (*restore_context) (MonoContext *);
MonoContext ctx;
/* Pop argument and return address */
ctx.esp = esp + (2 * sizeof (gpointer));
ctx.eip = eip;
ctx.ebp = ebp;
ctx.edi = edi;
ctx.esi = esi;
ctx.ebx = ebx;
ctx.edx = edx;
ctx.ecx = ecx;
ctx.eax = eax;
if (mono_object_isinst (exc, mono_defaults.exception_class)) {
MonoException *mono_ex = (MonoException*)exc;
if (!rethrow)
mono_ex->stack_trace = NULL;
}
/* adjust eip so that it point into the call instruction */
ctx.eip -= 1;
mono_handle_exception (&ctx, exc, (gpointer)eip, FALSE);
restore_context = mono_arch_get_restore_context ();:
restore_context (&ctx); // mono_arch_get_restore_context ()
g_assert_not_reached ();
}
throw_exception () essentially does the following -
-
Marshals a MonoContext struct that is representative of the
machine state at the point of exception. It then kicks off
exception handling via mono_handle_exception ().Shortly we will examine handling of exceptions in detail. For now
it is sufficient to remark that the context struct is both an
‘in’ and an ‘out’ param of mono_handle_exception (). In essence
the exception handling mechanism examines the current machine
state and also advises a machine state for resumption of normal
control. -
Restores the execution context to that advised by the EH
mechanism and transfers control to the new EIP address.
7 Fault Recovery and Restoring Context
Native code for mono_arch_get_restore_context () @0xb7bbcc38 - @0xb7bbcc50: 0xb7bbcc38: mov eax,DWORD PTR [esp+4] // eax = ctx 0xb7bbcc3c: mov edx,DWORD PTR [eax+32] // edx = ctx->eip 0xb7bbcc3f: mov ebx,DWORD PTR [eax+4] // ebx = ctx->ebx 0xb7bbcc42: mov edi,DWORD PTR [eax+28] // edi = ctx->edi 0xb7bbcc45: mov esi,DWORD PTR [eax+24] // esi = ctx->esi 0xb7bbcc48: mov esp,DWORD PTR [eax+20] // esp = ctx->esp 0xb7bbcc4b: mov ebp,DWORD PTR [eax+16] // ebp = ctx->ebp 0xb7bbcc4e: jmp edx // jump to ctx->eip
Note that the registers eax and edx aren’t restored.
8 Code Generation for Try-Catch-Finally blocks
Before we proceed to look at specifics of exception handling
mechanism, let’s look at the native code generated for
Try-Catch-Finally block in Main ().
In method_to_ir (), a ‘leave’ instruction is translated in to zero
or more OP_CALL_HANDLER followed by an OP_BR. OP_CALL_HANDLER
arranges for a call to into the ‘finally’ block while OP_BR arranges
for control to flow out of the current SEHBlock.
In case of ‘leave’ instruction that occur within a catch handler,
prior to arranging for calling of ‘finally’ handlers as described
above the following special case handling for ThreadAbortException
is done – If the running thread has a ThreadAbortException pending,
it is rethrown immediately 2.
Furthermore, catch (and filter handlers) require an exception object
be passed in the event of an exception. These are passed via
specially allocated variables on the method stack.
Native code for Main ()
@0xb796e280 - @0xb796e35d:
0xb796e280: push ebp
0xb796e281: mov ebp,esp
0xb796e283: sub esp,0x28
0xb796e286: mov DWORD PTR [ebp-24],0x0
0xb796e28d: sub esp,0xc
0xb796e290: push 0x1111
0xb796e295: call 0xb796e388
0xb796e29a: add esp,0x10
0xb796e29d: sub esp,0xc <------+
0xb796e2a0: push 0x2222 |
0xb796e2a5: call 0xb796e388 |
0xb796e2aa: add esp,0x10 |
|
0xb796e2ad: call 0xb797b010 // ThrowException () |
|
| C# try block
0xb796e2b2: sub esp,0xc |
0xb796e2b5: push 0x3333 |
0xb796e2ba: call 0xb796e360 |
0xb796e2bf: add esp,0x10 |
|
|
0xb796e2c2: sub esp,0xc |
0xb796e2c5: call 0xb796e31f // call finally block |
0xb796e2ca: add esp,0xc |
0xb796e2cd: jmp 0xb796e34b <------+
<------+
0xb796e2d2: mov eax,DWORD PTR [ebp-20] // eax = exception |
0xb796e2d5: mov eax,DWORD PTR [ebp-20] |
0xb796e2d8: mov DWORD PTR [ebp-24],eax |
|
0xb796e2db: sub esp,0xc |
0xb796e2de: push 0x4444 |
0xb796e2e3: call 0xb796e388 |
0xb796e2e8: add esp,0x10 |
|
0xb796e2eb: sub esp,0xc | C# catch block
0xb796e2ee: push 0x5555 |
0xb796e2f3: call 0xb796e388 |
0xb796e2f8: add esp,0x10 |
|
0xb796e2fb: call 0xb797b038 // __icall_wrapper_mono_thread_ge|_undeniable_exception
0xb796e300: mov DWORD PTR [ebp-28],eax |
0xb796e303: cmp DWORD PTR [ebp-28],0x0 |
0xb796e307: je 0xb796e312 |
0xb796e309: mov eax,DWORD PTR [ebp-28] |
0xb796e30c: push eax |
0xb796e30d: call 0xb7bbccf8 // get_throw_exception () |
0xb796e312: sub esp,0xc |
0xb796e315: call 0xb796e31f // call finally block |
0xb796e31a: add esp,0xc |
0xb796e31d: jmp 0xb796e34b <------+
<------+
0xb796e31f: mov DWORD PTR [ebp-4],esp |
|
0xb796e322: sub esp,0xc |
0xb796e325: push 0x6666 |
0xb796e32a: call 0xb796e388 |
0xb796e32f: add esp,0x10 |
| finally block
0xb796e332: sub esp,0xc |
0xb796e335: push 0x7777 |
0xb796e33a: call 0xb796e388 |
0xb796e33f: add esp,0x10 |
|
0xb796e342: call 0xb797b098 // Console.ReadLine |
0xb796e347: mov esp,DWORD PTR [ebp-4] |
0xb796e34a: ret <-------+
<--------+
0xb796e34b: sub esp,0xc |
0xb796e34e: push 0x8888 | Block 'following' the try block
0xb796e353: call 0xb796e360 |
0xb796e358: add esp,0x10 <--------+
0xb796e35b: leave
0xb796e35c: ret
9 Exception Handler Internals
Exception Handling is done in following passes -
-
Handler Identification Pass – In this pass, the runtime walks the
stack and tries to identify a block that is willing to handle the
exception. -
Handler Invocation Pass – In this pass the runtime invokes fault
and finally handlers on all blocks that intervene between the
point at which the exception is thrown and the point at which the
exception is handled. Subsequently it transfers control to the
handler identified in part 1.
For the sake of completion, the pseudocode for Handler
Identification and Handler Invocation Passes is given below.
Before one proceeds to examine the pseudo code, it needs to be borne
in mind that the entries in a Method’s Exception Handler Table are
stored in such as way that a nested protected block always occurs
before any of it’s parent protected block.
The following specifics may be noted in the pseudo-code
- Stack is merely walked.
In the Handler Identification Pass,
-
The exception object is passed to the filtering code by storing
it in a pre-defined location on the current stack frame. -
The stack walk is terminated as soon as a catch or a filter
handler that is willing to handle the exception is identified. - Failure to locate a handler causes a FALSE to be returned.
In the Handler Invocation Pass,
-
The intervening finally and fault handlers are called via an
architecture specific stub – mono_arch_get_call_filter () -
Once the matching filter or catch handler is identified, the EIP
in ctx is adjusted to point to the handler address using the macroMONO_CONTEXT_SET_IP (ctx, ei->handler_start)
The exception object is passed to the catch handler by storing it
in a pre-defined location on the current stack frameAs was earlier mentioned, it is the responsibility of
throw_exception () to actually resume exception at the handler.
9.1 Handler Identification Pass
// Handler Identification Pass
// test_only == FALSE for Handler Invocation Pass
mono_handle_internal_excpetion (MonoContext *ctx, gpointer obj,
gpointer original_ip, gboolean test_only, gint32 * out_filter_idx)
MonoJitTlsData *jit_tls = TlsGetValue (mono_jit_tls_id)
MonoLMF *lmf = mono_get_lmf ()
*out_filter_idx = -1
filter_idx = 0
initial_ctx = *ctx
MonoJitInfo rji = {0}
call_filter = mono_arch_get_call_filter ();
while (1)
MonoContext new_ctx;
guint32 free_stack;
ji = mono_find_jit_info (domain, jit_tls, &rji, &rji, ctx,
&new_ctx, NULL, &lmf, NULL, NULL)
assert (ji)
-----------+
if (ji == -1) | Handler not found
return FALSE -----------+
if !(ji->code_start <= MONO_CONTEXT_GET_IP (ctx)
<= ji->code_start + ji->code_size)
// skip native frame
*ctx = new_ctx
continue;
++ frame_count
if (!ji->num_clauses)
// not within a protected block
*ctx = new_ctx
continue;
foreach MonoJitExceptionInfo *ei in &ji->clauses
filtered = FALSE
if (!(ei->try_start <= IP <= ei->try_end))
continue;
MonoClass *catch_class = get_exception_class (ei, ji, ctx)
-----------+
if (ei->flags == MONO_EXCEPTION_CLAUSE_NONE | FILTER) o | Pass Exception Object
(*MONO_CONTEXT_GET_BP (ctx) + ei->exvar_offset) = obj |
-----------+
if (ei->flags == MONO_EXCEPTION_CLAUSE_FILTER)
filtered = call_filter (ctx, ei->data.filter)
if (filtered && out_filter_idx)
*out_filter_idx = filter_idx;
++ filter_idx;
------------+
if (ei->flags == MONO_EXCEPTION_CLAUSE_NONE && | Handler Identified
mono_object_isinst (obj, catch_class)) || filtered |
return TRUE; ------------+
continue;
*ctx = new_ctx;
continue;
9.2 Handler Invocation Pass
// Handler Invocation Pass
// test_only == FALSE for Handler Invocation Pass
mono_handle_internal_excpetion (MonoContext *ctx, gpointer obj,
gpointer original_ip, gboolean test_only, gint32 * out_filter_idx)
MonoJitInfo rji;
MonoJitTlsData *jit_tls = TlsGetValue (mono_jit_tls_id)
MonoLMF *lmf = mono_get_lmf ();
MonoContext ctx_cp = *ctx ------------+
if (!mono_handle_exception_internal (&ctx_cp, obj, |
original_ip, TRUE, &first_filter_idx) | Unhandled Exception
mono_unhandled_exception (obj) ------------+
if (out_filter_idx)
*out_filter_idx = -1
filter_idx = 0
initial_ctx = *ctx
rji = {0}
while (1)
MonoContext new_ctx;
guint32 free_stack;
ji = mono_find_jit_info (domain, jit_tls, &rji, &rji, ctx, &new_ctx,
NULL, &lmf, NULL, NULL)
assert (ji) // Exception inside fucntion with unwind info
if ji == -1
*ctx = new_ctx
*(mono_get_lmf_addr ()) = lmf
jit_tls->abort_func (obj)
g_assert_not_reached ();
if ! (ji->code_start <= MONO_CONTEXT_GET_IP (ctx) <= ji->code_start + ji->code_size)
// exception in native code. We got back to managed code using LMF
*ctx = new_ctx
conitnue;
++ frame_count
if (!ji->num_clauses)
*ctx = new_ctx
continue;
foreach MonoJitExceptionInfo *ei in ji->clauses [i]
filtered = FALSE
if ! (ei->try_start <= MONO_CONTEXT_GET_IP (ctx) <= ei->try_end)
continue;
MonoClass *catch_class = get_exception_catch_class (ei, ji, ctx)
------------+
if (ei->flags == MONO_EXCEPTION_CLAUSE_NONE | FILTER) | Pass Exception Object
*(MONO_CONTEXT_GET_BP (ctx) + ei->exvar_offset) = obj |
------------+
if (ei->flags == MONO_EXCEPTION_CLAUSE_FILTER)
filtered = (filter_idx == first_filter_idx)
filter_idx ++
if (ei->flags == MONO_EXCEPTION_CLAUSE_NONE &&
mono_object_isinst (obj, catch_class) || filtered)
------------+
MONO_CONTEXT_SET_IP (ctx, ei->handler_start) | Update EIP for Fault Recovery
*(mono_get_lmf_addr ()) = lmf |
------------+
------------+
if (ei->flags == MONO_EXCEPTION_CLAUSE_FAULT) |
call_filter (ctx, ei->handler_start) |
| Call fault and finally handlers
if (ei->flags == MONO_EXCEPTION_CLAUSE_FINALLY) |
call_filter (ctx, ei->handler_start) |
------------+
continue;
*ctx = new_ctx
continue;
10 Call Filter
// Native code for mono_arch_get_call_filter () // @ 0xb7bbccb8 - 0xb7bbcd7c push ebp mov ebp,esp push ebx push edi push esi mov eax,DWORD PTR [ebp+8] // eax = ctx mov ecx,DWORD PTR [ebp+12] // ecx = exception handler push ebp // save ebp mov ebp,DWORD PTR [eax+16] // ebp = ctx->ebp mov ebx,DWORD PTR [eax+4] // ebx = ctx->ebx mov esi,DWORD PTR [eax+24] // esi = ctx->esi mov edi,DWORD PTR [eax+28] // edi = ctx->edi mov edx,esp and esp,0xfffffff0 // align esp sub esp,0x8 push edx // save original esp call ecx // call handler pop esp // restore esp pop ebp // restore ebp pop esi // restore esi pop edi // restore edi pop ebx // restore ebx leave ret
11 References
Footnotes:
1 Marshalling In Runtime talks of use of filter blocks with Runtime
Invoke Wrappers
Date: 2009-05-10 21:16:46 IST
HTML generated by org-mode 6.17c in emacs 21