首先声明一下,这是一个很深的话题,也是朋友真实遇到的,它用 ​​DynamicMethod + ILGenerator​​​ 生成了很多动态方法,然而这动态方法中有时候经常会遇到​​溢出异常​​​,寻求如何调试 ​​动态方法体​​​,我知道如果用 ​​visual studio​​​ 来调试的话,我个人觉得很难,这时候只能用 ​​windbg​​ 了,接下来我聊一下具体调试步骤。

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));
}
}

这是一个动态生成的 ​​Add(int a,int b)​​ 方法,那如何调试它的方法体呢?这里有两个技巧。

第一:使用 ​​Debugger.Break();​​​ 这个语句可以通知附加到该进程的 ​​Debugger​​ 中断,也就是 Windbg。

第二:使用 ​​Marshal.GetFunctionPointerForDelegate​​​ 获取 ​​委托方法​​ 的函数指针地址。

基于上面两点,修改代码如下:

        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上的输出如下:

如何调试 C# Emit 生成的动态代码?_开发语言

2. 寻找 codeheap 上的方法体字节码

接下来我们反编译下 ​​0x00000000023d062e​​ 这个函数指针。

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​​​ 才是最后真实的 ​​动态方法​​​ 指针地址,接下来我们用 ​​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​​  直接下断点调试。

  • 困难模式

使用托管命令 ​​!bpmd xxx​​ 寻找方法描述符下断点调试。

这里我就选择 ​​困难模式​​ 来处理。

3. 使用 bpmd 下断点

要用 ​​!bpmd​​​ 下断点,必须要有 ​​方法描述符​​​, 现在我们有了 ​​codeaddr​​​ 如何反向找描述符呢?这里可用 ​​!mln​​。

0:000> !mln 00a90050
Method instance: (BEGIN=00a90050)(MD=0071537c disassemble)[DynamicClass.Add(Int32, Int32)]

上面输出的 ​​MD=0071537c​​​ 就是方法描述符的地址,接下来就可以用 ​​!bpmd -md 0071537c​​ 设置断点即可。

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

从输出看,已经成功命中断点,而且 clr 也帮我自动转接到了 ​​bp 00A90050​​,接下来看下命中的断点图:

如何调试 C# Emit 生成的动态代码?_描述符_02

上面的二条汇编指令就是 ​​a+b​​ 的结果,也就是 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​​ 便是。