.NET高级调试 | 通过JIT拦截无侵入调试 C# Emit 生成的动态代码

Posted dotNET跨平台

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了.NET高级调试 | 通过JIT拦截无侵入调试 C# Emit 生成的动态代码相关的知识,希望对你有一定的参考价值。

大家还记得上一篇的测试代码吗?我们用了:

Console.WriteLine("Function Pointer: 0x0:x16", Marshal.GetFunctionPointerForDelegate(addDelegate).ToInt64());

来获得 委托函数指针 地址,通过这个突破口最终实现了 动态代码 的调试,这种方式可以是可以,但很显然这是侵入式的,那有没有办法实现 非侵入 调试动态代码呢?在 .NET高级调试 这本书上还真给找到了,方法就是在  JIT 编译动态方法时进行拦截,获取其中的 方法描述符

为了方便讲解,先上测试代码:

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

            //Debugger.Break();
            //Console.WriteLine("Function Pointer: 0x0:x16", Marshal.GetFunctionPointerForDelegate(addDelegate).ToInt64());

            Console.WriteLine(addDelegate(10, 20));
        
    

可以看到,我把上面两行侵入式的代码给屏蔽掉了,接下来在 il.Emit(OpCodes.Ret); 处下断点,目的是为了在 clr 加载后寻找 JIT的 compileMethod 方法。

0:000> !mbp Program.cs 28
The CLR has not yet been initialized in the process.
Breakpoint resolution will be attempted when the CLR is initialized.
0:000> g
ModLoad: 76910000 7698a000   C:\\Windows\\SysWOW64\\ADVAPI32.dll
...
ModLoad: 77190000 77226000   C:\\Windows\\SysWOW64\\OLEAUT32.dll
Breakpoint: JIT notification received for method ConsoleApp1.Program.Main(System.String[]) in AppDomain 00783758.
Breakpoint set at ConsoleApp1.Program.Main(System.String[]) in AppDomain 00783758.
Breakpoint 1 hit
eax=00000001 ebx=0019f5ac ecx=023c3684 edx=ffffffff esi=023c230c edi=0019f4fc
eip=048a0a02 esp=0019f4ac ebp=0019f508 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
048a0a02 b901000000      mov     ecx,1

接下来可以用 x 命令模糊搜索 compileMethod 签名,找出签名是为了更好的下断点。

0:000> x *!*compileMethod*
...
61413700          clrjit!CILJit::compileMethod (class ICorJitInfo *, struct CORINFO_METHOD_INFO *, unsigned int, unsigned char **, unsigned long *)

可以看到 compileMethod 的完整签名是 clrjit!CILJit::compileMethod, 并且它的方法入口点地址是 61413700,有了它就可以对其下断点啦!

0:000> bp 61413700
0:000> g
Breakpoint 0 hit
eax=61494698 ebx=80000004 ecx=61413700 edx=00005c10 esi=6148b3fc edi=0019efa4
eip=61413700 esp=0019ede0 ebp=0019ee38 iopl=0         nv up ei ng nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000282
clrjit!CILJit::compileMethod:
61413700 55              push    ebp
0:000> kb
 # ChildEBP RetAddr      Args to Child              
00 0019ee38 62a4ccc3     61494698 0019efa4 0019ef1c clrjit!CILJit::compileMethod [f:\\dd\\ndp\\clr\\src\\jit32\\ee_il_dll.cpp @ 151] 
01 0019ee38 62a4cd9b     0019ef1c 0019f06c 0019f024 clr!invokeCompileMethodHelper+0x10b

很开心,成功命中,接下来提取 compileMethod 方法的第三个参数,它就是需要编译方法所指向的 方法描述符 地址,可以用 dp 给提取出来。

0:000> dp 0019ef1c L1
0019ef1c  0071537c
0:000> !dumpmd 0071537c
Method Name:  DynamicClass.Add(Int32, Int32)
Class:        007152e8
MethodTable:  0071533c
mdToken:      06000000
Module:       00714ea8
IsJitted:     no
CodeAddr:     ffffffff
Transparency: Transparent

方法描述符果然给调出来了,但这里的方法字节码是 CodeAddr: ffffffff ,说明此时动态方法还没有开始编译,为了能够使其编译,我们在 Console.WriteLine(addDelegate(10, 20)); 处再下一个断点,因为代码到此处时, JIT 肯定编译了该办法,自然就能看到编译后的 CodeAddr 地址。

0:000> !mbp Program.cs 35
Breakpoint set at ConsoleApp1.Program.Main(System.String[]) in AppDomain 00783758.
0:000> g
Breakpoint 3 hit
eax=023c5f88 ebx=0019f5ac ecx=023c5f3c edx=00008f17 esi=023c230c edi=0019f4fc
eip=048a0a9b esp=0019f4ac ebp=0019f508 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
048a0a9b 6a14            push    14h
0:000> !dumpmd 0071537c
Method Name:  DynamicClass.Add(Int32, Int32)
Class:        007152e8
MethodTable:  0071533c
mdToken:      06000000
Module:       00714ea8
IsJitted:     yes
CodeAddr:     04a00050
Transparency: Transparent

可以看到,此时的 CodeAddr: 04a00050 ,也就表明已经编译完成了,接下来继续 bp 。

0:000> bp 04a00050
0:000> g
Breakpoint 4 hit
eax=023c5f98 ebx=0019f5ac ecx=0000000a edx=00000014 esi=023c230c edi=0019f4fc
eip=04a00050 esp=0019f4a8 ebp=0019f508 iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
04a00050 8bc1            mov     eax,ecx

可以看到,全部搞定,非侵入式,🐂

以上是关于.NET高级调试 | 通过JIT拦截无侵入调试 C# Emit 生成的动态代码的主要内容,如果未能解决你的问题,请参考以下文章

如何激活/使用 JIT 调试?

Windbg非侵入性调试(用户模式)

Visual Studio 2015 Express for Desktop 是不是支持 JIT 调试?

很多.net 程序员不知道又非常重要的 .net高级调试技巧.调试别人的dll方法内的变量

批评我的非侵入式堆调试器

玩好.NET高级调试,你也要会写点汇编