如何調(diào)試 C# Emit 生成的動態(tài)代碼?
首先聲明一下,這是一個很深的話題,也是朋友真實遇到的,它用 DynamicMethod + ILGenerator 生成了很多動態(tài)方法,然而這動態(tài)方法中有時候經(jīng)常會遇到溢出異常,尋求如何調(diào)試 動態(tài)方法體,我知道如果用 visual studio 來調(diào)試的話,我個人覺得很難,這時候只能用 windbg 了,接下來我聊一下具體調(diào)試步驟。
1. 測試代碼
為了方便講解,上一段測試代碼。
class Program
{
private delegate int AddDelegate(int a, int b);
static void Main(string[] args)
{
var dynamicAdd = new DynamicMethod("Add", typeof(int), new[] { typeof(int), typeof(int) }, true);
var il = dynamicAdd.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Add);
il.Emit(OpCodes.Ret);
var addDelegate = (AddDelegate)dynamicAdd.CreateDelegate(typeof(AddDelegate));
Console.WriteLine(addDelegate(10, 20));
}
}
這是一個動態(tài)生成的 Add(int a,int b) 方法,那如何調(diào)試它的方法體呢?這里有兩個技巧。
第一:使用 Debugger.Break(); 這個語句可以通知附加到該進程的 Debugger 中斷,也就是 Windbg。
第二:使用 Marshal.GetFunctionPointerForDelegate 獲取 委托方法 的函數(shù)指針地址。
基于上面兩點,修改代碼如下:
static void Main(string[] args)
{
var dynamicAdd = new DynamicMethod("Add", typeof(int), new[] { typeof(int), typeof(int) }, true);
var il = dynamicAdd.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Add);
il.Emit(OpCodes.Ret);
var addDelegate = (AddDelegate)dynamicAdd.CreateDelegate(typeof(AddDelegate));
Console.WriteLine("Function Pointer: 0x{0:x16}", Marshal.GetFunctionPointerForDelegate(addDelegate).ToInt64());
Debugger.Break();
Console.WriteLine(addDelegate(10, 20));
}
接下來可以用 windbg 把 exe 程序啟動起來,可以看到console上的輸出如下:
圖片
2. 尋找 codeheap 上的方法體字節(jié)碼
接下來我們反編譯下 0x00000000023d062e 這個函數(shù)指針。
0:000> !U 0x00000000023d062e
Unmanaged code
023d062e b818063d02 mov eax,23D0618h
023d0633 e9e4c934fe jmp 0071d01c
023d0638 ab stos dword ptr es:[edi]
023d0639 ab stos dword ptr es:[edi]
023d063a ab stos dword ptr es:[edi]
023d063b ab stos dword ptr es:[edi]
023d063c ab stos dword ptr es:[edi]
023d063d ab stos dword ptr es:[edi]
023d063e ab stos dword ptr es:[edi]
023d063f ab stos dword ptr es:[edi]
上面的 23D0618h 才是最后真實的 動態(tài)方法 指針地址,接下來我們用 dp 看看指針上的值。
0:000> dp 23D0618h L1
023d0618 00a90050
接下來我們反編譯下 00a90050 地址看看方法體的匯編代碼。
0:000> !U 00a90050
Normal JIT generated code
DynamicClass.Add(Int32, Int32)
Begin 00a90050, size 5
>>> 00a90050 8bc1 mov eax,ecx
00a90052 03c2 add eax,edx
00a90054 c3 ret
接下來有兩條路:
- 熟路模式
使用非托管命令 bp 00a90050 直接下斷點調(diào)試。
- 困難模式
使用托管命令 !bpmd xxx 尋找方法描述符下斷點調(diào)試。
這里我就選擇 困難模式 來處理。
3. 使用 bpmd 下斷點
要用 !bpmd 下斷點,必須要有 方法描述符, 現(xiàn)在我們有了 codeaddr 如何反向找描述符呢?這里可用 !mln。
0:000> !mln 00a90050
Method instance: (BEGIN=00a90050)(MD=0071537c disassemble)[DynamicClass.Add(Int32, Int32)]
上面輸出的 MD=0071537c 就是方法描述符的地址,接下來就可以用 !bpmd -md 0071537c 設(shè)置斷點即可。
0:000> !bpmd -md 0071537c
MethodDesc = 0071537c
Setting breakpoint: bp 00A90050 [DynamicClass.Add(Int32, Int32)]
0:000> g
Breakpoint 0 hit
eax=02505fe8 ebx=0019f5ac ecx=0000000a edx=00000014 esi=0250230c edi=0019f4fc
eip=00a90050 esp=0019f488 ebp=0019f508 iopl=0 nv up ei pl nz na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206
00a90050 8bc1 mov eax,ecx
從輸出看,已經(jīng)成功命中斷點,而且 clr 也幫我自動轉(zhuǎn)接到了 bp 00A90050,接下來看下命中的斷點圖:
圖片
上面的二條匯編指令就是 a+b 的結(jié)果,也就是 ecx 放了 a, edx 放了 b,不信的話可以 step 二次。
0:000> t
eax=0000000a ebx=0019f5ac ecx=0000000a edx=00000014 esi=0250230c edi=0019f4fc
eip=00a90052 esp=0019f488 ebp=0019f508 iopl=0 nv up ei pl nz na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206
00a90052 03c2 add eax,edx
0:000> t
eax=0000001e ebx=0019f5ac ecx=0000000a edx=00000014 esi=0250230c edi=0019f4fc
eip=00a90054 esp=0019f488 ebp=0019f508 iopl=0 nv up ei pl nz na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206
00a90054 c3 ret
這里的 ecx=0000000a edx=00000014 便是。