Monday, February 14, 2011

Disassembly view и goto from try { }

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

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 { }.

Read more: Control::Flow