一:背景

1. 讲故事

上篇聊到了 C#程序编译成Native代码 的宏观过程,有粉丝朋友提了一个问题,能不能在 dotnet publish 发布的过程中对AOT编译器拦截进行源码级调试,这是一个好问题,也是深度研究的必经之路,这篇我们就来分享下吧。

二:托管和非托管调试器

1. 调试器介绍

相信大家现在都知道AOT Compiler (ilc.exe) 是用 C# 代码写的,也就表明它是一个托管程序,对托管程序的调试有两种调试器:

  • Visual Studio 托管调试器

调试 C# 代码它是当仁不让,缺点在于对非托管部分的查看缺少了手段。

  • WinDbg 非托管调试器

调试 C/C++ 是一把利器,但用它调试托管的C#代码,虽然可以用,但在变量显示各方面不是很直观。

截个图如下,总之各有利弊吧:

2. 测试代码

为了方便演示,先上一段测试代码,非常简单。


internal class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, World!");
Console.ReadLine();
}
}

有了代码之后,接下来一起观赏下如何通过 Visual Studio 和 Windbg 实现各自的拦截。

三:调试器拦截实战

1. WinDbg 拦截

作为 Windows平台上王者般存在的非托管调试器,用它来劫持ilc.exe非常方便,在注册表的 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\ilc.exe 下配置个 Deubgger 键值即可,截图如下:

接下来使用 dotnet publish 发布程序,稍等片刻之后会看到 windbg 立即启动拦截了 ilc.exe,然后 ctrl+o 打开我们需要下断点的 cs 文件,比如核心的 Compilation 方法,下完断点之后直接 g 执行,截图如下:

从卦中可以看到 Compilation.ctor 果然命中,而且用 dv 也能看到各个局部变量的内存地址,是不是挺有意思的。

总的来说这种方式使用起来简单粗暴,但用 windbg 这种非托管调试器调试C# 总有点 名不正言不顺,更好的方式应该还是用 visual studio 这种专业级的家宝,不是吗?

2. Visual Studio 拦截

上一篇文章跟大家说过执行 dotnet publish 调用的ilc.exe 是来自于目录 .nuget\packages\runtime.win-x64.microsoft.dotnet.ilcompiler\8.0.8\tools 下的,截图如下:

为了能够用上托管调试器,这里我们把 ilc.sln 项目手工编译出一个 ilc.exe 来替换这里的 ilc.exe 即可,截图如下:

为了能够让 VS 附加到 ilc.exe 进程上,ilc 提供了一个 --waitfordebugger 参数,参考如下:


PS D:\csharpapplication\21.20240910\src\Example\Example_21_1> ilc -h
Description:
.NET Native IL Compiler Usage:
ilc <input-file-path>... [options] Arguments:
<input-file-path> Input file(s) Options:
-?, -h, --help Show help and usage information
...
--waitfordebugger Pause to give opportunity to attach debugger

这个参数的作用就是通过 Console.ReadLine 让程序暂停,好让你用 VS 去 Attach ,源码中是这么写的:


public Program(ILCompilerRootCommand command)
{
_command = command; if (Get(command.WaitForDebugger))
{
Console.WriteLine("Waiting for debugger to attach. Press ENTER to continue");
Console.ReadLine();
}
}

但在我手工编译的 ilc.exe 上用 Console.ReadLine 貌似拦不住,所以这里稍微改一下,参考如下:


public Program(ILCompilerRootCommand command)
{
_command = command; while (!Debugger.IsAttached)
{
Console.WriteLine("Waiting for debugger to attach. Press ENTER to continue");
//Console.ReadLine();
Thread.Sleep(1000);
}
}

接下来重新编译项目,将生成目录 runtime\artifacts\bin\coreclr\windows.x64.Debug\ilc\net8.0 下的所有文件复制到nugut目录 .nuget\packages\runtime.win-x64.microsoft.dotnet.ilcompiler\8.0.8\tools 下,截图如下:

一切都准备好之后,接下来使用 dotnet publish 重新发布程序,从 cmd 输出中可以看到正在等待 attach 附加。


PS D:\csharpapplication\21.20240910\src\Example\Example_21_1> dotnet publish -r win-x64 -c Debug -o D:\testdump
正在确定要还原的项目…
所有项目均是最新的,无法还原。
Example_21_1 -> D:\csharpapplication\21.20240910\src\Example\Example_21_1\bin\Debug\net8.0\win-x64\Example_21_1.d
ll
Generating native code
Waiting for debugger to attach. Press ENTER to continue
Waiting for debugger to attach. Press ENTER to continue
Waiting for debugger to attach. Press ENTER to continue
Waiting for debugger to attach. Press ENTER to continue
Waiting for debugger to attach. Press ENTER to continue

在VS菜单上 Debug -> Attach to Process 到我们的 ilc.exe 进程,可以看到果然就命中了,大家看看这调试体验是不是高了很多,截图如下:

体验过这种方式的朋友我相信又有一些新的问题,那就是重复调试的时候太麻烦了,能不能直接以 启动程序 的方式来调试?这就是接下来我们要聊的。

3. VS 对ilc的启动调试

看过上篇的朋友知道,每一次AOT编译之前在 native 目录下都会有一个 xxx.ilc.rsp ,这个文件是 AOT Compiler 的 input 来源,截图如下:

所以完全可以将它作为 ilc.sln 项目的启动参数,接下来我们将 @D:\csharpapplication\21.20240910\src\Example\Example_21_1\obj\Debug\net8.0\win-x64\native\Example_21_1.ilc.rsp 放到 ILCompiler 项目的 command line 中,截图如下:

配置好之后,接下来把 Example_21_1.ilc.rsp 中的 Example_21_1.dll,Example_21_1.obj,Example_21_1.def 三块都改成全路径,参考如下:


D:\csharpapplication\21.20240910\src\Example\Example_21_1\obj\Debug\net8.0\win-x64\Example_21_1.dll
-o:D:\csharpapplication\21.20240910\src\Example\Example_21_1\obj\Debug\net8.0\win-x64\native\Example_21_1.obj
-r:C:\Users\Administrator\.nuget\packages\microsoft.netcore.app.runtime.win-x64\8.0.8\runtimes\win-x64\lib\net8.0\WindowsBase.dll
-r:C:\Users\Administrator\.nuget\packages\runtime.win-x64.microsoft.dotnet.ilcompiler\8.0.8\sdk\System.Private.CoreLib.dll
...
--targetarch:x64
--dehydrate
-g
--exportsfile:D:\csharpapplication\21.20240910\src\Example\Example_21_1\obj\Debug\net8.0\win-x64\native\Example_21_1.def
...

直接 F5 启动 ILCompiler 项目,可以看到轻轻松松的成功调试,这种方式就很好的解决了反复调试的问题,截图如下:

三:总结

以劫持的方式对 AOT Compiler 自身进行源码级调试,这本身就是一个很有意思的话题,不断的介入Compiler编译的的各个阶段,相信能给大家深度学习AOT提供了一些不寻常的手段。

AOT漫谈专题(第五篇): 如何劫持.NET AOT编译器 进行源码级调试的更多相关文章

  1. PerfView专题 (第五篇):如何寻找 C# 托管内存泄漏

    一:背景 前几篇我们聊的都是 非托管内存泄漏,这一篇我们再看下如何用 PerfView 来排查 托管内存泄漏 ,其实 托管内存泄漏 比较好排查,尤其是用 WinDbg,毕竟C#是带有丰富的元数据,不像 ...

  2. 深入浅出Mybatis系列(五)---TypeHandler简介及配置(mybatis源码篇)

    上篇文章<深入浅出Mybatis系列(四)---配置详解之typeAliases别名(mybatis源码篇)>为大家介绍了mybatis中别名的使用,以及其源码.本篇将为大家介绍TypeH ...

  3. 循序渐进做项目系列(4)迷你QQ篇(2)——视频聊天!(附源码)

    一·效果展示 源码派送:MiniQQ1.1 文字聊天的实现参见:循序渐进做项目系列(3):迷你QQ篇(1)——实现客户端互相聊天 二·服务端设计 对于实现视频聊天而言,服务端最核心的工作就是要构造多媒 ...

  4. SpringBoot 源码解析 (五)----- Spring Boot的核心能力 - 自动配置源码解析

    在上一篇博客中分析了springBoot启动流程,大体的轮廓只是冰山一角.今天就来看一下springBoot的亮点功能:自动化装配功能. 先从@SpringBootApplication开始.在启动流 ...

  5. Tars | 第4篇 Subset路由规则业务分析与源码探索

    目录 前言 1. Subset不是负载均衡 1.1 任务需求 1.2 负载均衡源码结构图 1.3 负载均衡四种调用器 1.4 新增两种负载均衡调用器 1.5 Subset应该是"过滤&quo ...

  6. arcgis api 3.x for js 热力图优化篇-不依赖地图服务(附源码下载)

    前言 关于本篇功能实现用到的 api 涉及类看不懂的,请参照 esri 官网的 arcgis api 3.x for js:esri 官网 api,里面详细的介绍 arcgis api 3.x 各个类 ...

  7. 学不懂Netty?看不懂源码?不存在的,这篇文章手把手带你阅读Netty源码!

    阅读这篇文章之前,建议先阅读和这篇文章关联的内容. 1. 详细剖析分布式微服务架构下网络通信的底层实现原理(图解) 2. (年薪60W的技巧)工作了5年,你真的理解Netty以及为什么要用吗?(深度干 ...

  8. Cocos2d-X3.0 刨根问底(五)----- Node类及显示对象列表源码分析

    上一章 我们分析了Cocos2d-x的内存管理,主要解剖了 Ref.PoolManager.AutoreleasePool这三个类,了解了对象是如何自动释放的机制.之前有一个类 Node经常出现在各种 ...

  9. 设计模式(五)——原型模式(加Spring框架源码分析)

    原型模式 1 克隆羊问题 现在有一只羊 tom,姓名为: tom, 年龄为:1,颜色为:白色,请编写程序创建和 tom 羊 属性完全相同的 10 只羊. 2 传统方式解决克隆羊问题 1) 思路分析(图 ...

  10. Tars | 第2篇 TarsJava SpingBoot启动与负载均衡源码初探

    目录 前言 1. Tars客户端启动 @EnableTarsServer 2. Communicator通信器 3. 客户端的负载均衡调用器LoadBalance 最后 前言 通过源码分析可以得出这样 ...

随机推荐

  1. kimchi – kvm虚拟机网页管理

    参考: https://mangolassi.it/topic/15882/kimchi-kvm-updated-and-better-and-easy-guide-for-kvm-beginners ...

  2. 【转载】 NVIDIA RTX2080ti不支持P2P Access,这是真的么?

    原文地址: http://www.gpus.cn/gpus_list_page_techno_support_content?id=30 ------------------------------- ...

  3. 网络问题解决:Ubuntu连接局域网中Windows主机上的v2r报错:rejected core/proxy/socks: unknown Socks version: 67

    参考: https://github.com/2dust/v2rayN/issues/3916 https://www.eolink.com/news/post/30941.html ======== ...

  4. 构建无服务器数仓(二)Apache DolphinScheduler 集成以及 LOB 粒度资源消费分析

    引言 在数据驱动的世界中,企业正在寻求可靠且高性能的解决方案来管理其不断增长的数据需求.本系列博客从一个重视数据安全和合规性的 B2C 金融科技客户的角度来讨论云上云下混合部署的情况下如何利用亚马逊云 ...

  5. curl可以访问虚拟机资源,但是宿主机浏览器不能访问

    如果想从宿主机访问到虚拟机内的php,需要关闭宿主机的代理,并且设置虚拟机内的防火墙不要屏蔽宿主机的ip. 设置虚拟机防火墙方法: 查找宿主机IP:win+r,输入ipconfig 打开虚拟机,输入s ...

  6. Dialog封装的消息映射(弄了好久终于弄过了,不是静态函数哦,和MFC一样,嘻嘻)

    前面弄的是全局的仿消息映射,现在这是封装到类中的消息映射,一直弄不明白,现在也不太明白,就是今天在看虚函数表的用法视频时有位老师用了个共有体转化全局函数为类成员函数,这就给我指了条明路,这不今晚又来弄 ...

  7. 速通c++

    文章目录 1.什么是c++. 2什么是面向对象,什么又是面向过程. 3.C++的灵魂,c++的类. 4.如何定义一个类. 5.什么是对象. 6.如何定义一个对象. 直接定义: 在堆里面定义. 删除对象 ...

  8. 安装部署harbor

    配置文件中修改内容hostname 和 证书 私钥 需要指定到文件 hostname: hub.rainsc.com # http related config http: # port for ht ...

  9. 【YashanDB知识库】字段加上索引后,SQL查询不到结果

    [标题]字段加上索引后,SQL查询不到结果 [问题分类]索引功能使用 [关键字]索引,SQL查询,时间类型,vachar字符类型 [问题描述]字段加上索引后,SQL查询不到结果. [问题原因分析]当前 ...

  10. Istio实现sidecar自动注入

    Istio实现sidecar自动注入 Sidecar模式 在Sidecar部署方式中,你会为每个应用的容器部署一个伴生容器.对于Service Mesh,Sidecar接管进出应用程序容器的所有网络流 ...