Mono Runtime Notes

May 10, 2009

Exception Handling

ExceptionHandling

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 -

  1. Native code generation for

    1. try-catch and try-finally blocks SEH blocks 1
    2. throw CIL instruction

  2. Exception Handling

    1. Fault Handler Identification
    2. 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 -

  1. 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.

  2. The property Test.Path traces and logs the control flow.
  3. 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 -

  1. 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.

  2. 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 -

  1. Handler Identification Pass – In this pass, the runtime walks the
    stack and tries to identify a block that is willing to handle the
    exception.

  2. 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

  1. Stack is merely walked.

In the Handler Identification Pass,

  1. The exception object is passed to the filtering code by storing
    it in a pre-defined location on the current stack frame.

  2. The stack walk is terminated as soon as a catch or a filter
    handler that is willing to handle the exception is identified.

  3. Failure to locate a handler causes a FALSE to be returned.

In the Handler Invocation Pass,

  1. The intervening finally and fault handlers are called via an
    architecture specific stub – mono_arch_get_call_filter ()

  2. Once the matching filter or catch handler is identified, the EIP
    in ctx is adjusted to point to the handler address using the macro

    MONO_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 frame

    As 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

Footnotes:

1 Marshalling In Runtime talks of use of filter blocks with Runtime
Invoke Wrappers

2 Undeniable Exception Propagation

Author: Jambunathan K. Consult About page for
my Inbox information.

Date: 2009-05-10 21:16:46 IST

HTML generated by org-mode 6.17c in emacs 21

Older Posts »

The Silver is the New Black Theme. Create a free website or blog at WordPress.com.

Follow

Get every new post delivered to your Inbox.