.NET高级调试 | 通过JIT拦截无侵入调试 C# Emit 生成的动态代码
共 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(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
可以看到,全部搞定,非侵入式,🐂