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

DotNetCore实战

共 4315字,需浏览 9分钟

 ·

2022-04-19 12:27

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


Console.WriteLine("Function Pointer: 0x{0: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: 0x{0:x16}", Marshal.GetFunctionPointerForDelegate(addDelegate).ToInt64());

            Console.WriteLine(addDelegate(1020));
        }
    }

可以看到,我把上面两行侵入式的代码给屏蔽掉了,接下来在 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 intunsigned 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

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


浏览 30
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报