Вот мне всегда было интересно как имплементится на машинном уровне поведение такого кода:

class Foo {
  static void Main() {
    bool jump = true;

    Label:
    try {
      System.Console.WriteLine("try");
      if (jump) {
        jump = false;
        goto Label;
      }
    } finally {
      System.Console.WriteLine("finally");
    }
  }
}

Который выводит на экран:

try
finally
try
finally

То есть любой выход из блока try, даже прыжком “вверх” по коду, должен завершаться вызовом блока finally. Я с ассемблером не дружу, но оказалось всё очень просто:

     4:     static void Main() {
...
     5:         bool jump = true;
00000039  mov         eax,1 
0000003e  and         eax,0FFh 
00000043  mov         dword ptr [ebp-24h],eax 
00000046  nop 
     6: 
     7:         Label:
     8:         try {
00000047  nop 
     9:             System.Console.WriteLine("try");
00000048  mov         ecx,dword ptr ds:[02C32030h] 
0000004e  call        641F703C 
00000053  nop                   // в зависимости от условия
    10:             if (jump) { // к выходу из try-блока
00000054  cmp         dword ptr [ebp-24h],0
00000058  sete        al                     
0000005b  movzx       eax,al                 
0000005e  mov         dword ptr [ebp-28h],eax
00000061  cmp         dword ptr [ebp-28h],0 
00000065  jne         00000083 
00000067  nop 
    11:                 jump = false;
00000068  xor         edx,edx 
0000006a  mov         dword ptr [ebp-24h],edx 
    12:                 goto Label;
0000006d  nop 
0000006e  mov         dword ptr [ebp-1Ch],0     // сохраняем
00000075  mov         dword ptr [ebp-18h],0FCh  // в стек
0000007c  push        3C012Dh // <== адрес перехода @ 00000047
00000081  jmp         0000009A 
    13:             }
    14:         }
00000083  nop 
00000084  nop 
00000085  mov         dword ptr [ebp-1Ch],0 
0000008c  mov         dword ptr [ebp-18h],0FCh // или
00000093  push        3C0136h  // <== адрес перехода @ 000000ac
00000098  jmp         0000009A 
    15:         finally {
0000009a  nop 
    16:             System.Console.WriteLine("finally");
0000009b  mov         ecx,dword ptr ds:[02C32034h] 
000000a1  call        641F703C 
000000a6  nop 
    17:         } 
000000a7  nop              // после finally каждый раз
000000a8  pop         eax  // достаём адрес из стека
000000a9  jmp         eax  // и прыгаем по нему
000000ab  nop 
    18: 
    19:         Debugger.Break();
000000ac  call        64700020 
000000b1  nop  
    20:     }
... 

То есть найдя прыжок из try-блока, JIT-компилятор сгенерировал после finally-блока прыжок по адресу из вершины стека и убедился, что при любом из выходов из try в стек положат адрес кода, с которого следует продолжать исполнение. Аналогичная магия происходит при использовании break, continue и return внутри try { }.

Возникает вопрос: как посмотреть достоверный disassembly управляемого кода? Очень просто - включаем в студии address-level debugging:

Вставляем в код вызов System.Diagnostics.Debugger.Break(), собираем проект в RELEASE. Если вам надо видеть *соответствие *C#-исходников с соответствующими машинными инструкциями, то надо компилировать с отключенными оптимизациями (это оптимизации на MSIL-уровне, они относительно не сильно видоизменяют машинный код):

Включая оптимизации C#, вы получите достоверный ассемблерный листинг, но что там есть что - вам придётся разбираться самому. В любом случае необходимо включить компиляцию с полной debug-информацией:

После этого запускаем код без отладчика (это задействует оптимизации на уровне JIT):

Дожидаемся срабатывания Debugger.Break() и вызываем отладчик:

Подключаемся нужным экземпляром Visual Studio и вызываем из контекстного меню Go to disassembly - вот и всё, можно смело курить ассмеблерные листинги. Однако следует обратить внимание вот ещё на что:

  • Некоторые вызовы могут быть попросту не скомпилированы JIT’ом. Следует организовывать код так, чтобы исследуемый фрагмент хотя раз бы выполнился, а уже потом подключаться отладчиком. Либо можно воспользоваться статическим методом PrepareMethod класса System.Runtime.CompilerServices.RuntimeHelpers для того, чтобы вручную вызвать JIT-компиляцию заданных методов.
  • Иногда исследуемый код подвергается inline-оптимизации и это может мешать. Запретить инлайнинг того или иного метода можно с помощью атрибута MethodImplAttributeиз того же пространства имён: ```c# using System.Runtime.CompilerServices;

class Foo { [MethodImpl(MethodImplOptions.NoInlining)] public static void NoInline() { } } ```