2024年4月

上次把磁性窗体的源码开源了,这次就开源另一个程序源码:MP3文件ID3信息编辑器。这个源码也比较简单,关键在于获取和写入MP3文件的这个ID3的信息即可。

1、
项目目录;

2、
源码介绍;

这个操作信息编辑的就封装在MP3ID3.bas文件中。

这里定义了类型ID3,因为ID3v1的信息是一个固定长度的文本串,所以通过这个类型我们能够看出来。

Title是该MP3文件的标题,长度是30个字符

Artist是MP3文件的演唱者,长度是30个字符

Album是MP3问价的专辑,长度是30个字符

sYear是MP3文件发布的年份,长度是4个字符

Comments是MP3文件的备注,长度是30个字符

Genre是MP3文件的所属歌曲类型,是一个字节长度

下面的sGenreMatirx矩阵就定义了所有的歌曲类型,具体到程序界面中就是一个下拉列表框,进行选择即可。

程序也比较简单,上面这个是打开文件获取MP3文件ID3v1信息的方法。这里说明下,ID3v1的信息是存放在MP3文件的最末尾端的。

上面这个是写入MP3文件的ID3v1的方法。先判断是否已有信息,如果已有,则直接覆盖,否则就添加。

3、
运行界面;

4、
使用介绍;

打开应用,点击选择文件夹,在左边列表里选择文件,在右边窗口里设置文件的信息。

5、
源码下载;

提供源码下载:
https://download.csdn.net/download/lzhdim/15448807

6、
其它建议;

这个例子只是提供了ID3v1的信息更改,至于其它信息更改请读者自己添加代码。

本文概要介绍了MP3文件的ID3v1的信息编辑程序,下面提供源码下载,希望能对需要的开发者以帮助。

随着跨平台应用的需求不断增长,开发人员需要一种能够在不同操作系统上运行的用户界面(UI)框架。

Avalonia 是一种引人注目的选择。在本文中,我们将深入了解 Avalonia 是什么,它与 WPF 的区别,以及它的 UI 绘制引擎和原理、优点,以及一个简单的示例代码。

Avalonia 是什么?

Avalonia 是一个开源的、跨平台的 UI 框架,旨在为 C# 开发人员提供一种简单且强大的方式来构建桌面应用程序。

借助 Avalonia,开发人员可以使用 XAML 和 C# 来创建应用程序,而这些应用程序可以在 Windows、Linux 和 macOS 等多个操作系统上无缝运行。

Avalonia 提供了高性能、灵活性和易用性,使得开发跨平台应用程序变得更加轻松。

Avalonia 与 WPF 区别

虽然 Avalonia 和 WPF(Windows Presentation Foundation)都是 UI 框架,但它们之间存在一些重要的区别:

  • 跨平台性:WPF 主要面向 Windows 平台,而 Avalonia 则支持多个操作系统,包括 Windows、Linux 和 macOS。这使得 Avalonia 更具有灵活性,能够满足不同操作系统下的应用需求。

  • 依赖性:WPF 是依赖于 Windows 平台的特定组件和库的,而 Avalonia 尽可能地避免了对特定平台的依赖,使得应用程序更容易移植和部署。

  • 开源性:Avalonia 是一个完全开源的项目。Avalonia 的开源性意味着它拥有一个活跃的社区,能够快速响应用户需求并不断改进框架。

Avalonia 画 UI 的原理与引擎是什么?

Avalonia 的 UI 绘制原理基于 SkiaSharp 图形库。

当开发人员使用 XAML 描述用户界面时,Avalonia 将这些 XAML 文件解析成 UI 元素树,并使用 SkiaSharp 来将这些 UI 元素绘制到屏幕上。

SkiaSharp 提供了丰富的绘图 API,能够实现高性能的图形渲染,同时具有跨平台的特性,使得 Avalonia 能够在不同操作系统上实现一致的用户界面。

Avalonia 的优点

Avalonia 作为一个跨平台的 UI 框架,具有以下优点:

  • 跨平台性:Avalonia 支持多个操作系统,使得开发人员可以更广泛地部署他们的应用程序。

  • 灵活性:Avalonia 提供了丰富的 UI 组件和布局选项,使得开发人员能够创建各种各样的用户界面。

  • 性能优化:Avalonia 使用 SkiaSharp 作为其渲染引擎,能够实现高性能的图形渲染,并在不同操作系统上实现一致的用户界面。

  • 开源社区:Avalonia 是一个活跃的开源项目,拥有一个庞大的开发社区,使得开发人员可以在社区中获取支持、分享经验,并参与到框架的开发中来。

Avalonia 的一个简单示例

下面是一个简单的 Avalonia 使用介绍。

首先安装Avalonia UI 模板

dotnet new install Avalonia.Templates

然后安装Avalonia for Visual Studio扩展,步骤如下:

  • 在Visual Studio中,点击
    扩展
    菜单上的
    管理扩展
  • 然后在搜索框中,输入"Avalonia"
  • 点击下载并按照说明进行操作(您需要关闭Visual Studio以完成安装)

打开Visual Studio,创建Avalonia MVVM应用

最后直接运行即可

一、简介
这是我的《
Advanced .Net Debugging
》这个系列的第六篇文章。这篇文章的内容是原书的第二部分的【调试实战】的第四章。这章主要讲的是程序集加载器,比如:CLR 加载器简介、简单的程序集加载故障、加载上下文故障、互用性与 DllNotFoundException 和轻量级代码生成的调试。有了这章内容的学习,对于 CLR 如何加载程序集,加载的上下文和算法会有一个充分的了解。当然,有关程序集加载的错误也会有所调试和验证。高级调试会涉及很多方面的内容,你对 .NET 基础知识掌握越全面、细节越底层,调试成功的几率越大,当我们遇到各种奇葩问题的时候才不会手足无措。

如果在没有说明的情况下,所有代码的测试环境都是 Net 8.0,如果有变动,我会在项目章节里进行说明。好了,废话不多说,开始我们今天的调试工作。

调试环境我需要进行说明,以防大家不清楚,具体情况我已经罗列出来。
操作系统:Windows Professional 10
调试工具:Windbg Preview(Debugger Client:1.2306.1401.0,Debugger engine:10.0.25877.1004)和 NTSD(10.0.22621.2428 AMD64)

下载地址:可以去Microsoft Store 去下载

开发工具:Microsoft Visual Studio Community 2022 (64 位) - Current版本 17.8.3
Net 版本:.Net 8.0
CoreCLR源码:
源码下载



二、调试源码
废话不多说,本节是调试的源码部分,没有代码,当然就谈不上测试了,调试必须有载体。

2.1、ExampleCore_4_1_1


1 usingSystem.Reflection;2 
3 namespaceExampleCore_4_1_14 {5     internal classProgram6 {7         static void Main(string[] args)8 {9             Assembly? assemblyLoadFromContext = null;10             Console.WriteLine("Press any key to load into load from context");11 Console.ReadKey();12 
13             assemblyLoadFromContext = Assembly.LoadFrom("ExampleCore_4_1_1.1.dll");14 
15             var localObject = assemblyLoadFromContext.CreateInstance("ExampleCore_4_1_1._1.SimpleType")!;16 
17             Console.WriteLine("Press any key to Exit");18 Console.ReadKey();19 }20 }21 }

View Code


2.2、ExampleCore_4_1_1.1


1 namespaceExampleCore_4_1_1._12 {3     public classSimpleType4 {5         private int_field1;6         private int_field2;7 
8         public intField19 {10             get { return_field1; }11 }12 
13         public intFields14 {15             get { return_field2; }16 }17 
18         publicSimpleType()19 {20             _field1 = 10;21             _field2 = 5;22 }23 }24 }

View Code


2.3、
ExampleCore_4_1_2
Entity 类的代码


1 namespaceExampleCore_4_1_22 {3 [Serializable]4     public classEntity5 {6         public inta;7 }8 }

View Code

EntityUtil 类的代码


1 namespaceExampleCore_4_1_22 {3 [Serializable]4     public classEntityUtil5 {6         public voidDump(Entity e)7 {8 Console.WriteLine(e.a);9 }10 }11 }

View Code

Program 类的代码


1 usingSystem.Runtime.Loader;2 usingSystem.Runtime.Remoting;3 
4 namespaceExampleCore_4_1_25 {6     internal classProgram7 {8         static void Main(string[] args)9 {10             Program p = newProgram();11 p.Run();12 
13 Console.ReadLine();14 }15 
16         public voidRun()17 {18             while (true)19 {20                 Console.WriteLine("1、Run in default app domain");21                 Console.WriteLine("2、Run in dedicated app domain");22                 Console.WriteLine("Q、To quit.");23                 Console.Write(">");24                 ConsoleKeyInfo consoleKeyInfo =Console.ReadKey();25 Console.WriteLine();26 
27                 if (consoleKeyInfo.KeyChar == '1')28 {29 RunInDefault();30 }31                 else if (consoleKeyInfo.KeyChar == '2')32 {33 RunInDedicated();34 }35                 else if (consoleKeyInfo.KeyChar == 'q' || consoleKeyInfo.KeyChar == 'Q')36 {37                     break;38 }39 }40 }41 
42         public voidRunInDefault()43 {44             EntityUtil t2 = newEntityUtil();45             Entity t = newEntity();46             t.a = 10;47 
48 t2.Dump(t);49 }50 
51         public voidRunInDedicated()52 {53             AssemblyLoadContext assemblyLoadContext = new AssemblyLoadContext("Myload");54             
55             var assembly = assemblyLoadContext.LoadFromAssemblyName(new System.Reflection.AssemblyName("ExampleCore_4_1_2"));56 
57             EntityUtil t2 = (EntityUtil)assembly.CreateInstance("ExampleCore_4_1_2.EntityUtil")!;58 
59             Entity t = newEntity();60             t.a = 10;61 t2.Dump(t);62 }63 }64 }

View Code


2.4、ExampleCore_4_1_3


1 usingSystem.Runtime.InteropServices;2 
3 namespaceExampleCore_4_1_34 {5     internal classProgram6 {7         [DllImport("ExampleCore_4_1_4.dll",  CharSet =CharSet.Auto)]8         public static extern void Alloc(stringstr);9 
10         static void Main(string[] args)11 {12             var str = "hello world";13 
14 Alloc(str);15 
16 Console.ReadLine();17 }18 }19 }

View Code


2.5、ExampleCore_4_1_4(C++)


1 extern "C"
2 {3     __declspec(dllexport) void Alloc(wchar_t*c);4 }5 
6 #include "iostream"
7 #include <Windows.h>
8 
9 using  namespacestd;10 
11 void Alloc(wchar_t*c)12 {13     wprintf(L"%s------\n", c);14 }

View Code


2.6、ExampleCore_4_1_5


1 usingSystem.Diagnostics;2 usingSystem.Reflection.Emit;3 
4 namespaceExampleCore_4_1_55 {6     internal classProgram7 {8         private delegate int Add(int a, intb);9 
10         static void Main(string[] args)11 {12             Type[] mylocalArgs = { typeof(int), typeof(int) };13             DynamicMethod method = new DynamicMethod("Add", typeof(int), mylocalArgs);14             
15             ILGenerator generator =method.GetILGenerator();16 generator.Emit(OpCodes.Ldarg_0);17 generator.Emit(OpCodes.Ldarg_1);18 generator.Emit(OpCodes.Add);19 generator.Emit(OpCodes.Ret);20 
21             Add add = (Add)method.CreateDelegate(typeof(Add));22 Debugger.Break();23             int result = add(1, 2);24 
25             Console.WriteLine($"1+2={result}");26 Console.Read();27 }28 }29 }

View Code


三、基础知识
本章主要介绍 CLR 加载器(代码名称 fusion),以及它是如何避免 DLL 地狱问题和与 CLR 加载器相关的其他一些陷阱。如果大家不知道 DLL Hell 是什么问题的,可以自行去网上查找学习。

3.1、CLR 加载器简介
从宏观来看,程序集分为两类,一种是:私有程序集,这类程序集是属于某个特定应用程序的,它们通常保存在与应用程序本身路径相同的文件中。第二种是:共享程序集,这类程序集是由同一台机器上的两个或者更多个应用程序使用的程序集。共享程序集会被安装到全局程序集缓存中(GAC)。
在面对不同类型的程序集时,CLR 加载器如何判断从什么位置加载程序集呢?我们直接上一个图来说明问题。

在请求加载一个程序集的时候,CLR 加载器将判断加载上下文,并且根据上下文采取不同的算法来判断程序集的位置。

3.1.1、程序集标识
程序集的标识定义了程序集的唯一性。当 CLR 加载器要加载程序集的时候,这个标识是非常重要的。接下来,我们看看程序集标识由什么组成。
程序集标识由以下关键部分组成:
程序集的名称
:程序集简单的名称,通常是程序集的文件名,不包含扩展名 .exe 或者 .dll。
文化
:程序集的目标地域(例如:中性文化=neutral)。
版本
:表示程序集的版本,形如:<major>.<minor>.<build>.<revision>。
公钥
:公钥属性用于强命名的程序集,包含了公钥的64位散列码,这个公钥与用于对程序集签名的私钥是相对应的。
处理器架构:
在 CLR 2.0 中引入的处理器架构属性指定了程序集的目标处理器架构。

由以上5个属性合在一起构成了程序集标识。在 CLR 中广泛的使用程序集标识来判断被加载的程序集是否正确。
需要注意的是,就程序集标识而言,当加载正确的程序集时,程序集的路径并不是 CLR 考虑的因素。
如果两个相同的程序集位于不同的路径中,那它们也可以加载到同一个应用程序域中,只是使用了不同的加载上下文。即使这两个程序集是相同的,它们仍然被视为两个不同的程序集。当然。在程序集中包含相同的类型同样被认为是不同的。


3.1.2、全局程序集缓存
GAC 是保存所有共享程序集的常用位置。GAC 位于:%windir%\assembly,我的实际目录地址是:C:\Windows\assembly,效果如图:

在 GAC 中的程序集必须是强命名的,并且拥有唯一的版本号。在共享程序集加载的过程中(在默认的加载上下文中),CLR 加载器会首先检查 GAC 来确定这个程序集是否存在,然后再查看私有的加载路径。
我们可以使用【Windbg Preview】工具,使用【lm f】命令查看目标进程中所有已加载的模块以及它们相应的加载路径。这个很简单,直接贴结果。这个里面,我使用【lm】和【lm f】两个命令,大家可以自己体会区别。

1 0:006>lm2 start             end                 module name3 000001d5`2a100000 000001d5`2a108000   ExampleCore_4_1_0 C (service symbols: CLR Symbols with PDB: C:\ProgramData\Dbg\sym\ExampleCore_4_1_0.pdb\E55CC9E3404548D5AF090BFC4FBE04D01\ExampleCore_4_1_0.pdb)4 000001d5`2a110000 000001d5`2a11e000   System_Runtime   (deferred)5 000001d5`2a120000 000001d5`2a128000   ExampleCore_4_1_0_Clone   (deferred)6 000001d5`2a130000 000001d5`2a138000   System_Text_Encoding_Extensions   (deferred)7 00007ff6`5f210000 00007ff6`5f239000   apphost  C (privatepdb symbols)  C:\ProgramData\Dbg\sym\apphost.pdb\5633DAB747FE452D91289F0AE5A53DEB1\apphost.pdb8 00007ffc`00b00000 00007ffc`00b28000   System_Console   (deferred)9 00007ffc`00b30000 00007ffc`00ce9000   clrjit     (deferred)10 00007ffc`00cf0000 00007ffc`0197c000   System_Private_CoreLib   (service symbols: CLR Symbols with PDB: C:\ProgramData\Dbg\sym\System.Private.CoreLib.pdb\44580BF6DF5DBDE0A6AF2B06590C0AF21\System.Private.CoreLib.pdb)11 00007ffc`01e80000 00007ffc`02366000coreclr    (deferred)12 00007ffc`0237000000007ffc`023d4000   hostpolicy   (deferred)13 00007ffc`06f10000 00007ffc`06f69000   hostfxr    (deferred)14 00007ffc`0dff0000 00007ffc`0e005000   System_Runtime_InteropServices   (deferred)15 00007ffc`0e1a0000 00007ffc`0e1b2000   System_Threading   (deferred)16 00007ffc`daa10000 00007ffc`daaad000   msvcp_win   (deferred)17 00007ffc`dab30000 00007ffc`dab42000   kernel_appcore   (deferred)18 00007ffc`dac00000 00007ffc`dad00000   ucrtbase   (deferred)19 00007ffc`dad00000 00007ffc`dad82000   bcryptPrimitives   (deferred)20 00007ffc`dae90000 00007ffc`dafa7000   gdi32full   (deferred)21 00007ffc`db000000 00007ffc`db027000   bcrypt     (deferred)22 00007ffc`db0b0000 00007ffc`db0d2000   win32u     (deferred)23 00007ffc`db240000 00007ffc`db536000   KERNELBASE   (deferred)24 00007ffc`db540000 00007ffc`db893000   combase    (deferred)25 00007ffc`db920000 00007ffc`db952000   IMM32      (deferred)26 00007ffc`db9c0000 00007ffc`dc12b000   SHELL32    (deferred)27 00007ffc`dc1e0000 00007ffc`dc290000   ADVAPI32   (deferred)28 00007ffc`dc460000 00007ffc`dc58b000   ole32      (deferred)29 00007ffc`dc680000 00007ffc`dc6ab000   GDI32      (deferred)30 00007ffc`dc830000 00007ffc`dc8fd000   OLEAUT32   (deferred)31 00007ffc`dd020000 00007ffc`dd0c0000   sechost    (deferred)32 00007ffc`dd0c0000 00007ffc`dd25e000   USER32     (deferred)33 00007ffc`dd380000 00007ffc`dd4a5000   RPCRT4     (deferred)34 00007ffc`dd530000 00007ffc`dd5ed000   KERNEL32   (pdb symbols)          C:\ProgramData\Dbg\sym\kernel32.pdb\B07C97792B439ABC0DF83499536C7AE51\kernel32.pdb35 00007ffc`dd5f0000 00007ffc`dd68e000   msvcrt     (deferred)36 00007ffc`dd6d0000 00007ffc`dd8c8000   ntdll      (pdb symbols)          C:\ProgramData\Dbg\sym\ntdll.pdb\90C2362B9D1F1F5088ABFA3BDE69BAAF1\ntdll.pdb37 
38 0:006>lm f39 start             end                 module name40 000001d5`2a100000 000001d5`2a108000   ExampleCore_4_1_0 E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_4_1_0\bin\Debug\net8.0\ExampleCore_4_1_0.dll41 000001d5`2a110000 000001d5`2a11e000   System_Runtime C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.3\System.Runtime.dll42 000001d5`2a120000 000001d5`2a128000   ExampleCore_4_1_0_Clone E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_4_1_0\bin\Debug\net8.0\ExampleCore_4_1_0_Clone.dll43 000001d5`2a130000 000001d5`2a138000   System_Text_Encoding_Extensions C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.3\System.Text.Encoding.Extensions.dll44 00007ff6`5f210000 00007ff6`5f239000   apphost  apphost.exe45 00007ffc`00b00000 00007ffc`00b28000   System_Console C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.3\System.Console.dll46 00007ffc`00b30000 00007ffc`00ce9000   clrjit   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.3\clrjit.dll47 00007ffc`00cf0000 00007ffc`0197c000   System_Private_CoreLib C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.3\System.Private.CoreLib.dll48 00007ffc`01e80000 00007ffc`02366000   coreclr  C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.3\coreclr.dll49 00007ffc`02370000 00007ffc`023d4000   hostpolicy C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.3\hostpolicy.dll50 00007ffc`06f10000 00007ffc`06f69000   hostfxr  C:\Program Files\dotnet\host\fxr\8.0.3\hostfxr.dll51 00007ffc`0dff0000 00007ffc`0e005000   System_Runtime_InteropServices C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.3\System.Runtime.InteropServices.dll52 00007ffc`0e1a0000 00007ffc`0e1b2000   System_Threading C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.3\System.Threading.dll53 00007ffc`daa10000 00007ffc`daaad000   msvcp_win C:\Windows\System32\msvcp_win.dll54 00007ffc`dab30000 00007ffc`dab42000   kernel_appcore C:\Windows\System32\kernel.appcore.dll55 00007ffc`dac00000 00007ffc`dad00000   ucrtbase C:\Windows\System32\ucrtbase.dll56 00007ffc`dad00000 00007ffc`dad82000   bcryptPrimitives C:\Windows\System32\bcryptPrimitives.dll57 00007ffc`dae90000 00007ffc`dafa7000   gdi32full C:\Windows\System32\gdi32full.dll58 00007ffc`db000000 00007ffc`db027000   bcrypt   C:\Windows\System32\bcrypt.dll59 00007ffc`db0b0000 00007ffc`db0d2000   win32u   C:\Windows\System32\win32u.dll60 00007ffc`db240000 00007ffc`db536000   KERNELBASE C:\Windows\System32\KERNELBASE.dll61 00007ffc`db540000 00007ffc`db893000   combase  C:\Windows\System32\combase.dll62 00007ffc`db920000 00007ffc`db952000   IMM32    C:\Windows\System32\IMM32.DLL63 00007ffc`db9c0000 00007ffc`dc12b000   SHELL32  C:\Windows\System32\SHELL32.dll64 00007ffc`dc1e0000 00007ffc`dc290000   ADVAPI32 C:\Windows\System32\ADVAPI32.dll65 00007ffc`dc460000 00007ffc`dc58b000   ole32    C:\Windows\System32\ole32.dll66 00007ffc`dc680000 00007ffc`dc6ab000   GDI32    C:\Windows\System32\GDI32.dll67 00007ffc`dc830000 00007ffc`dc8fd000   OLEAUT32 C:\Windows\System32\OLEAUT32.dll68 00007ffc`dd020000 00007ffc`dd0c0000   sechost  C:\Windows\System32\sechost.dll69 00007ffc`dd0c0000 00007ffc`dd25e000   USER32   C:\Windows\System32\USER32.dll70 00007ffc`dd380000 00007ffc`dd4a5000   RPCRT4   C:\Windows\System32\RPCRT4.dll71 00007ffc`dd530000 00007ffc`dd5ed000   KERNEL32 C:\Windows\System32\KERNEL32.DLL72 00007ffc`dd5f0000 00007ffc`dd68e000   msvcrt   C:\Windows\System32\msvcrt.dll73 00007ffc`dd6d0000 00007ffc`dd8c8000   ntdll    ntdll.dll   

当使用 Windows 资源管理器查看 GAC 时,它是 GAC 的视图版本,它是由 shell 扩展 shfusion.dll 实现的。

3.1.3、默认加载上下文
大多数程序集都是使用默认加载上下文(Default load context),也是最安全的加载方式,可以避免加载不正确的程序集。
使用默认加载上下文的程序集是通过 Assembly.Load 及其变化形式来加载的。在默认的加载上下文中,CLR 要执行所有的探测逻辑,从而保证加载正确版本的程序集。
如图:

这样做有一个好处,依赖的程序集也可以在默认加载上下文中自动被找到。这与指定加载上下文(Load-From)或者无加载上下文(Load-Without)是不同的,在这些上下文中,调用者可以显示的选择程序集,因此,更容易出现错误。
当一个程序集加载请求进入默认加载上下文时,CLR 会首先判断这个程序集是否在 GAC 中。如果是,则直接加载 GAC 中正确版本的程序集。如果不在 GAC 中,CLR 加载器将探测其他路径,包括应用程序所在的路径和私有二进制文件所在的路径等。如果在这两个位置中的某一个找到了程序集,CLR 则直接从该位置加载程序集。

3.1.4、指定加载上下文
当一个程序集被加载到指定加载上下文中时,通常是通过 Assembly.LoadFrom 等 API 的某种变化形式来实现的,此时,CLR 不会执行探测逻辑,而是由调用者负责消除程序集的冲突。
程序集依赖的所有其他程序集将从同一路径加载。
被加载到指定加载上下文中的程序集可以使用默认加载上下文中的程序集。
使用指定加载上下文时有些需要注意的,如图:


3.1.5、无加载上下文
有些程序集的加载是不需要上下文的,比如:一些通过 Reflection 命令空间和 Emit 命令空间生成的程序集。在这种情况下,CLR 是不做任何探测的。

3.2、简单的程序集加载故障
如果我们想查看 CLR 加载器在加载程序集的时候使用的探测算法,可以使用【fuslogvw.exe】工具,我们打开【Visual Studio 2022 Developer Command Prompt v17.9.4】命令行工具,直接输入【fuslogvw】,就可以打开,该文件的路径在:C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools\x64。
如图:

打开【程序集绑定日志查看器】,效果如图:

主界面分为两个部分,第一部分包含了实际的日志项目,共三列:
应用程序【Application】:该列给出了日志项是来之哪个应用程序。
说明【Description】:该列给出日志项的描述信息。
日期/时间【Date/Time】:该列给出了日志项的时间戳。
在主界面的右侧有一组按钮,控制日志项和如何执行日志。【设置(Setting)】按钮控制着如何执行日志记录以及何时执行。如图:

在默认情况下,日志是禁用的。
已禁用日志:日志功能不启用。
记录异常文本【Log in Exception Text】:程序集绑定操作会被记录在异常中。
记录失败绑定到磁盘【Log Bind failures to Disk】:只有发生故障的时候才会记录到磁盘上。
记录所有绑定到磁盘【Log All Binds to Disk】:所有绑定操作都会被记录到磁盘上。
启用自定义日志路径【Enable Custom Log Path】:改变日志文件的保存路径。如果选择了改变日志路径,必须在主界面上【自定义日志路径(Custom Log Location)】中输入日志的新路径。

其实,这些设置都是通过注册表实现的,具体事项如下:

HKLM 表示的是【HKEY_LOCAL_MACHINE】,完整路径如下:HKEY_LOCAL_MACHINE\Software\Microsoft\Fusion。
我们在【眼见为实】中要演示如何查看 CLR 加载器加载程序集时的探测算法中,使用的是【记录失败绑定到磁盘(Log Bind failures to Disk)】这个选项。


眼见为实:
调试源码:ExampleCore_4_1_1(控制台程序)和 ExampleCore_4_1_1.1(类库项目)
调试任务:演示程序集加载故障和CLR 加载程序集的探测算法
开始之前,先说明操作过程,我们首先将 ExampleCore_4_1_1.1 类库项目编译好,并将 dll 文件拷贝到 ExampleCore_4_1_1 项目的 bin 目录下,并将 ExampleCore_4_1_1.1.dll 文件的后缀名改成 ExampleCore_4_1_1.1.old,这样就可以出错了,用于测试。然后我们再编译 ExampleCore_4_1_1 控制台项目,就可以开始我们的测试了。
A、NTSD 调试
在开始之前,说明一下,在这里调试,就不验证 CLR 加载器的探测算法了,因为了在 Windbg Preview 调试环节做过了,内容都是一样的。
编译项目,打开【Visual Studio 2022 Developer Command Prompt v17.9.4】命令行工具,输入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_4_1_1\bin\Debug\net8.0\ExampleCore_4_1_1.exe】,打开调试器。
我们继续使用【g】命令运行调试器,直到调试器输出【Press any key to load into load from context】,然后我们随便按一个键,继续运行调试器,直到程序抛出异常,调试器也进入了中断模式。

调试器进入中断模式,如图:

我们使用【kb】命令查看线程的调用栈,并展示了每个方法的前三个参数。

1 0:000>kb2 RetAddr               : Args to Child                                                           : Call Site3 00007ffa`b957b3a3     : 000002a1`3cc0a090 000000c0`88d7e730 000000c0`88d7ed60 000002a1`3cc09e08 : KERNELBASE!RaiseException+0x69
4 00007ffa`b94c9cfc     : 000002a1`388bc9a0 000000c0`88d7ea58 000002a1`3889c3c0 000002a1`3a2fa630 : coreclr!RaiseTheExceptionInternalOnly+0x26b
5 00007ffa`b964cc41     : 000000c0`88d7ea58 000002a1`3889c3c0 000000c0`88d7eaa0 00007ffa`b9947940 : coreclr!UnwindAndContinueRethrowHelperAfterCatch+0x38
6 00007ffa`b8ba5ea3     : 000002a1`3cc09cec 000002a1`3cc09e80 00000000`00000000 000000c0`88d7ea58 : coreclr!AssemblyNative_LoadFromPath+0x181fb1
7 00007ffa`b8bcd4ab     : 000002a1`3cc09e80 000002a1`3cc09ce0 000002a1`3cc09de8 00000000`00000002 : System_Private_CoreLib!System.Reflection.Assembly System.Runtime.Loader.AssemblyLoadContext::LoadFromAssemblyPath(System.String)$##600462A+0xf3
8 00007ffa`59ab1992     : 000002e1`cef00510 00000000`00000001 000002a1`388a3380 000002a1`3cc09c38 : System_Private_CoreLib!System.Reflection.Assembly System.Reflection.Assembly::LoadFrom(System.String)$##6006760+0x17b
9 00007ffa`b961b8c3     : 000002a1`3cc08ea0 000000c0`88d7f1e8 000000c0`88d7f1e8 000000c0`88d7edd9 : 0x00007ffa`59ab199210 00007ffa`b9550b19     : 00000000`00000000 00000000`00000130 000000c0`88d7ede8 00007ffa`b94d0232 : coreclr!CallDescrWorkerInternal+0x83
11 (Inline Function)     : --------`-------- --------`-------- --------`-------- --------`-------- : coreclr!CallDescrWorkerWithHandler+0x56
12 00007ffa`b954d730     : 000000c0`88d7ee68 00000000`00000000 00000000`00000048 00007ffa`b95bd046 : coreclr!MethodDescCallSite::CallTargetWorker+0x2a1
13 (Inline Function)     : --------`-------- --------`-------- --------`-------- --------`-------- : coreclr!MethodDescCallSite::Call+0xb
14 00007ffa`b9572fc6     : 000002a1`3cc08ea0 000002a1`3cc08ea0 00000000`00000000 000000c0`88d7f1e8 : coreclr!RunMainInternal+0x11c
15 00007ffa`b95732fb     : 000002a1`388a3380 000002a1`00000000 000002a1`388a3380 00000000`00000000 : coreclr!RunMain+0xd2
16 00007ffa`b94c9141     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000130 : coreclr!Assembly::ExecuteMainMethod+0x1bf
17 00007ffa`b95de8b8     : 00000000`00000001 000000c0`88d7f401 000000c0`88d7f410 00007ffb`788a23ea : coreclr!CorHost2::ExecuteAssembly+0x281
18 00007ffb`788c2b76     : 000002a1`38871980 000002a1`38871680 00000000`00000000 000002a1`38871680 : coreclr!coreclr_execute_assembly+0xd8
19 (Inline Function)     : --------`-------- --------`-------- --------`-------- --------`-------- : hostpolicy!coreclr_t::execute_assembly+0x2a
20 00007ffb`788c2e5c     : 000002a1`3885c358 000000c0`88d7f639 00007ffb`788fc9c0 000002a1`3885c358 : hostpolicy!run_app_for_context+0x596
21 00007ffb`788c379a     : 00000000`00000000 000002a1`3885c350 000002a1`3885c350 00000000`00000000 : hostpolicy!run_app+0x3c
22 00007ffb`7891b5c9     : 000002a1`388707a8 000002a1`38870690 00000000`00000000 000000c0`88d7f739 : hostpolicy!corehost_main+0x15a
23 00007ffb`7891e066     : 000002a1`3886f6a0 000000c0`88d7fac0 00000000`00000000 00000000`00000000 : hostfxr!execute_app+0x2e9
24 00007ffb`789202ec     : 00007ffb`789525f8 000002a1`3886df30 000000c0`88d7fa00 000000c0`88d7f9b0 : hostfxr!`anonymous namespace'::read_config_and_execute+0xa6
25 00007ffb`7891e644     : 000000c0`88d7fac0 000000c0`88d7fae0 000000c0`88d7fa31 000002a1`3886e350 : hostfxr!fx_muxer_t::handle_exec_host_command+0x16c
26 00007ffb`789185a0     : 000000c0`88d7fae0 000002a1`3886d830 00000000`00000001 000002a1`38850000 : hostfxr!fx_muxer_t::execute+0x494
27 *** WARNING: Unable to verify checksum forapphost.exe28 00007ff7`bff4f998     : 00007ffb`8b0ef4e8 00007ffb`78919b10 000000c0`88d7fc80 000002a1`3886d520 : hostfxr!hostfxr_main_startupinfo+0xa0
29 00007ff7`bff4fda6     : 00007ff7`bff5b6c0 00000000`00000007 000002a1`3885c350 00000000`0000005e : apphost!exe_start+0x878
30 00007ff7`bff512e8     : 00000000`00000000 00000000`00000000 000002a1`3885c350 00000000`00000000 : apphost!wmain+0x146
31 (Inline Function)     : --------`-------- --------`-------- --------`-------- --------`-------- : apphost!invoke_main+0x22
32 00007ffb`8bda7344     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : apphost!__scrt_common_main_seh+0x10c
33 00007ffb`8d7626b1     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : KERNEL32!BaseThreadInitThunk+0x14
34 00000000`00000000     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x21

KERNELBASE!RaiseException
方法的第一个参数的地址就是异常对象的地址,这个地址就是
000002a1`3cc0a090
。有了地址,我们可以使用【!PrintException
000002a1`3cc0a090
】命令,打印出异常的具体信息。

1 0:000> !PrintException 000002a1`3cc0a0902 WARNING: SOS needs to be upgraded for thisversion of the runtime. Some commands may not work correctly.3 For more information see https://go.microsoft.com/fwlink/?linkid=2135652
4 
5 Exception object: 000002a13cc0a0906 Exception type:   System.IO.FileNotFoundException7 Message:8 InnerException:   <none>
9 StackTrace (generated):10 SP               IP               Function11     000000C088D7E990 00007FFAB8BA5EA2 System_Private_CoreLib!System.Runtime.Loader.AssemblyLoadContext.LoadFromAssemblyPath(System.String)+0xf2
12     000000C088D7EAB0 00007FFAB8BCD4AA System_Private_CoreLib!System.Reflection.Assembly.LoadFrom(System.String)+0x17a
13     000000C088D7EB30 00007FFA59AB1991 ExampleCore_4_1_1!ExampleCore_4_1_1.Program.Main(System.String[])+0x61
14 
15 StackTraceString: <none>
16 HResult: 80070002

我们看到了具体的异常就是
System.IO.FileNotFoundException 对象,错误码是 80070002。


B、Windbg Preview 调试
所有准备工作完成后,我们打开【Windbg Preview】调试器,依次点击【文件】--->【Launch executable】,加载我们的项目文件:ExampleCore_4_1_1.exe。进入调试器后,直接使用【g】命令运行调试器,直到我们的控制台程序输出【Press any key to load into load from context】文本,我们按任意键,继续运行。此时,系统发生异常,调试器也中断执行。
调试器输出如下:

1 0:000>g2 ModLoad: 00007ffa`7fd80000 00007ffa`7fdb2000   C:\Windows\System32\IMM32.DLL3 ModLoad: 00007ffa`47080000 00007ffa`470d9000   C:\Program Files\dotnet\host\fxr\8.0.3\hostfxr.dll4 ModLoad: 00007ffa`33b70000 00007ffa`33bd4000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.3\hostpolicy.dll5 ModLoad: 00007ff9`bcc60000 00007ff9`bd146000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.3\coreclr.dll6 ModLoad: 00007ffa`8060000000007ffa`8072b000   C:\Windows\System32\ole32.dll7 ModLoad: 00007ffa`8016000000007ffa`804b3000   C:\Windows\System32\combase.dll8 ModLoad: 00007ffa`804c0000 00007ffa`8058d000   C:\Windows\System32\OLEAUT32.dll9 ModLoad: 00007ffa`7eb80000 00007ffa`7ec02000   C:\Windows\System32\bcryptPrimitives.dll10 (3608.614): Unknown exception - code 04242420(first chance)11 ModLoad: 00007ff9`952c0000 00007ff9`95f4c000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.3\System.Private.CoreLib.dll12 ModLoad: 00007ff9`ae470000 00007ff9`ae629000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.3\clrjit.dll13 ModLoad: 00007ffa`7e7d0000 00007ffa`7e7e2000   C:\Windows\System32\kernel.appcore.dll14 ModLoad: 000001b1`09ba0000 000001b1`09ba8000   E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_4_1_1\bin\Debug\net8.0\ExampleCore_4_1_1.dll15 ModLoad: 000001b1`09bb0000 000001b1`09bbe000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.3\System.Runtime.dll16 ModLoad: 00007ffa`49890000 00007ffa`498b8000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.3\System.Console.dll17 ModLoad: 00007ffa`47060000 00007ffa`47072000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.3\System.Threading.dll18 ModLoad: 000001b1`09bc0000 000001b1`09bc8000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.3\System.Text.Encoding.Extensions.dll19 ModLoad: 00007ffa`47040000 00007ffa`47055000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.3\System.Runtime.InteropServices.dll20 (3608.614): C++ EH exception - code e06d7363 (first chance)21 (3608.614): CLR exception - code e0434352 (first chance)22 (3608.614): CLR exception - code e0434352 (!!! second chance !!!)23 KERNELBASE!RaiseException+0x69:24 00007ffa`7eeccf19 0f1f440000      nop     dword ptr [rax+rax]

当有任何 CLR 异常发生时,调试器都将停止执行。
KERNELBASE!RaiseException
这个方法抛出了异常

,我们使用【kb】命令查看线程的堆栈帧,并显示传递给堆栈跟踪中每个函数的前三个参数。

1 0:000>kb2 # RetAddr               : Args to Child                                                           : Call Site3 00 00007ff9`bcd1b3a3     : 000001b1`0c80a000 000000ac`9837e720 000000ac`9837ed50 000001b1`0c809dc0 : KERNELBASE!RaiseException+0x69
4 01 00007ff9`bcc69cfc     : 000001b1`082dbdb0 000000ac`9837ea48 000001b1`082c52e0 000001b1`082dcdc0 : coreclr!RaiseTheExceptionInternalOnly+0x26b [D:\a\_work\1\s\src\coreclr\vm\excep.cpp @ 2795]5 02 00007ff9`bcdecc41     : 000000ac`9837ea48 000001b1`082c52e0 000000ac`9837ea90 00007ff9`bd0e7940 : coreclr!UnwindAndContinueRethrowHelperAfterCatch+0x38 [D:\a\_work\1\s\src\coreclr\vm\excep.cpp @ 7726]6 03 00007ff9`95635ea3     : 000001b1`0c809cec 000001b1`0c809e38 00000000`00000000 000000ac`9837ea48 : coreclr!AssemblyNative_LoadFromPath+0x181fb1 [D:\a\_work\1\s\src\coreclr\vm\assemblynative.cpp @ 221]7 04 00007ff9`9565d4ab     : 000001b1`0c809e38 000001b1`0c809ce0 000001b1`0c809da0 00000000`00000002 : System_Private_CoreLib!System.Runtime.Loader.AssemblyLoadContext.LoadFromAssemblyPath+0xf3 [/_/src/libraries/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyLoadContext.cs @ 347]8 05 00007ff9`5d241992     : 000001f1`9e840510 00000000`00000001 000001b1`082cc500 000001b1`0c809c38 : System_Private_CoreLib!System.Reflection.Assembly.LoadFrom+0x17b [/_/src/libraries/System.Private.CoreLib/src/System/Reflection/Assembly.cs @ 374]9 06 00007ff9`bcdbb8c3     : 000001b1`0c808ea0 000000ac`9837f1d8 000000ac`9837f1d8 000000ac`9837edc9 : ExampleCore_4_1_1!ExampleCore_4_1_1.Program.Main+0x62 [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_4_1_1\Program.cs @ 13]10 07 00007ff9`bccf0b19     : 00000000`00000000 00000000`00000130 000000ac`9837edd8 00007ff9`bcc70232 : coreclr!CallDescrWorkerInternal+0x83 [D:\a\_work\1\s\src\coreclr\vm\amd64\CallDescrWorkerAMD64.asm @ 100]11 08 (Inline Function)     : --------`-------- --------`-------- --------`-------- --------`-------- : coreclr!CallDescrWorkerWithHandler+0x56 [D:\a\_work\1\s\src\coreclr\vm\callhelpers.cpp @ 67]12 09 00007ff9`bcced730     : 000000ac`9837ee58 00000000`00000000 00000000`00000048 00007ff9`bcd5d046 : coreclr!MethodDescCallSite::CallTargetWorker+0x2a1 [D:\a\_work\1\s\src\coreclr\vm\callhelpers.cpp @ 570]13 0a (Inline Function)     : --------`-------- --------`-------- --------`-------- --------`-------- : coreclr!MethodDescCallSite::Call+0xb [D:\a\_work\1\s\src\coreclr\vm\callhelpers.h @ 458]14 0b 00007ff9`bcd12fc6     : 000001b1`0c808ea0 000001b1`0c808ea0 00000000`00000000 000000ac`9837f1d8 : coreclr!RunMainInternal+0x11c [D:\a\_work\1\s\src\coreclr\vm\assembly.cpp @ 1304]15 0c 00007ff9`bcd132fb     : 000001b1`082cc500 000001b1`00000000 000001b1`082cc500 00000000`00000000 : coreclr!RunMain+0xd2 [D:\a\_work\1\s\src\coreclr\vm\assembly.cpp @ 1375]16 0d 00007ff9`bcc69141     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000130 : coreclr!Assembly::ExecuteMainMethod+0x1bf [D:\a\_work\1\s\src\coreclr\vm\assembly.cpp @ 1504]17 0e 00007ff9`bcd7e8b8     : 00000000`00000001 000000ac`9837f401 000000ac`9837f400 00007ffa`33b723ea : coreclr!CorHost2::ExecuteAssembly+0x281 [D:\a\_work\1\s\src\coreclr\vm\corhost.cpp @ 349]18 0f 00007ffa`33b92b76     : 000001b1`0829b9a0 000001b1`0829b6a0 00000000`00000000 000001b1`0829b6a0 : coreclr!coreclr_execute_assembly+0xd8 [D:\a\_work\1\s\src\coreclr\dlls\mscoree\exports.cpp @ 504]19 10 (Inline Function)     : --------`-------- --------`-------- --------`-------- --------`-------- : hostpolicy!coreclr_t::execute_assembly+0x2a [D:\a\_work\1\s\src\native\corehost\hostpolicy\coreclr.cpp @ 109]20 11 00007ffa`33b92e5c     : 000001b1`08288438 000000ac`9837f629 00007ffa`33bcc9c0 000001b1`08288438 : hostpolicy!run_app_for_context+0x596 [D:\a\_work\1\s\src\native\corehost\hostpolicy\hostpolicy.cpp @ 256]21 12 00007ffa`33b9379a     : 00000000`00000000 000001b1`08288430 000001b1`08288430 00000000`00000000 : hostpolicy!run_app+0x3c [D:\a\_work\1\s\src\native\corehost\hostpolicy\hostpolicy.cpp @ 285]22 13 00007ffa`4708b5c9     : 000001b1`0829a7c8 000001b1`0829a6b0 00000000`00000000 000000ac`9837f729 : hostpolicy!corehost_main+0x15a [D:\a\_work\1\s\src\native\corehost\hostpolicy\hostpolicy.cpp @ 426]23 14 00007ffa`4708e066     : 000001b1`082996c0 000000ac`9837fab0 00000000`00000000 00000000`00000000 : hostfxr!execute_app+0x2e9 [D:\a\_work\1\s\src\native\corehost\fxr\fx_muxer.cpp @ 145]24 15 00007ffa`470902ec     : 00007ffa`470c25f8 000001b1`08297ed0 000000ac`9837f9f0 000000ac`9837f9a0 : hostfxr!`anonymous namespace'::read_config_and_execute+0xa6 [D:\a\_work\1\s\src\native\corehost\fxr\fx_muxer.cpp @ 532]
25 16 00007ffa`4708e644     : 000000ac`9837fab0 000000ac`9837fad0 000000ac`9837fa21 000001b1`082982f0 : hostfxr!fx_muxer_t::handle_exec_host_command+0x16c [D:\a\_work\1\s\src\native\corehost\fxr\fx_muxer.cpp @ 1007]26 17 00007ffa`470885a0     : 000000ac`9837fad0 000001b1`082977d0 00000000`00000001 000001b1`08280000 : hostfxr!fx_muxer_t::execute+0x494 [D:\a\_work\1\s\src\native\corehost\fxr\fx_muxer.cpp @ 578]27 18 00007ff7`5f0ef998     : 00007ffa`7ecff4e8 00007ffa`47089b10 000000ac`9837fc70 000001b1`082974c0 : hostfxr!hostfxr_main_startupinfo+0xa0 [D:\a\_work\1\s\src\native\corehost\fxr\hostfxr.cpp @ 62]28 19 00007ff7`5f0efda6     : 00007ff7`5f0fb6c0 00000000`00000007 000001b1`08288430 00000000`0000005e : apphost!exe_start+0x878 [D:\a\_work\1\s\src\native\corehost\corehost.cpp @ 240]29 1a 00007ff7`5f0f12e8     : 00000000`00000000 00000000`00000000 000001b1`08288430 00000000`00000000 : apphost!wmain+0x146 [D:\a\_work\1\s\src\native\corehost\corehost.cpp @ 311]30 1b (Inline Function)     : --------`-------- --------`-------- --------`-------- --------`-------- : apphost!invoke_main+0x22 [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 90]31 1c 00007ffa`7fcd7344     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : apphost!__scrt_common_main_seh+0x10c [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 288]32 1d 00007ffa`814e26b1     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : KERNEL32!BaseThreadInitThunk+0x14
33 1e 00000000`00000000     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x21

KERNELBASE!RaiseException
方法的第一个参数就是具体的异常对象地址,这个地址就是
000001b1`0c80a000
,我们可以使用【!PrintException
000001b1`0c80a000
】命令查看具体的异常信息。

1 0:000> !PrintException 000001b1`0c80a0002 Exception object: 000001b10c80a0003 Exception type:   System.IO.FileNotFoundException4 Message:5 InnerException:   <none>
6 StackTrace (generated):7 SP               IP               Function8     000000AC9837E980 00007FF995635EA2 System_Private_CoreLib!System.Runtime.Loader.AssemblyLoadContext.LoadFromAssemblyPath(System.String)+0xf2
9     000000AC9837EAA0 00007FF99565D4AA System_Private_CoreLib!System.Reflection.Assembly.LoadFrom(System.String)+0x17a
10     000000AC9837EB20 00007FF95D241991 ExampleCore_4_1_1!ExampleCore_4_1_1.Program.Main(System.String[])+0x61
11 
12 StackTraceString: <none>
13 HResult: 80070002

System.Runtime.Loader.AssemblyLoadContext.LoadFromAssemblyPath 和
System.Reflection.Assembly.LoadFrom

就是通过指定上下文加载程序集的,这也证明我们所说的。

CLR 加载器加载程序集的时候路径探测:

当我们设置好【Log Bind failures to Disk】选项时,直接运行 ExampleCore_4_1_1.exe 我们的程序,直到抛出异常。如图:

回到【fuslogvw.exe】工具并点击【刷新(Refresh)】按钮。这是,我们就能在【fuslogvw.exe】主界面中看到一条日志项。如图:

这里的信息很少,我们可以双击日志项,使用浏览器打开。输出如下:

1 *** 程序集联编程序日志项 (2024/3/28 @ 14:50:58) ***
2 
3 操作失败。4 绑定结果: hr = 0x80070002。系统找不到指定的文件。5 
6 程序集管理器加载位置:  C:\Windows\Microsoft.NET\Framework64\v4.0.30319\clr.dll7 在可执行文件下运行  C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe8 ---详细的错误日志如下。9 
10 === 预绑定状态信息 ===
11 日志: DisplayName = System.Memory, Version=4.0.1.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd5112  (Fully-specified)13 日志: Appbase = file:///C:/Windows/System32/WindowsPowerShell/v1.0/
14 日志: 初始 PrivatePath =NULL15 日志: 动态基 =NULL16 日志: 缓存基 =NULL17 日志: AppName =powershell.exe18 调用程序集: Microsoft.VisualStudio.Telemetry, Version=16.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a。19 ===
20 日志: 此绑定从 LoadFrom 加载上下文开始。21 警告: 将不在 LoadFrom 上下文中探测本机映像。仅在默认加载上下文中探测本机映像,例如,使用 Assembly.Load()。22 日志: 正在使用应用程序配置文件: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe.Config23 日志: 使用主机配置文件:24 日志: 使用 C:\Windows\Microsoft.NET\Framework64\v4.0.30319\config\machine.config 的计算机配置文件。25 日志: 策略后引用: System.Memory, Version=4.0.1.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd5126 日志: GAC 查找不成功。27 日志: 尝试下载新的 URL file:///C:/Windows/System32/WindowsPowerShell/v1.0/System.Memory.DLL。
28 日志: 尝试下载新的 URL file:///C:/Windows/System32/WindowsPowerShell/v1.0/System.Memory/System.Memory.DLL。
29 日志: 尝试下载新的 URL file:///C:/Windows/System32/WindowsPowerShell/v1.0/System.Memory.EXE。
30 日志: 尝试下载新的 URL file:///C:/Windows/System32/WindowsPowerShell/v1.0/System.Memory/System.Memory.EXE。
31 日志: 尝试下载新的 URL file:///D:/Program Files/Microsoft Visual Studio/2022/Community/Common7/Tools/System.Memory.DLL。
32 日志: 尝试下载新的 URL file:///D:/Program Files/Microsoft Visual Studio/2022/Community/Common7/Tools/System.Memory/System.Memory.DLL。
33 日志: 尝试下载新的 URL file:///D:/Program Files/Microsoft Visual Studio/2022/Community/Common7/Tools/System.Memory.EXE。
34 日志: 尝试下载新的 URL file:///D:/Program Files/Microsoft Visual Studio/2022/Community/Common7/Tools/System.Memory/System.Memory.EXE。
35 日志: 已尝试所有探测 URLs 但全部失败。

输出信息包含三个部分,第一部分包含了日志的操作状态(
操作失败
)、HRESULT(hr =
0x80070002
)、失败原因(
系统找不到指定的文件。
)、CLR 加载器的路径以及可执行程序的路径。
第二部分信息主要是【预绑定状态信息】,在这部分信息中包括加载过程可能被探测的其他路径,例如:应用程序库(Appbase)、私有路径(PrivatePath)、动态库(动态基)以及缓存库(缓存基)等。这些不同路径,表示在加载程序集的时候需要探测的路径。
第三部分包含的就是实际的探测记录。从输出信息中我们可以知道它处于指定加载上下文中(因为使用了 LoadFrom)。这些信息还指出,由于没有找到应用程序的配置文件,转而使用了全局的配置文件(使用 C:\Windows\Microsoft.NET\Framework64\v4.
0.30319
\config\machine.config 的计算机配置文件

)。找遍了所有路径,都没有找到,所以最后失败(已尝试所有探测 URLs 但全部失败。)


3.3、加载上下文故障
调试源码:ExampleCore_4_1_2
调试任务:加载上下文的故障
这节的内容是证明在不同应用程序域中传递对象会出现问题,但是原书内容写的很早,而且是 .Net Framework 框架,在 .NET 8.0 或者以后的框架已经发生了变化。而且在 .NET 8.0 环境中已经不支持 AppDomian.CreateDomain 方法了。源码我也修改了,测试没有问题,可能我还没有领悟到真谛,需要继续学习,具体就不做测试了。大家如果想测试,自己可以试试。

3.4、互用性与 DllNotFoundException
CLR 提供了一个互用性层,可以让 .NET 代码和底层的非托管代码模块相互调用。根据不同类型的非托管代码,可以有两种不同的互用性机制。第一种:平台调用(Platform Invocation,P/Invoke),这种机制可以使开发人员调用非托管代码模块(DLL)中导出的函数。要实现这种机制,必须定义与托管代码等价的方法原型,并使用【DllImport】特性来说明这是一个 P/Invoke 方法。
第二种:COM 互用性。正是由于有这个特性,才可以使托管代码和非托管 COM 对象一起工作。
当我们使用 P/Invoke 调用非托管方法时,如果没有找到指定的 dll,就会抛出 System.DllNotFoundException 异常。这个没什么好说的,也很容易理解。
如果大家想测试,我提供源码,Net8.0 项目:ExampleCore_4_1_3,C++ 项目:ExampleCore_4_1_4,该项目必须设置成输出 dll。将生成的 dll 拷贝到我们 Net 项目 debug 目录下,为了能出错,我们 ExampleCore_4_1_4.dll 的名称改成 ExampleCore_4_1_4.old,直接运行程序就可以了。如果想查看正确输出结果,把后缀名改成 .dll 就可以了。
给大家一个感性认识,效果如图:

说明一下,在 P/Invoke 调用过程中使用的非托管 DLL,都是通过底层 kernel32!LoadLibrary 来加载的,这个 API 使用了与 Windows 加载动态库时相同的探测逻辑。如果我们想使用【fuslogvw.exe】分析 P/Invoke 加载问题时是不起作用的。

3.5、轻量级代码生成的调试
CLR 提供了一种既方便又高效动态生成代码的机制(也称为 CodeGen)。在 CLR 2.0 之前,开发人员如果想实现动态生成代码,必须自己维护在 CLR 上运行的任何代码段的结构完整性。比如:如果我们想定义一个方法,该方法接受两个整型参数,返回两个参数之和,我们要做的工作是,必须定义方法本身的实现逻辑(包括 IL)、这个方法使用的类型、这个类型定义所在的模块以及程序集所在的模块等。当我们只希望生成一下规模很小或者是不是很复杂的方法时就很不方便。
在 CLR 2.0 引入了【轻量级代码生成机制(Light Weight Code Generation,LCG)】。当使用 LCG 时,我们定义代码更简单,不需要关注和代码无关的东西(模块、类型、程序集等),并通过委托机制来调用它。
System.Reflection.Emit 命名空间下的 API 可以高效、动态的生成代码。
很庆幸,使用非托管调试器调试动态生成的代码并不困难。如果我们想使用【!bpmd】命令为方法设置断点,必须先找到方法的描述符。但是,对于 LCG 代码,我们需要做一些手动工作。具体来说,我们需要通过 JIT 组件中某个函数来获得方法描述符。这个函数就是:clrjit!CILJit::compileMethod,如果我们在这个方法上设置一个非托管断点(通过 bp 命令),那么可以从这个函数的第一个参数中获得方法描述符。

眼见为实:
调试代码:ExampleCore_4_1_5
调试任务:调试由 System.Reflection.Emit 生成轻量级代码,在 Add 方法上设置断点
A、NTSD 调试
编译项目,打开【Visual Studio 2022 Developer Command Prompt v17.9.4】命令行工具,输入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_4_1_5\bin\Debug\net8.0\ExampleCore_4_1_5.EXE】,如图:

打开【NTSD】 调试器窗口,效果如图:

我们使用【g】命令,运行调试器,直到调试器自动中断执行,开始我们的调试。
首先,我们使用【x clrjit!*compileMethod*】命令,查找【compileMethod】方法,因为 CLR 使用该方法编译我们代码。

1 0:000> x clrjit!*compileMethod*
2 00007ffb`c13abc60 clrjit!CILJit::compileMethod (class ICorJitInfo *, struct CORINFO_METHOD_INFO *, unsigned int, unsigned char **, unsigned int *)

我们找到了这个方法,可以针对 clrjit!CILJit::compileMethod 这个方法下一个断点。

1 0:000> bp clrjit!CILJit::compileMethod

【g】继续运行,直到调试器在断点处暂停。

1 0:000>g2 Breakpoint 0hit3 clrjit!CILJit::compileMethod:4 00007ffb`c13abc60 48895c2408      mov     qword ptr [rsp+8],rbx ss:00000008`889fda00=00007ffb62a444a0

我们继续使用【kb】命令,查看线程调用栈,并查看前3个参数。当然也可以为【kb 1】只显示一条栈帧。

1 0:000>kb2 RetAddr               : Args to Child                                                           : Call Site3 00007ffb`c23789fe     : 00007ffb`62a444a0 00007ffb`62a444a0 0000019a`516b8550 00000000`00000000 : clrjit!CILJit::compileMethod4 (Inline Function)     : --------`-------- --------`-------- --------`-------- --------`-------- : coreclr!invokeCompileMethodHelper+0x70
5 (Inline Function)     : --------`-------- --------`-------- --------`-------- --------`-------- : coreclr!invokeCompileMethod+0xb0
6 00007ffb`c2365507     : 00007ffb`62a10008 00000000`00000000 00007ffb`62a10008 00000000`00000000 : coreclr!UnsafeJitFunction+0x7ee
7 00007ffb`c2365327     : 00010000`00000001 00007b33`88000001 00000000`00007b33 00007ffb`c2749b98 : coreclr!MethodDesc::JitCompileCodeLocked+0xef
8 00007ffb`c2364ffc     : 000001da`e7d321d0 00000008`889fe2f0 000001da`e7d321d0 000001da`e7d321d0 : coreclr!MethodDesc::JitCompileCodeLockedEventWrapper+0x17b
9 00007ffb`c2364c43     : 00000008`889fe3f0 00000008`00000000 00000000`00000000 00000000`00000000 : coreclr!MethodDesc::JitCompileCode+0x2bc
10 00007ffb`c23fb146     : 00007ffb`62a44430 00000000`00000000 00000000`00000000 00000002`51650000 : coreclr!MethodDesc::PrepareILBasedCode+0xc3
11 (Inline Function)     : --------`-------- --------`-------- --------`-------- --------`-------- : coreclr!MethodDesc::PrepareCode+0x12
12 00007ffb`c2374c6f     : 00007ffb`62a44430 00007ffb`627d4190 0000019a`516a04c0 00007ffb`6283fc18 : coreclr!MethodDesc::PrepareInitialCode+0x4e
13 00007ffb`c23748d9     : 0000019a`5169b7e0 0000019a`516a04c0 00007ffb`0002a020 00007ffb`62a10008 : coreclr!MethodDesc::DoPrestub+0x26f
14 00007ffb`c24acc05     : 00000008`889fe9e8 00000008`889fe998 01e2ff49`20c38349 ff800180`02800080 : coreclr!PreStubWorker+0x1f9
15 00007ffb`62931c5a     : 00000000`00000001 00000000`00000002 00000000`00000002 00000000`00000000 : coreclr!ThePreStub+0x55
16 00007ffb`c24ab8c3     : 0000019a`55c08ea0 00000008`889fef38 00000008`889fef38 00000008`889feb29 : 0x00007ffb`62931c5a17 00007ffb`c23e0b19     : 00000000`00000000 00000000`00000130 00000008`889feb38 00007ffb`c2360232 : coreclr!CallDescrWorkerInternal+0x83
18 (Inline Function)     : --------`-------- --------`-------- --------`-------- --------`-------- : coreclr!CallDescrWorkerWithHandler+0x56
19 00007ffb`c23dd730     : 00000008`889febb8 00000000`00000000 00000000`00000048 00007ffb`c244d046 : coreclr!MethodDescCallSite::CallTargetWorker+0x2a1
20 (Inline Function)     : --------`-------- --------`-------- --------`-------- --------`-------- : coreclr!MethodDescCallSite::Call+0xb
21 00007ffb`c2402fc6     : 0000019a`55c08ea0 0000019a`55c08ea0 00000000`00000000 00000008`889fef38 : coreclr!RunMainInternal+0x11c
22 00007ffb`c24032fb     : 0000019a`516a04c0 0000019a`00000000 0000019a`516a04c0 00000000`00000000 : coreclr!RunMain+0xd2
23 00007ffb`c2359141     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000130 : coreclr!Assembly::ExecuteMainMethod+0x1bf
24 00007ffb`c246e8b8     : 00000000`00000001 00000008`889ff101 00000008`889ff160 00007ffb`c28423ea : coreclr!CorHost2::ExecuteAssembly+0x281
25 00007ffb`c2862b76     : 0000019a`51671980 0000019a`51671680 00000000`00000000 0000019a`51671680 : coreclr!coreclr_execute_assembly+0xd8
26 (Inline Function)     : --------`-------- --------`-------- --------`-------- --------`-------- : hostpolicy!coreclr_t::execute_assembly+0x2a
27 00007ffb`c2862e5c     : 0000019a`5165c358 00000008`889ff389 00007ffb`c289c9c0 0000019a`5165c358 : hostpolicy!run_app_for_context+0x596
28 00007ffb`c286379a     : 00000000`00000000 0000019a`5165c350 0000019a`5165c350 00000000`00000000 : hostpolicy!run_app+0x3c
29 00007ffb`c28bb5c9     : 0000019a`516707a8 0000019a`51670690 00000000`00000000 00000008`889ff489 : hostpolicy!corehost_main+0x15a
30 00007ffb`c28be066     : 0000019a`5166f6a0 00000008`889ff810 00000000`00000000 00000000`00000000 : hostfxr!execute_app+0x2e9
31 00007ffb`c28c02ec     : 00007ffb`c28f25f8 0000019a`5166df30 00000008`889ff750 00000008`889ff700 : hostfxr!`anonymous namespace'::read_config_and_execute+0xa6
32 00007ffb`c28be644     : 00000008`889ff810 00000008`889ff830 00000008`889ff781 0000019a`5166e350 : hostfxr!fx_muxer_t::handle_exec_host_command+0x16c
33 00007ffb`c28b85a0     : 00000008`889ff830 0000019a`5166d830 00000000`00000001 0000019a`51650000 : hostfxr!fx_muxer_t::execute+0x494
34 *** WARNING: Unable to verify checksum forapphost.exe35 00007ff6`2595f998     : 00007ffc`9a0df4e8 00007ffb`c28b9b10 00000008`889ff9d0 0000019a`5166d520 : hostfxr!hostfxr_main_startupinfo+0xa0
36 00007ff6`2595fda6     : 00007ff6`2596b6c0 00000000`00000007 0000019a`5165c350 00000000`0000005e : apphost!exe_start+0x878
37 00007ff6`259612e8     : 00000000`00000000 00000000`00000000 0000019a`5165c350 00000000`00000000 : apphost!wmain+0x146
38 (Inline Function)     : --------`-------- --------`-------- --------`-------- --------`-------- : apphost!invoke_main+0x22
39 00007ffc`9a997344     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : apphost!__scrt_common_main_seh+0x10c
40 00007ffc`9c4226b1     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : KERNEL32!BaseThreadInitThunk+0x14
41 00000000`00000000     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x21

clrjit!CILJit::compileMethod
方法的第一个参数就是我们 Add 方法的方法描述符。我们可以使用【!DumpMD】命令来验证。

1 0:000> !dumpmd 00007ffb`62a444a02 Method Name:          DynamicClass.Add(Int32, Int32)(这就是我们动态生成的方法)3 Class:                00007ffb62a443b84 MethodTable:          00007ffb62a444305 mdToken:              0000000006000000
6 Module:               00007ffb62a1b0707 IsJitted:             no(还没有编译)8 Current CodeAddr:     ffffffffffffffff(因为没有编译,所以地址就是 f)9 Version History:10   ILCodeVersion:      0000000000000000
11   ReJIT ID:           0
12   IL Addr:            0000000000000000
13      CodeAddr:           0000000000000000(Optimized)14      NativeCodeVersion:  0000000000000000

我们有了 MD,就是方法描述符,就可以使用【!bpmd -md
00007ffb`62a444a0
】命令,为 Add 方法设置断点了。

1 0:000> !bpmd -md 00007ffb`62a444a02 MethodDesc =00007FFB62A444A03 This DynamicMethodDesc is not yet JITTED. Placing memory breakpoint at 00007FFB62A444D0

【g】继续运行,会到我们到 Add 方法开始暂停,我们可以使用【!clrstack -a】命令,查看托管线程调用栈来确认。

1 0:000>g2 Unable to insert breakpoint 2at 00007ffb`62a50040, Win32 error 0n9983     "内存位置访问无效。"
4 The breakpoint was setwith BP.  If you want breakpoints5 to track module load/unload state you must use BU.6 bp2 at 00007ffb`62a50040 failed7 WaitForEvent failed, Win32 error 0n9988 内存位置访问无效。9 coreclr!MethodDesc::SetNativeCodeInterlocked+0x24 [inlined in coreclr!PrepareCodeConfig::SetNativeCode+0x49]:10 00007ffb`c245dfe9 7523            jne     coreclr!PrepareCodeConfig::SetNativeCode+0x6e (00007ffb`c245e00e) [br=0]

我这里测试有点问题,所有使用【NTSD】做的断点测试都失败,还没有找到原因。


B、Windbg Preview 调试
编译项目,打开【Windbg Preview】,依次点击【文件】----》【launch executable】加载我们的项目文件:ExampleCore_4_1_5.exe。进入调试器后,继续使用【g】命令,运行调试器,调试会自动进入中断模式,也就是在这行源代码【Debugger.Break();】,我们的代码已经编译,但是还没有执行,也可以说,此时的代码还没有经过 JIT 编译。
我们先使用【lm】命令查看一下我们的 CLR 和 JIT 是否已经加载了。

1 0:000>lm2 start             end                 module name3 0000029f`73ad0000 0000029f`73ad8000   ExampleCore_4_1_5 C (service symbols: CLR Symbols with PDB: C:\ProgramData\Dbg\sym\ExampleCore_4_1_5.pdb\11A5E302B38142129CE69ACABA26395C1\ExampleCore_4_1_5.pdb)4 0000029f`73ae0000 0000029f`73aee000   System_Runtime   (deferred)5 0000029f`73af0000 0000029f`73af8000   System_Reflection_Emit_Lightweight   (deferred)6 0000029f`73b00000 0000029f`73b08000   System_Reflection_Emit_ILGeneration   (deferred)7 0000029f`73b10000 0000029f`73b18000   System_Reflection_Primitives   (deferred)8 00007ff7`4bf90000 00007ff7`4bfb9000   apphost  C (privatepdb symbols)  C:\ProgramData\Dbg\sym\apphost.pdb\5633DAB747FE452D91289F0AE5A53DEB1\apphost.pdb9 00007ffd`4be50000 00007ffd`4cadc000   System_Private_CoreLib   (service symbols: CLR Symbols with PDB: C:\ProgramData\Dbg\sym\System.Private.CoreLib.pdb\44580BF6DF5DBDE0A6AF2B06590C0AF21\System.Private.CoreLib.pdb)10 00007ffd`4d7f0000 00007ffd`4d9a9000   clrjit     (deferred)11 00007ffd`4dfc0000 00007ffd`4e4a6000   coreclr    (privatepdb symbols)  C:\ProgramData\Dbg\sym\coreclr.pdb\0FCDE80BC2AB441FBFBD19C1354D99E71\coreclr.pdb12 00007ffd`4f350000 00007ffd`4f3b4000   hostpolicy   (deferred)13 00007ffd`81620000 00007ffd`81648000System_Console   (deferred)14 00007ffd`91b40000 00007ffd`91b99000   hostfxr    (deferred)15 00007ffe`35ec0000 00007ffe`35fd7000   gdi32full   (deferred)16 00007ffe`35fe0000 00007ffe`35ff2000   kernel_appcore   (deferred)17 00007ffe`36050000 00007ffe`36346000KERNELBASE   (pdb symbols)          C:\ProgramData\Dbg\sym\kernelbase.pdb\4D43CAB87CB573B14B1F37FFEAF274731\kernelbase.pdb18 00007ffe`3635000000007ffe`363ed000   msvcp_win   (deferred)19 00007ffe`364a0000 00007ffe`364c2000   win32u     (deferred)20 00007ffe`364d0000 00007ffe`36552000bcryptPrimitives   (deferred)21 00007ffe`366c0000 00007ffe`367c0000   ucrtbase   (deferred)22 00007ffe`36810000 00007ffe`36837000bcrypt     (deferred)23 00007ffe`36d90000 00007ffe`36ebb000   ole32      (deferred)24 00007ffe`36f20000 00007ffe`37045000RPCRT4     (deferred)25 00007ffe`37050000 00007ffe`37082000IMM32      (deferred)26 00007ffe`3709000000007ffe`3722e000   USER32     (deferred)27 00007ffe`3738000000007ffe`3741e000   msvcrt     (deferred)28 00007ffe`3742000000007ffe`37b8b000   SHELL32    (deferred)29 00007ffe`37c90000 00007ffe`37d40000   ADVAPI32   (deferred)30 00007ffe`37e00000 00007ffe`37ecd000   OLEAUT32   (deferred)31 00007ffe`37ed0000 00007ffe`37f70000   sechost    (deferred)32 00007ffe`3846000000007ffe`387b3000   combase    (deferred)33 00007ffe`387c0000 00007ffe`3887d000   KERNEL32   (deferred)34 00007ffe`389a0000 00007ffe`389cb000   GDI32      (deferred)35 00007ffe`38a10000 00007ffe`38c08000   ntdll      (pdb symbols)          C:\ProgramData\Dbg\sym\ntdll.pdb\90C2362B9D1F1F5088ABFA3BDE69BAAF1\ntdll.pdb

ExampleCore_4_1_5
我们的项目已经加载了,说明 CLR 和 JIT 也已经加载了,
clrjit
红色标注的就是我们处理器,说明我的猜测也是对的。
然后,我们通过【x clrjit!*CompileMethod*】命令查找
CompileMethod方法。

1 0:000> x clrjit!*CompileMethod*
2 00007ffd`4d8dbc60 clrjit!CILJit::compileMethod (class ICorJitInfo *, struct CORINFO_METHOD_INFO *, unsigned int, unsigned char **, unsigned int *)

我们找到了 CompileMethod 方法,给这个方法下一个断点。

1 0:000> bp clrjit!CILJit::compileMethod

我们继续【g】运行调试器,已经在 CILJit::compileMethod 方法处断住了。

1 0:000>g2 Breakpoint 0hit3 clrjit!CILJit::compileMethod:4 00007ffd`4d09bc60 48895c2408      mov     qword ptr [rsp+8],rbx ss:0000005a`bc97d8a0=00007ffcee8644a0

我们在
CILJit::compileMethod
这个方法已经断住了,然后我们使用【kb 1】命令,查看一下它的参数。

1 0:000> kb 1
2 # RetAddr               : Args to Child                                                           : Call Site3 00 00007ffd`4e1989fe     : 00007ffc`ee8644a0 00007ffc`ee8644a0 0000024a`1f3716a0 00000000`00000000 : clrjit!CILJit::compileMethod [D:\a\_work\1\s\src\coreclr\jit\ee_il_dll.cpp @ 279] 

00007ffc`ee8644a0
这个地址就是我们 Add 方法的描述符地址。

1 0:000> !dumpmd 00007ffc`ee8644a02 Method Name:          DynamicClass.Add(Int32, Int32) 这就是我们要查找的方法3 Class:                00007ffcee8643b84 MethodTable:          00007ffcee8644305 mdToken:              0000000006000000
6 Module:               00007ffcee83b0707 IsJitted:             no(说明还没有被 JIT 编译)8 Current CodeAddr:     ffffffffffffffff9 Version History:10   ILCodeVersion:      0000000000000000
11   ReJIT ID:           0
12   IL Addr:            0000000000000000
13      CodeAddr:           0000000000000000(Optimized)14      NativeCodeVersion:  0000000000000000             

我们使用【
!bpmd -md 00007ffb`5d3244a0
】命令为 Add 方法设置断点。

1 0:000> !bpmd -md 00007ffb`5d3244a02 MethodDesc =00007FFB5D3244A03 This DynamicMethodDesc is not yet JITTED. Placing memory breakpoint at 00007FFB5D3244D0

我们继续【g】运行调试器,直到调试器在断点处中断。

1 0:000>g2 Breakpoint 2hit3 00007ffb`5d330040 8d0411          lea     eax,[rcx+rdx]

【g】继续运行,我们使用【!clrstack -a】命令查看一下托管调用栈。

1 0:000> !clrstack -a2 OS Thread Id: 0x4d70 (0)3 Child SP               IP Call Site4 0000008E08B7E558 00007ffb5d330040 SOS Warning: Loading symbols for dynamic assemblies isnot yet supported5 DynamicClass.Add(Int32, Int32)6 PARAMETERS:7         <no data>
8         <no data>
9 
10 0000008E08B7E560 00007ffb5d211c5a ExampleCore_4_1_5.Program.Main(System.String[]) [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_4_1_5\Program.cs @ 23]11 PARAMETERS:12         args (0x0000008E08B7E6A0) = 0x000001f28e808ea0
13 LOCALS:14         0x0000008E08B7E688 = 0x000001f28e809640
15         0x0000008E08B7E680 = 0x000001f28e809668
16         0x0000008E08B7E678 = 0x000001f28e809bc8
17         0x0000008E08B7E670 = 0x000001f28e809ea8
18         0x0000008E08B7E66C = 0x0000000000000000
19         0x0000008E08B7E640 = 0x0000000000000000

四、总结
这篇文章的终于写完了,这篇文章的内容相对来说,不是很多。写完一篇,就说明进步了一点点。Net 高级调试这条路,也刚刚起步,还有很多要学的地方。皇天不负有心人,努力,不辜负自己,我相信付出就有回报,再者说,学习的过程,有时候,虽然很痛苦,但是,学有所成,学有所懂,这个开心的感觉还是不可言喻的。不忘初心,继续努力。做自己喜欢做的,开心就好。

引言

在现代化的软件开发中,
单元测试

集成测试
是确保代码质量和可靠性的关键部分。
ASP.NET Core
社区内提供了强大的单元测试框架,
xUnit
是其中之一,它提供了简单、清晰和强大的测试功能,编写单元测试有许多优点;有助于回归、提供文档及辅助良好的设计。下面几节我们来深入浅出探讨如何使用
xUnit
进行
ASP.NET Core
应用程序的单元测试和集成测试。

内容大纲:

xUnit 简介

xUnit.net
是一个免费、开源、面向社区的
.NET
单元测试工具。由
NUnit v2
的原始发明者编写,
xUnit.net
是用于
C#

F#
(其他
.NET
语言可能也可以使用,但不受支持)的最新技术单元测试。
xUnit.net
可与
Visual Studio

Visual Studio Code

ReSharper

CodeRush

TestDriven.NET
一起使用。它是
.NET
基金会的一部分,并遵守其行为准则。其许可协议为 Apache 2(为 OSI 批准的许可协议)。

xUnit.net 官方网站

创建单元测试项目

在单元测试中通常要遵循
AAA
模式,也就是
Arrange

Act

Assert
,这是一种常见的测试组织结构。

  • Arrange(准备)
    : 在这个阶段,将设置测试的前提条件,初始化对象、设置输入参数等。简单讲就是准备测试环境,确保被测代码在正确的上下文中执行。
  • Act(执行)
    : 在这个阶段,会执行要测试的代码或方法。这是针对被测代码的实际调用或操作。
  • Assert
    : 在这个阶段,会验证被测代码的行为是否符合预期。检查实际结果与期望结果是否一致,如果不一致则测试失败。

示例:

[Fact]
public void Add_EmptyString_ReturnsZero()
{
    // Arrange
    var stringCalculator = new StringCalculator();

    // Act
    var actual = stringCalculator.Add("");

    // Assert
    Assert.Equal(0, actual);
}

可读性是编写单元测试最重要的方面之一,在测试中分离这些操作 都明确地突出调用代码所需的依赖项、调用代码的方式以及尝试断言的内容,让测试尽可能具有可读性。

好了理解了这个核心概念我们可以先创建项目一步步的练习了。

用 VS 创建单元测试项目

image

在项目创建完之后我们可以简单浏览一下
xUnit
单元测试项目装了那些
nuget
依赖,做到对项目有个简单的了解

  <ItemGroup>
    <PackageReference Include="coverlet.collector" Version="6.0.0" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
    <PackageReference Include="xunit" Version="2.5.3" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
  </ItemGroup>

下面我们创建一个简单的数据计算类。

  • 创建数学计算类
public class MathCalculator
{
    public int Add(int a, int b)
    {
        return a + b;
    }
}
  • 创建数据计算测试类
public class MathCalculatorTests
{
    [Fact]
    public void Add_TwoNumbers_ReturnSum()
    {
        // Arrange
        var calculator = new MathCalculator();

        // Act
        var result = calculator.Add(3, 5);

        // Assert
        Assert.Equal(8, result);
    }
}

测试一下,测试类库右键->运行测试

image

可以看到 我们的单元测试通过。

单元测试命名规范

本着
代码自文档的原则
,测试的名称建议应包括三个部分:

  • 要测试的方法的名称。
  • 测试的方案。
  • 调用方案时的预期行为。

示例

    [Fact]
    public void Add_TwoNumbers_ReturnSum()
    {
        // Arrange
        var calculator = new MathCalculator();

        // Act
        var result = calculator.Add(3, 5);

        // Assert
        Assert.Equal(8, result);
    }

要测试的方法名称是
MathCalculator
中的 Add 方法,测试的方案是传两个数,预期是返回两数之和 按照上面的测试名称的命名规则可以命名为
Add_TwoNumbers_ReturnSum

单元测试最佳命名规范应该包括三个关键部分:要测试的方法的名称、测试的场景,以及调用该场景时的预期行为。良好的命名标准能清晰表达测试意图,提供有效文档,便于他人理解代码行为和快速定位问题。

单元测试最佳实践


将方法标记为测试方法在
xUnit
中有两个属性,
Fact

Theory

Fact 属性

在方法上我们看到有一个
Attribute
[Fact] ,[Fact] 属性是 xUnit 中最基本的测试属性之一,用于标记一个方法作为一个无需参数且不返回任何内容的测试方法。被标记为 [Fact] 的方法将会被
xUnit
框架识别并执行.

Theory 属性

Theory
属性用于标记一个测试方法,该方法可以接受参数并运行多次,每次运行时使用不同的参数值。Theory 属性通常用于数据驱动测试,允许在同一个测试方法中使用不同的输入数据进行测试.

InlineData 属性

[InlineData] 属性指定这些输入 Theory 标记的测试方法的参数值。

示例:

[Theory]
[InlineData(-1)]
[InlineData(0)]
[InlineData(1)]
public void IsPrime_ValuesLessThan2_ReturnFalse(int value)
{
    var result = _primeService.IsPrime(value);

    Assert.False(result, $"{value} should not be prime");
}

InlineData
适用于静态、硬编码的测试数据集合,适合于简单且固定的测试场景。

MemberData 属性

MemberData
属性是
xUnit
中用于数据驱动测试的一种方式,它允许从一个字段、属性或方法中获取测试数据,并将这些数据传递给测试方法进行多次测试。通过
MemberData
属性,可以更灵活地管理和提供测试数据,适用于需要动态生成测试数据的情况。

使用方式

  • 标记测试方法:使用 [Theory] 属性标记测试方法,以便接受从 MemberData 属性提供的测试数据。
  • 准备测试数据:创建一个公共静态字段、属性或方法,该字段、属性或方法返回一个 IEnumerable<object[]> 对象,其中每个 object[] 对象代表一组测试数据。
  • 传递测试数据:在 MemberData 属性中指定要使用的数据源,从而将数据传递给测试方法。

示例

    public static IEnumerable<object[]> GetComplexTestData()
    {
        yield return new object[] { 10, 5, 15 }; // 测试数据 1
        yield return new object[] { -3, 7, 4 }; // 测试数据 2
        yield return new object[] { 0, 0, 0 }; // 测试数据 3
        // 可以根据需要继续添加更多的测试数据
    }

   [Theory]
   [MemberData(nameof(GetComplexTestData))]
   public void Add_TwoNumbers_ReturnsSumofNumbers01(int first, int second, int sum)
   {
       // Arrange
       var calculator = new MathCalculator();

       // Act
       var result = calculator.Add(first, second);

       // Assert
       Assert.Equal(sum, result);
   }

MemberData
适用于动态、灵活的测试数据集合,适合于需要从外部源动态获取测试数据的情况。

image

自定义属性

除了上面提到的
InlineData

MemberData
之外还可以有更加灵活的方式继承
DataAttribute
实现自定义的
Attribute

我们来做一个实现和上面一样的需求

  • 实现 Custom Attribute

[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class CustomDataAttribute : DataAttribute
{

    private readonly int _first;
    private readonly int _second;
    private readonly int _sum;

    public CustomDataAttribute(int first, int second, int sum)
    {
        _first = first;
        _second = second;
        _sum = sum;
    }
    public override IEnumerable<object[]> GetData(MethodInfo testMethod)
    {
        yield return new object[] { _first, _second, _sum };
    }
}



  • 用例
   [Theory]
   [CustomData(1, 2, 3)]
   [CustomData(2, 3, 5)]
   public void Add_TwoNumbers_ReturnSum03(int num1, int num2, int expectedSum)
   {
       // Arrange
       var calculator = new MathCalculator();

       // Act
       var result = calculator.Add(num1, num2);

       // Assert
       Assert.Equal(expectedSum, result);
   }

自定义属性相较于使用
InlineData

MemberData
有以下优势:

  1. 灵活性
    :自定义属性允许您实现更复杂的逻辑来动态生成测试数据,可以从不同数据源中获取数据,实现更灵活的数据驱动测试。

  2. 重用性
    :通过自定义属性,您可以将相同的测试数据逻辑应用于多个测试方法,提高测试代码的重用性和可维护性。

  3. 可扩展性
    :自定义属性可以根据需求进行定制和扩展,适应不同的测试场景和数据需求,使得测试数据的生成更具灵活性。

  4. 可读性
    :通过自定义属性,可以使测试代码更具可读性和表达力,更清晰地表达测试数据的来源和意图。

尽管使用
InlineData

MemberData
可以满足大多数简单的测试数据需求,但当需要更复杂的数据生成逻辑、数据源、或者对测试数据进行处理时,使用自定义属性会更具优势,能够更好地满足个性化的测试需求。

在测试中应避免逻辑

[Theory]的出现就是为了避免我们在单元测试时编写一些额外的逻辑,造成测试之外的一些错误。

编写单元测试时,请避免手动字符串串联、逻辑条件(例如 if、while、for 和 switch)以及其他条件。

错误示范:

   [Fact]
   public void Add_TwoNumbers_ReturnsSumofNumbers02()
   {
       // Arrange
       var calculator = new MathCalculator();
       var testData = new List<(int, int, int)>
   {
       (1, 2, 3),
       (2, 3, 5),
       (3, 4, 7)
   };

       // Act & Assert
       foreach (var (first, second, sum) in testData)
       {
           var result = calculator.Add(first, second);
           Assert.Equal(sum, result);
       }
   }

此处用了
forEach
循环来批量断言,违反了单元测试的最佳实践。

测试中应避免逻辑的好处是:

  • 降低在测试中引入 bug 的可能性。
  • 专注于最终结果,而不是实现细节。

ITestOutputHelper 控制台输出

在 xUnit 中我们利用
Console.WriteLine
输出时发现什么也不会显示,在 xUnit 单元测试项目中我们需要利用
ITestOutputHelper

ITestOutputHelper
是 xUnit 中的一个接口,用于在单元测试中输出信息。通过
ITestOutputHelper
,您可以在测试运行时将调试信息、日志信息等输出到测试结果中,方便调试和查看测试过程中的输出信息。

调试

再要测试的方法上右键选择调试测试,或者点击方法上面的小点
image

最后

本篇文章简单的讲了单元测试的基础知识,让大家先对单元测试有个基本的概念,这些用在具体的项目中显然是不够的,后面的章节我们聊一下
TDD
,
Fake
管理,
Log
日志输出,单元测试覆盖率,
WebApi
的集成测试,
DependencyInjection
,
Bogus
,还有
Devops
的单元测试等知识。

本文完整源代码

一.简介

QWidget是所有用户界面对象的基类,而QMainWindow
用于创建主应用程序窗口的类。它是
QWidget
的一个子类,提供了创建具有菜单栏、工具栏、状态栏等的主窗口所需的功能。上篇主要介绍了基本使用,创建窗口时都是继承单个模块,本章主要介绍下两个模块的结合使用。

二.具体用法

1.我们先来创建一个类来继承QMainWindow实现一个简单的窗口

importsysfrom PyQt5.QtWidgets importQApplication, QMainWindowclassFileChooserApp(QMainWindow):def __init__(self):
super().
__init__()
self.initUI()
definitUI(self):#设置窗口的标题和大小 self.setWindowTitle('文件选择器')
self.setGeometry(
300, 300, 500, 500)if __name__ == '__main__':
app
=QApplication(sys.argv)
ex
=FileChooserApp()
ex.show()
sys.exit(app.exec_())

2.在主窗口中添加一个文本编辑器,使用垂直布局来分布展示

    definitUI(self):
ql
=QTextEdit()
central_widget
=QWidget()
self.setCentralWidget(central_widget)
#创建一个中心部件 vbox = QVBoxLayout(central_widget) #创建一个垂直布局 vbox.addWidget(ql)
vbox.addStretch(
1)
central_widget.setLayout(vbox)
#添加到布局器中

上述中我们在主窗口中创建一个中心部件将实例化后的central_widget 添加进去,这样就可以设置我们想要的布局了。

3.具体效果

三.实例


importsysfrom PyQt5.QtWidgets importQApplication, QWidget, QVBoxLayout, QFileDialog, QMainWindow, QAction, QTextEditclassFileChooserApp(QMainWindow):def __init__(self):
super().
__init__()
self.ql
=QTextEdit()
self.initUI()
definitUI(self):
action
= QAction('打开', self) #创建QAction实例 action.setShortcut('Ctrl+a') #设置快捷键操 action.triggered.connect(self.openFileNameDialog)
menubar
= self.menuBar() #创建一个菜单栏 menu_open = menubar.addMenu('文件') #设置菜单栏tab menu_open.addAction(action) #关联事件 action_updata= QAction('删除', self) #创建QAction实例 action_updata.setShortcut('Ctrl+d') #设置快捷键操 action_updata.triggered.connect(self.text_del)

action_a
= QAction('保存', self) #创建QAction实例 action_a.triggered.connect(self.ctrl_s)

menubar_updata
= self.menuBar() #创建一个菜单栏 menu_updata = menubar_updata.addMenu('编辑') #设置菜单栏tab menu_updata.addAction(action_updata) #关联事件 menu_updata.addAction(action_a) #关联事件 central_widget=QWidget()
self.setCentralWidget(central_widget)
vbox
= QVBoxLayout(central_widget) #创建一个垂直布局 vbox.addWidget(self.ql)
vbox.addStretch(
1)
central_widget.setLayout(vbox)
#设置窗口的标题和大小 self.setWindowTitle('文件选择器')
self.setGeometry(
300, 300, 500, 500)defctrl_s(self):print(self.ql.toPlainText())
options
=QFileDialog.Options()
options
|=QFileDialog.DontUseNativeDialog
fileName, _
= QFileDialog.getSaveFileName(self, "保存文件", "C://", "Text Files (*.txt)", options=options)iffileName:
with open(fileName,
'w', encoding='utf8') as file:
file.write(self.ql.toPlainText())
deftext_del(self):
self.ql.clear()
defopenFileNameDialog(self):#打开文件对话框并获取文件路径 options = QFileDialog.Options() #初始化 options |= QFileDialog.DontUseNativeDialog #使用pyqt对话框 fileName, _ = QFileDialog.getOpenFileName(self, "对话框标题", "C://","All Files (*);;Text Files (*.txt);;Text Files (*.py)",
options
=options) #打开一个对话框 """C://设置打开的目录,”“模式当前py目录
"All Files (*);;Text Files (*.txt);;Text Files (*.py)" 设置选择文件的类型
""" iffileName:
with open(fileName, mode
="r", encoding="utf8") as f:
self.ql.setText(f.read())
if __name__ == '__main__':
app
=QApplication(sys.argv)
ex
=FileChooserApp()
ex.show()
sys.exit(app.exec_())

View Code

上述中创建了两个菜单栏,分别是打开和编辑,内置功能是打开txt文件后读取内容显示在文本框中,文本框中内容可以进行清空和保存。