如何调试 C# Emit 生成的动态代码?

简介: 如何调试 C# Emit 生成的动态代码?

首先声明一下,这是一个很深的话题,也是朋友真实遇到的,它用 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上的输出如下:

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,接下来看下命中的断点图:

上面的二条汇编指令就是 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 便是。

相关文章
|
2月前
|
缓存 C# Windows
C#程序如何编译成Native代码
【10月更文挑战第15天】在C#中,可以通过.NET Native和第三方工具(如Ngen.exe)将程序编译成Native代码,以提升性能和启动速度。.NET Native适用于UWP应用,而Ngen.exe则通过预编译托管程序集为本地机器代码来加速启动。不过,这些方法也可能增加编译时间和部署复杂度。
166 2
|
24天前
|
监控 测试技术 C#
C# 一分钟浅谈:GraphQL 错误处理与调试
本文从C#开发者的角度,探讨了GraphQL中常见的错误处理与调试方法,包括查询解析、数据解析、权限验证和性能问题,并提供了代码案例。通过严格模式定义、详细错误日志、单元测试和性能监控等手段,帮助开发者提升应用的可靠性和用户体验。
93 67
|
4月前
|
C# 开发者 Windows
在VB.NET项目中使用C#编写的代码
在VB.NET项目中使用C#编写的代码
63 0
|
2月前
|
C#
C# 图形验证码实现登录校验代码
C# 图形验证码实现登录校验代码
103 2
|
2月前
|
中间件 数据库连接 API
C#数据分表核心代码
C#数据分表核心代码
44 0
|
4月前
|
物联网 C# Windows
看看如何使用 C# 代码让 MQTT 进行完美通信
看看如何使用 C# 代码让 MQTT 进行完美通信
662 0
|
4月前
|
数据安全/隐私保护 C# UED
利用 Xamarin 开展企业级移动应用开发:从用户登录到客户管理,全面演示C#与Xamarin.Forms构建跨平台CRM应用的实战技巧与代码示例
【8月更文挑战第31天】利用 Xamarin 进行企业级移动应用开发能显著提升效率并确保高质量和高性能。Xamarin 的跨平台特性使得开发者可以通过单一的 C# 代码库构建 iOS、Android 和 Windows 应用,帮助企业快速推出产品并保持一致的用户体验。本文通过一个简单的 CRM 示例应用演示 Xamarin 的使用方法,并提供了具体的代码示例。该应用包括用户登录、客户列表显示和添加新客户等功能。此外,还介绍了如何增强应用的安全性、数据持久化、性能优化及可扩展性,从而构建出功能全面且体验良好的移动应用。
60 0
|
4月前
|
前端开发 开发者 Apache
揭秘Apache Wicket项目结构:如何打造Web应用的钢铁长城,告别混乱代码!
【8月更文挑战第31天】Apache Wicket凭借其组件化设计深受Java Web开发者青睐。本文详细解析了Wicket项目结构,帮助你构建可维护的大型Web应用。通过示例展示了如何使用Maven管理依赖,并组织页面、组件及业务逻辑,确保代码清晰易懂。Wicket提供的页面继承、组件重用等功能进一步增强了项目的可维护性和扩展性。掌握这些技巧,能够显著提升开发效率,构建更稳定的Web应用。
119 0
|
4月前
|
前端开发 程序员 API
从后端到前端的无缝切换:一名C#程序员如何借助Blazor技术实现全栈开发的梦想——深入解析Blazor框架下的Web应用构建之旅,附带实战代码示例与项目配置技巧揭露
【8月更文挑战第31天】本文通过详细步骤和代码示例,介绍了如何利用 Blazor 构建全栈 Web 应用。从创建新的 Blazor WebAssembly 项目开始,逐步演示了前后端分离的服务架构设计,包括 REST API 的设置及 Blazor 组件的数据展示。通过整合前后端逻辑,C# 开发者能够在统一环境中实现高效且一致的全栈开发。Blazor 的引入不仅简化了 Web 应用开发流程,还为习惯于后端开发的程序员提供了进入前端世界的桥梁。
504 0
|
4月前
|
C#
C# 跳过值班时间代码逻辑
C# 跳过值班时间代码逻辑
38 0