朋友公司的一套面试题,很有意思,参见如下代码:
class Program{static void Main(string[] args){var t = Num();Console.WriteLine(t);Console.ReadLine();}static int Num(){int i = 10;try{return i;}finally{i = 11;Console.WriteLine($"i={i}");}}}
请问这段代码会输出什么?相信有一点编程经验的朋友都知道,答案是:output: i=11
,接下来的一个问题是这个 Num()
方法的返回值是多少, 10
还是 11
?相信有很多朋友就有点迷糊了,那答案是多少呢?在不运行程序的情况下,我们从 IL
和 汇编角度
来寻找答案。
一:IL 上寻找答案
要想看 IL,可以用 ILSpy 反编译一下。
.method private hidebysig static int32 Num () cil managed
{// Method begins at RVA 0x2074// Code size 39 (0x27).maxstack 2.locals init ([0] int32 i,[1] int32)IL_0000: nopIL_0001: ldc.i4.s 10IL_0003: stloc.0.try{IL_0004: nopIL_0005: ldloc.0IL_0006: stloc.1IL_0007: leave.s IL_0025} // end .tryfinally{IL_0009: nopIL_000a: ldc.i4.s 11IL_000c: stloc.0IL_000d: ldstr "i={0}"IL_0012: ldloc.0IL_0013: box [mscorlib]System.Int32IL_0018: call string [mscorlib]System.String::Format(string, object)IL_001d: call void [mscorlib]System.Console::WriteLine(string)IL_0022: nopIL_0023: nopIL_0024: endfinally} // end handlerIL_0025: ldloc.1IL_0026: ret
} // end of method Program::Num
对比 return i
生成的 IL 代码,它的做法是将 i
保存到了一个临时变量 loc.1
处,最后将 loc.1
处的变量返回出去,我们很惊讶的发现 finally
块中并没有对 loc.1
处的变量赋值,也就是没有 stloc.1
指令,综合下来结果应该是 10, 而不是 11。
不知道可有朋友发现,这里的跳转指令 IL_0007: leave.s IL_0025
,貌似直接跳过了 finally
块,那到底有没有执行呢?这个从 IL 上看不出,只能从 汇编角度 看啦。
二:查看汇编
接下来祭出windbg,汇编代码如下:
0:006> !U /d 008608a8
Normal JIT generated code
ConsoleApp1.Program.Num()
Begin 008608a8, size aaD:\net5\ConsoleApp1\ConsoleApp1\Program.cs @ 23:
008608d4 c745e40a000000 mov dword ptr [ebp-1Ch],0AhD:\net5\ConsoleApp1\ConsoleApp1\Program.cs @ 26:
008608db 90 nopD:\net5\ConsoleApp1\ConsoleApp1\Program.cs @ 27:
008608dc 8b45e4 mov eax,dword ptr [ebp-1Ch]
008608df 8945e0 mov dword ptr [ebp-20h],eax
008608e2 90 nop
008608e3 c745ec00000000 mov dword ptr [ebp-14h],0
008608ea c745f0fc000000 mov dword ptr [ebp-10h],0FCh
008608f1 6849098600 push 860949h
008608f6 eb00 jmp 008608f8D:\net5\ConsoleApp1\ConsoleApp1\Program.cs @ 30:
008608f8 90 nopD:\net5\ConsoleApp1\ConsoleApp1\Program.cs @ 31:
008608f9 c745e40b000000 mov dword ptr [ebp-1Ch],0BhD:\net5\ConsoleApp1\ConsoleApp1\Program.cs @ 32:
00860900 b9a8429778 mov ecx,offset mscorlib_ni+0x142a8 (789742a8) (MT: System.Int32)
00860905 e8ea27daff call 006030f4 (JitHelp: CORINFO_HELP_NEWSFAST)
0086090a 8945dc mov dword ptr [ebp-24h],eax
0086090d 8b0544235d03 mov eax,dword ptr ds:[35D2344h] ("i={0}")
00860913 8945d4 mov dword ptr [ebp-2Ch],eax
00860916 8b45dc mov eax,dword ptr [ebp-24h]
00860919 8b55e4 mov edx,dword ptr [ebp-1Ch]
0086091c 895004 mov dword ptr [eax+4],edx
0086091f 8b45dc mov eax,dword ptr [ebp-24h]
00860922 8945d0 mov dword ptr [ebp-30h],eax
00860925 8b4dd4 mov ecx,dword ptr [ebp-2Ch]
00860928 8b55d0 mov edx,dword ptr [ebp-30h]
0086092b e820d74c78 call mscorlib_ni!System.String.Format(System.String, System.Object)$##6000545 (78d2e050)
00860930 8945d8 mov dword ptr [ebp-28h],eax
00860933 8b4dd8 mov ecx,dword ptr [ebp-28h]
00860936 e829465b78 call mscorlib_ni!System.Console.WriteLine(System.String)$##6000B79 (78e14f64)
0086093b 90 nopD:\net5\ConsoleApp1\ConsoleApp1\Program.cs @ 33:
0086093c 90 nop
0086093d 58 pop eax
0086093e ffe0 jmp eaxD:\net5\ConsoleApp1\ConsoleApp1\Program.cs @ 34:
00860940 8b45e0 mov eax,dword ptr [ebp-20h]
00860943 8d65fc lea esp,[ebp-4]
00860946 5f pop edi
00860947 5d pop ebp
00860948 c3 retD:\net5\ConsoleApp1\ConsoleApp1\Program.cs @ 22:
00860949 c745f000000000 mov dword ptr [ebp-10h],0
00860950 ebee jmp 00860940
为了方便比对,我再把代码行数给截出来。
一般函数的返回值都是放在 eax
中,所以重点关注下 eax
的赋值部分,仔细观察它的路径大概就是下面四句代码:
mov dword ptr [ebp-1Ch],0Ah
mov eax,dword ptr [ebp-1Ch]
mov dword ptr [ebp-20h],eax
mov eax,dword ptr [ebp-20h]
也就是说,最后的 eax 还是当初的 0Ah= 10
, 也和 IL 反映出来的一致,return 操作的底层会将返回值放到一个 临时区域 = ebp-20h
中,最后返回 临时区域 中的值。
再回到刚才 IL 部分的疑问,从上面的 jmp 008608f8
指令看,return 的下一步就直接进了 finally
块,最后执行 RET
弹出下一行代码指令到 EIP 中完成方法体的执行,大概就是这个样子。