Mono Runtime Notes

May 4, 2009

Opcode Emulation and Related Non-Trivia

OpCodeEmulation

1 Introduction

In this article, we will examine how Mono does Opcode
emulation.

Using Opcode Emulation as a convenience, we would examine the
following components of the runtime

  • JIT Internal Calls (or JIT Icalls)
  • Managed to Native Wrappers
  • Method Prolog and Epilog

While examining the method prologs for icall wrappers we would
familiarize ourselves with LMF – Last Managed Frame.

All this we do by considering a simple example. In step with our
tradition, we will explore only those aspects of the above
components that are relevant to the example under consideration.

2 Example Assembly

Let’s consider the example assembly down below.

The Main () method performs a long-long multiplication within and
without overflow checking.

  // file: LongMul.cs
  using System;

  class HelloWorld
  {
   public static void Main ()
   {
       long a = 0x1L;

       a *= a;

       checked {
           a *= a;
       }

       Console.ReadLine ();
   }
  }

The CIL code generated by compiler is shown down below.

Note that the C# longs are mapped to 64-bit entities. Furthermore
the non-checked and checked long-long multiplication is mapped to
the following two different opcodes – mul and mul.ovf.

On my Intel 32-bit machine, the above 64-bit operation cannot be
directly mapped to a hardware instruction. As we shall see shortly,
the Mono runtime emulates these operations using 32-bit
arithmetic. The emulator is implemented in C and is embedded right
within the runtime (as opposed to an external library).

  $ mcs LongMul.cs

  $ ildasm LongMul.exe

   .method public static hidebysig void Main() cil managed
   {
           // Start of method header: 20f4
           .entrypoint
           .maxstack  3
           .locals    init (int64)
   ?L2100:
           ldc.i4.1
           conv.i8
           stloc.0
           ldloc.0
           ldloc.0
           mul
           stloc.0
           ldloc.0
           ldloc.0
           mul.ovf
           stloc.0
           call       class System.String [mscorlib]System.Console::ReadLine()
           pop
           ret
   }

3 Opcode Emulator as a JIT ICall

The following code snippet shows how the Opcode Emulators are
initialized in the runtime.

Note that the emulator for ‘mul’ is registered as a routine that
will never throw an exception while that for mul.ovf is registered
as one that can raise an exception.

In the next section we will see that this ‘can throw’ behaviour of
the opcode has implications on how the call to emulators are
arranged for by the runtime.

  // Register Long Mul Emulators

  mono_register_opcode_emulation (OP_LMUL,     "__emul_lmul",     "long long long", mono_llmult,     TRUE);
  mono_register_opcode_emulation (OP_LMUL_OVF, "__emul_lmul_ovf", "long long long", mono_llmult_ovf, FALSE);
                                      ^               ^                  ^                ^            ^
                                      |               |                  |                |            |
                                      |               |                  |                |            |
                       (Effective) CIL Opcode    Emulator Name      Signature String   Emulator    Compile Now / Doesn't Throw



  // Opcode Emulators are but JIT Icalls

  void
  mono_register_opcode_emulation (int opcode, const char *name,
                                  const char *sigstr, gpointer func,
                                  gboolean no_throw)
  {
      MonoMethodSignature *sig = mono_create_icall_signature (sigstr);

      // Snippet of code from mono_register_jit_icall (func, name, sig, no_throw);
      MonoJitICallInfo *info =  g_new0 (MonoJitICallInfo, 1);
      info->name = name;
      info->func = func;
      info->sig = sig;

      if (no_throw) {
          info->wrapper = func;
      } else {
          info->wrapper = NULL;
      }

      emul_opcode_map [opcode] = info;
  }

4 Code Generation for Emulated Ops

Let’s now trace down the sequence of steps as the runtime goes about
generating code for the ‘mul’ and ‘mul.ovf’ in the Main ().

Firstly, the ‘generic’ CIL opcodes – CEE_MUL and CEE_MUL _OVF gets
mapped to more specific ‘long’ opcodes – OP_LMUL and OP_LMUL _OVF
respectively. This happens as part of type_from _op ().

Next, the complex OP_LMUL and OP_LMUL _OVF opcodes are mapped as
calls to their respective emulators. This is done as part of
mono_decompose _opcode ().

  // Snippet code from mono_decompose_opcode () for opcodes - OP_LMUL
  // and OP_LMUL_OVF

  MonoJitICallInfo *info = mono_find_jit_opcode_emulation (ins->opcode)
  gpointer wrapper = mono_icall_get_wrapper_full (info, FALSE)
  MonoInst *call = mono_emit_native_call (cfg, wrapper, info->sig, args)
  static gconstpointer
  mono_icall_get_wrapper_full (MonoJitICallInfo* callinfo, gboolean do_compile)
  {
      char *name;
      MonoMethod *wrapper;
      gconstpointer trampoline;
      MonoDomain *domain = mono_get_root_domain ();

      if (callinfo->wrapper) {
          return callinfo->wrapper;
      }

      if (callinfo->trampoline)
          return callinfo->trampoline;

      name = g_strdup_printf ("__icall_wrapper_%s", callinfo->name);
      wrapper = mono_marshal_get_icall_wrapper (callinfo->sig, name, callinfo->func, check_for_pending_exc);

      callinfo->trampoline =
       mono_create_ftnptr (domain,
                           mono_create_jit_trampoline_in_domain (domain, wrapper));
      return callinfo->trampoline;
  }

For the impatient, the mapping is as follows:

  1. OP_LMUL -> long mono_llmult (long, long)
  2. OP_LMUL _OVF -> long object:__icall _wrapper ___emul _lmul _ovf (long, long)

Note that OP_LMUL maps to the emulator itself while OP_LMUL _OVF gets
mapped to emulator via a wrapper.

This wrapping of ovf emulator is essential to do stack unwinding in
case an exception is raised from the emulated code.

5 Internals of ICall Wrapper (CIL)

The code down below lists the runtime generated IL code that wraps
around mono_llmult _ovf . This wrapper is generated as part of
mono_marshal _get _icall _wrapper () using the method builder
interface. The handle of the wrapper so generated is of type
MonoMethodWrapper *.

It is sufficient to note that a native call is translated to the
following sequence – CEE_LDARG *, CEE_MONO _LDPTR and
CEE_CALLI . CEE_CALLI is an indirect method call and requires that a
pointer to native method be pushed on the CIL stack
apriori. CEE_MONO _LDPTR is a mono-only opcode that does just
this. (It pushes mono_llmult _ovf in our case)

Furthermore note that the ‘ptr descriptor’ – ‘2 in mono_ldptr2’ and
‘call site descriptors’ – ‘0x00000003 in calli’ in the IL code down
below are method-specific tokens through which the actual entities –
the target native pointer and method signature could be
accessed. Refer mono_method _get _wrapper _data () for the specifics.

  // managed-to-native wrapper for mul.ovf
  // object:__icall_wrapper___emul_lmul_ovf (long,long)

  IL_fffe100c: ldarg.0
  IL_fffe100d: ldarg.1
                                                                    <----+
  IL_fffe100e: mono_ldptr2          // mono_mb_emit_ptr (mb, func)       |--  mono_mb_emit_native_call (mb, sig, func /* func = mono_llmult_ovf */)
  IL_fffe1014: calli     0x00000003 // mono_mb_emit_calli (mb, sig) <----+

  IL_fffe1019: mono_ldptr4          --+
  IL_fffe101f: ldind.u4               |
  IL_fffe1020: brfalse   IL_fffe102d  |
  IL_fffe1025: mono_not_taken         |--- // emit_thread_interrupt_checkpoint (mb)
  IL_fffe1027: mono_icall5            |
  IL_fffe102a: nop                    |
  IL_fffe102b: nop                    |
  IL_fffe102c: nop                 ---+
  IL_fffe102d: ret

6 LMF – Last Managed Frame

Before we examine, the native code for the ICall wrapper, let’s
cursorily see what a Mono LMF – Last Managed Frame is.

  struct MonoLMF {
      /* Offset by 1 if this is a trampoline LMF frame */
      guint32    previous_lmf;
      gpointer    lmf_addr;
      /* Only set in trampoline LMF frames */
      MonoMethod *method;
      /* Only set in trampoline LMF frames */
      guint32     esp;
      guint32     ebx;
      guint32     edi;
      guint32     esi;
      guint32     ebp;
      guint32     eip;
  };

As seen from the definition above, MonoLMF stores machine state and
are chained together (just like any other stack frame)

A MonoLMF is pushed on to a thread stack whenever a thread
transitions from Managed-to-Native code. An entry so pushed is
popped back on the way out.

Under conditions of fault in the native code, the top entry of ‘LMF
stack’ is used by the runtime to unwind through all native frames in
a single go and jump straight to the topmost managed frame of which
it knows well enough to deconstruct.

In a way, maintaining LMF stack helps the Mono runtime side-step
idiosyncracies associated with unwinding of native stack frames 1.

7 Internals of ICall Wrapper (Native)

The code down below lists the native code for the icall wrapper. The
generated code is typical of a managed-to-native wrapper. The prolog
pushes an LMF frame on top of LMF Stack while the epilog pops it
out. Refer mono_arch _emit _prolog () and mono_arch _emit _epilog () for
additional details.

As an aside, an interested reader can note the following –

  1. Variable that contains the current LMF is accessed through the GS
    register – indicating thread specific storage.
  2. The basic block containing mono_not _taken is offlined.
  3. Components parts of all compiled methods – A prolog and a
    matching epilog in addition to the method body.

     // object:__icall_wrapper___emul_lmul_ovf (long,long)
     // @0xb79962e0 - 0xb799635b:
                                                                    ^
           push   ebp                                               |
           mov    ebp,esp                                           |
           push   0xb79962e4                <----+                  |
           push   ebp                            |                  |
           push   esi                            |                  |  method prolog
           push   edi                            |--push an entry   |
           push   ebx                            |   on top of      |
           mov    eax,DWORD PTR gs:0xffffffe4    |   LMF stack      |
           sub    esp,0xc                        |                  |
           push   eax                        <---+                  |
           mov    DWORD PTR gs:0xffffffe4,esp <---- update top of   |
           sub    esp,0x24                            LMF stack     V
    
    
                                                                    ^
           push   DWORD PTR [ebp+20]                                |
           push   DWORD PTR [ebp+16]                                |
           push   DWORD PTR [ebp+12]                                |  method body
           push   DWORD PTR [ebp+8]                                 |
           call   0x80b6fe0 <mono_llmult_ovf>                       |
           add    esp,0x10                                          |
           mov    DWORD PTR [ebp-44],edx                            |
           mov    DWORD PTR [ebp-48],eax                            |
                                                                    |
           mov    eax,DWORD PTR ds:0x82a22d0  &thread_interruption_r|quested
                                                                    |
           mov    ecx,DWORD PTR [ebp-48]                            |
           mov    DWORD PTR [ebp-56],ecx                            |
           mov    ecx,DWORD PTR [ebp-44]                            |
           mov    DWORD PTR [ebp-52],ecx                            |
           test   eax,eax                                           |
           jne    74                                                |
                                                                    |
      51:  mov    eax,DWORD PTR [ebp-56]                            |
           mov    edx,DWORD PTR [ebp-52]                            |
                                                                    V
    
    
           mov    ecx,DWORD PTR gs:0xffffffdc                       ^
           mov    ecx,DWORD PTR [ecx+12]                            |
           cmp    ecx,0x0                                           |  method epilog
           je     0xb7996348                                        |
           call   ecx                                               |
           mov    ecx,DWORD PTR [ebp-36]                            |
           mov    DWORD PTR gs:0xffffffe4,ecx <--- pop LMF stack    V
           leave
           ret
                                                                    ^ method body (contd) - Rare code path 'offlined'
      74:  call   0xb79962d4  // tramp  for __icall_wrapper_mono_thr|ad_interruption_checkpoint
           jmp    51                                                |
                                                                    V
    

8 Compiled Assembly

For the sake of completion, the native code for HelloWorld.Main ()
is given below.

Note that

  1. CIL opcode – ‘mul’ for long operands is translated as a call to
    mono_llmult (long, long)

  2. CIL opcode – ‘mul.ovf’ for long operands is translated as a call
    to object:__icall _wrapper ___emul _lmul _ovf (long, long).

      // Compiled code for HelloWorld.Main ()
      // @0xb7996280 to @0xb79962c6
    
      0xb7996280:     push   ebp
      0xb7996281:     mov    ebp,esp
      0xb7996283:     sub    esp,0x18
      0xb7996286:     push   0x0
      0xb7996288:     push   0x1
      0xb799628a:     push   0x0
      0xb799628c:     push   0x1
      0xb799628e:     call   0x80b6a70             // mono_llmult (long, long)
      0xb7996293:     add    esp,0x10
      0xb7996296:     mov    DWORD PTR [ebp-4],edx
      0xb7996299:     mov    DWORD PTR [ebp-8],eax
    
      0xb799629c:     push   DWORD PTR [ebp-4]
      0xb799629f:     push   DWORD PTR [ebp-8]
      0xb79962a2:     push   DWORD PTR [ebp-4]
      0xb79962a5:     push   DWORD PTR [ebp-8]
      0xb79962a8:     call   0xb79962e0             // object:__icall_wrapper___emul_lmul_ovf (long, long)
      0xb79962ad:     add    esp,0x10
      0xb79962b0:     mov    DWORD PTR [ebp-4],edx
      0xb79962b3:     mov    DWORD PTR [ebp-8],eax
      0xb79962b6:     mov    DWORD PTR [ebp-16],eax
      0xb79962b9:     mov    eax,DWORD PTR [ebp-4]
      0xb79962bc:     mov    DWORD PTR [ebp-12],eax
    
      0xb79962bf:     call   0xb7996360            // Console.ReadLine ()
      0xb79962c4:     leave
      0xb79962c5:     ret
    

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

Date: 2009-05-04 14:27:49 IST

Advertisements

Blog at WordPress.com.

%d bloggers like this: