|
欢迎深入了解 .NET 恶意软件逆向工程世界。作为一名安全研究员或分析师,您可能知道,.NET 框架(https://dotnet.microsoft.com/en-us/learn/dotnet/what-is-dotnet)以其快速、强大的应用程序开发能力而闻名,但它也是一把双刃剑。.NET框架对合法开发者具有吸引力的特性,也使其成为恶意软件作者的最爱。
那么,为什么要投入时间和精力来破解 .NET 恶意软件呢?简而言之,网络威胁环境中充斥着使用 .NET 框架构建的恶意软件,要应对这些威胁,就必须深入了解其底层代码结构,并具备分析其操作复杂性的能力。掌握了相关知识和逆向工程技能,您就能解读恶意软件的机制,发现其攻击载体,并加强对这些威胁的防御。
本博客旨在揭开.NET逆向工程过程的神秘面纱,使其更加平易近人、易于理解。我们的旅程将使您掌握分析 .NET 文件的基本知识,并深入了解其有害功能和行为。首先,我们将深入探讨 .NET 可执行文件的格式,让您对这些文件的结构有一个扎实的了解。随后,我们将广泛介绍可用于逆向工程 .NET 恶意软件的工具和技术,确保您为应对这些威胁做好充分准备。
.NET 框架
微软公司开发的 .NET 软件开发框架和生态系统于 2002 年首次发布。它旨在提供一个可控的编程环境,让软件可以在基于 Windows 的操作系统上开发、安装和执行。随着.NET Core (https://github.com/dotnet/core)的推出,.NET 5 及以后的版本也接替了.NET Core,.NET 已经扩展到为 Linux 和 macOS 提供跨平台支持。与传统编程语言不同,.NET 不是一种语言,而是一个框架,支持多种托管语言,包括 C#、VB.NET 和 F#。
.NET框架包括一个称为框架类库(FCL)的大型类库,它为开发人员提供了从数据访问、加密到XML解析等一系列全面的即用、经过测试和优化的功能。这种广泛的支持和托管执行环境使.NET有别于需要更多人工干预此类任务的语言和框架。将这些功能结合在一起,就能创建一个高效的环境,适用于从网络、桌面到移动的各种应用。
.NET威胁状况
与 C/C++ 相比,.NET 框架具有用户友好的开发流程、丰富的功能集以及与 Windows 的平滑集成,因此恶意软件开发者可能更喜欢使用.NET 框架。然而,dnSpy 等工具使基于 .NET 的恶意软件的逆向工程变得更加简单,这促使这些创建者采用混淆方法来增加分析难度。此外,.NET 能够使恶意软件改变其行为或隐藏起来,这也增加了检测和逆向工程的难度。虽然 C/C++ 允许对系统资源进行更精细的控制,并能开发出更隐蔽、更高效的恶意软件,但它需要对系统的内部运作有更深入的了解。因此,.NET 对那些希望在开发速度和简便性之间寻求平衡的人来说更有吸引力。
.NET威胁环境在不断演变,攻击者经常利用适应性强、被广泛采用的.NET框架来制作和部署各种复杂的威胁。这种框架是许多网络威胁的基础,如臭名昭著的勒索软件Locky和Killnet。包括RedLine Stealer 在内的凭证窃取程序和CryptoClippy等银行木马也是基于 .NET 的威胁。此外,用.NET编写的破坏性擦除程序也在不断涌现,DoubleZero和最近发现的Hatef Wiper就说明了这一趋势。
此外,.NET 还有助于创建远程访问木马(RAT)。这方面的例子包括QuasarRAT和NanoCore,它们因功能丰富、易于修改和混淆而受到地下圈子的称赞。此外,.NET 也是创建恶意软件加载器的常用工具,这些加载器可以隐蔽地安装和执行其他类型的恶意软件。
.NET编译和运行过程
编译 - 管理代码
C#、F# 或 VB.NET等语言的执行由运行时控制。当合适的语言编译器编译源代码时,输出是中间语言(IL),也称为 MSIL(微软中间语言)、托管代码或通用中间语言(CIL)。
例如,在 .NET 框架中编译 C# 代码时,C# 编译器 (csc.exe) 的输出是一个 .NET 程序集。该程序集可以是独立程序的可执行文件(EXE),也可以是可重用库的动态链接库(DLL)。
这与 C/C++ 等非托管语言的编译不同,在非托管语言中,源代码被直接翻译成机器代码。然后,托管代码被打包成一个程序集,并附带一个包含必要元数据的清单。
在此过程中,托管代码的优点在于其可移植性和灵活性;同一个程序集可以在.NET支持的任何平台上运行,而无需重新编译。此外,托管代码允许跨语言继承代码访问的安全性。它还具有支持后期绑定的优势,方法调用可以在运行时而不是编译时解决。托管代码和程序集结构提供的这种抽象级别是.NET 框架的多功能性和优势的基石,使开发人员能够创建更安全、更易管理、更能适应变化的应用程序。
下图演示了 .NET 的编译和执行过程。我们使用 C# 进行演示,但它适用于所有 .NET 语言。
Compilation and execution of .NET.
Source(https://cihansol.com/blog/index. ... compiling-net-code/)
运行时执行 - 通用语言运行时(CLR)
通用语言运行时(CLR)是 Microsoft .NET 框架的重要组成部分,用于管理 .NET 程序的执行。它本质上是一个执行引擎,提供运行 .NET 应用程序所需的各种服务,无论这些程序是用哪种编程语言编写的。
执行 .NET 二进制程序时,CLR 会设置执行环境,但不会立即将所有托管代码转换为本地机器代码。即时编译器(JIT)会根据需要将托管代码转换为本地代码,并在调用方法时对其进行编译。这确保了在特定硬件上的高效执行。此外,.NET Core 和 .NET 5+ 中的NGEN和AOT编译等技术可以在执行前将托管代码预编译为本地代码,从而进一步提高性能。
CLR的功能不仅限于执行应用程序,它还提供内存管理、异常处理、垃圾回收、类型安全检查和安全性等关键服务。由 CLR 协调的内存管理抽象了开发人员手动分配和释放内存的需要,从而大大减少了内存泄漏和相关错误。所提供的自动垃圾回收功能可管理对象的生命周期,通过删除应用程序不再使用的对象来回收内存。
此外,CLR 执行严格的类型安全,有助于确保应用程序不会尝试执行不安全或未经验证的操作。CLR 还在 .NET 的安全架构中扮演重要角色,它提供代码访问安全(CAS),可根据分配给应用程序的信任级别控制程序可访问的资源。总之,CLR 创建了一个高级环境,减少了传统编程语言所需的许多低级编程任务,从而加快了开发周期,提高了生产率,并使应用程序更加安全可靠。这使得 CLR 成为 .NET 生态系统不可或缺的组成部分。
非托管函数
.NET框架中的非托管函数是指在通用语言运行时(CLR)的托管环境之外运行的代码。这些函数通常用 C 或 C++ 等语言编写,绕过 CLR 的管理,直接编译成特定于机器的代码。这意味着.NET托管环境固有的自动垃圾回收、类型安全和异常处理等功能并不适用于这些非托管函数。它们主要用于互操作性目的,允许.NET 应用程序使用非.NET 兼容语言编写的遗留代码或外部库。当需要使用现有的非.NET 库或调用只能通过非托管代码访问的系统级 API 时,这种功能是必不可少的。
不过,这也增加了复杂性和责任,因为开发人员必须手动处理内存管理和错误处理,增加了内存泄漏和安全漏洞等问题的可能性。在恶意软件分析中,了解非托管函数至关重要,因为它们可用于执行绕过托管环境中某些保护措施的代码,从而给分析和检测带来独特的挑战。
示例 - 非托管函数
要创建一个使用非托管函数的简单 .NET 程序,我们可以使用 C# 中的平台调用服务 (PInvoke)。PInvoke 允许托管代码从动态链接库(DLL)中调用非托管函数。
下面是一个示例,我们将调用标准 Windows 库 user32.dll 中的 MessageBox 函数:
using System;
using System.Runtime.InteropServices;
class Program
{
// Importing the MessageBox function from user32.dll
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern int MessageBox(IntPtr hWnd, String text, String caption, uint type);
static void Main(string[] args)
{
// Calling the MessageBox function - this is an unmanaged function call
MessageBox(new IntPtr(0), "Hello, World!", "Message Box", 0);
}
}
让我们探究一下上述代码的流程:
1.我们使用 DllImport 属性从 user32.dll 中导入 MessageBox 函数。这对于告诉 CLR 该函数是外部函数且不受 .NET 运行时管理是必要的。
2.MessageBox 函数的声明签名与 user32.dll 中的非托管函数相同。
3.在 Main 方法中,我们调用 MessageBox 函数。参数包括窗口句柄(我们传递 IntPtr(0),因为我们没有窗口句柄)、文本信息、标题和消息框类型(0 表示简单的确定按钮框)。
4.运行该程序后,将显示一个文本为 "Hello, World!"、标题为 "Message Box "的消息框。
.NET 程序集
.NET 程序集是 .NET 应用程序的基本构件,是一个或多个代码模块或资源文件的集合。生成的程序集内容包括:
◆中间语言(IL)代码:IL 是一种与 CPU 无关的指令集,可使同一程序集在 .NET 框架支持的不同平台上执行。
◆元数据:它描述了由 CLR 管理的结构元素,如程序集、类型(类、接口、枚举、结构体)、方法等。其中包括调试、垃圾回收、安全属性所需的信息,以及运行时管理代码所需的详细信息。
◆Manifest:是元数据中描述程序集本身的特定部分。它包括程序集的名称、版本、文化,可能还包括用于唯一标识程序集的强名称。元数据描述的是程序集内部的内容,而清单提供的是程序集整体的高层概览,确保程序集能与所依赖的其他程序集的正确版本进行交互。
深入了解 .NET 可执行文件格式
在本节中,我们将深入研究 .NET 内部结构。为了演示我们所涉及的概念,我们将使用一个示例--臭名昭著的旭日 (SolarWinds),以便您能跟上。Hash: 32519b85c0b422e4656de6e6c41878e95fd95026267daab4215ee59c107d6c77
我们将在演示中使用dnSpy、ILSpy 和PEStudio。
.NET程序集中的运行时头文件是 CLR 指定使用的可移植可执行文件(PE)格式中的重要元素。它包含了 CLR 正确执行 .NET 程序集所需的元数据和关键细节。该运行时标头是 PE 标头中的第 15 个数据目录条目,称为 "CLR 运行时标头"。
PE 文件中的数据目录就像索引或目录一样,列出重要部分并提供有关其位置和大小的信息。这种结构可以有效地访问 PE 文件的不同部分,如导入和导出表、资源,尤其是 .NET 程序集的 CLR 运行时头文件。
该条目描述了运行时头文件的相对虚拟地址(RVA)及其大小,从而引导 CLR 在加载时使用该头文件来管理 .NET 程序集的执行。
下面的截图显示了第 15 个数据目录条目,其标识为 .NET。
Analysis of a .NET file in PeStudio.
在该部分之后,我们将进入元数据头,它在组织这些数据流方面起着至关重要的作用。它提供了一个目录,列出了每个数据流、其大小以及在元数据部分中的偏移量。当 CLR 或像 dnSpy 或 ILDasm 这样的工具需要访问一段元数据时,它会查阅元数据头来找到相应的流。然后,它会导航到该数据流中的正确位置来读取数据。接下来,让我们来看看 .NET 元数据头内容中的一些关键字段:
◆签名总是相同的:BSJB (0x42534a42)。
◆GUID 是 128 位长的唯一标识符。
◆仅限 IL:该标志表示程序集只包含中间语言(IL)代码,不包含特定于 CPU 的本地代码。PE 有可能同时包含托管代码和非托管代码。
◆需要 32 位:设置该标志时,表示程序集需要 32 位运行环境,即使它运行在 64 位操作系统上。它通常用于依赖 32 位本地依赖关系或 32 位运行时特定行为的程序集。
◆已签名的强名称:表示程序集是否已使用强命名签名。强命名包括使用公钥/私钥对对程序集进行签名,提供唯一身份并确保程序集未被篡改。
◆流指的是包含特定类型元数据的结构化数据段。.NET 程序集元数据中的关键流包括:
◆#~ (Tilde) 流:主元数据流包含元数据表。这些表存储有关类型、方法、字段、参数和程序集中定义的其他元素的信息。Tilde 流的结构遵循 CLI 规范定义的元数据表模式。
◆#字符串流:该流存储元数据使用的字符串,如类型名称、方法名称和字段名称。元数据表中的引用(在 #~ 流中)指向该流中实际字符串值的偏移量。
◆#US(用户字符串)流:该流保存程序集中使用的字面字符串值,如字符串变量的默认值或代码中的字符串常量。元数据会引用这些字符串,尤其是在加载字符串文字的说明中。
◆#GUID 流:包含程序集使用的 GUID(全球唯一标识符)。该数据流中的每个条目都是 GUID,用于唯一标识元数据的某些方面,如模块版本 ID(MVID)。
◆#Blob 流:二进制大对象 "流存储用于各种目的的二进制数据,如字段默认值、方法签名、属性签名和编译信息。元数据表中的项目会引用该流,以获取详细的二进制信息。
◆#Pdb 流:该可选流包含调试信息,可将元数据和 IL 代码与源代码行和文件关联起来。
在某些.NET程序集查看器中访问可执行文件的元数据可能比较麻烦,因为它们的表示方法不同。下面的截图显示了 dnspy 和 ILspy 的区别。
ILSpy 中的元数据表
The metadata tables in dnspy.
元数据
.NET 中的元数据是一组描述程序元素及其特征的二进制信息。其中包括代码中定义的类型(类、接口、枚举等)、成员定义(方法、属性、字段、事件)、对其他类型和成员的引用以及程序集本身的信息。
元数据在 PE 文件中被分为多个表,统称为元数据表,用于存储这些描述性信息。每个表都遵循特定的模式,该模式概述了所含数据的结构和性质。下面是元数据表中的一些主要信息类型:
定义表:包含当前程序集中定义的代码信息。其中包括:
◆TypeDef 表: 源代码中定义的每个类或接口的详细信息,包括名称、可见性、基本类型以及包含的方法或属性。关键字段包括:
◆TypeName: 类型名称
◆TypeNamespace: 类型所属的命名空间
◆BaseType: An index into the TypeDef, TypeRef, or TypeSpec table, indicating the base class of the type.
◆Flags: 描述类型属性(可见性、抽象/密封状态等)
◆MethodDef 表: 每个方法的详细信息,包括名称、签名(参数和返回类型)以及与之相关的 IL 代码。关键字段包括:
◆Name: 方法的名称
◆Signature: 指向方法签名的 blob 索引,包括调用约定、返回类型和参数
◆RVA: 相对虚拟地址,指向 PE 文件中方法的实现
◆FieldDef 表: 每个字段(类变量)的详细信息,包括名称和类型。关键字段包括:
◆Name: 字段的名称.
◆Signature: 指向字段类型签名的 blob 索引
◆Flags: 指定字段属性,如可见性、静态/实例和仅初始化(只读)
Reference 表: 包含程序集外部但被程序集引用的代码信息。这包括:
◆TypeRef 表: 当前程序集引用的其他程序集中定义的类型信息。
◆MemberRef 表: 其他模块或程序集定义的成员(方法、属性等)的描述。
Manifest Metadata 表: 介绍程序集本身,包括:
◆Assembly 表: 有关程序集的信息,如名称、版本、文化和强名称签名。
◆AssemblyRef 表: 该程序集依赖的其他程序集的详细信息,包括其名称、版本和公钥(如果它们是强命名的)。
◆Other Metadata 表: 除上述表格外,还有其他几个表格存储信息,如:
Module 表: 有关当前模块的信息,如名称和唯一标识该模块的 GUID(全球统一标识符)。
CustomAttribute 表: 包含应用于组件内各种元素的自定义属性的详细信息。
Event 表 和 Property 表: 描述类型中声明的事件和属性。
Param 表: 方法参数信息
StandAloneSig 表:独立签名可用于封装类型或方法签名。
Constant 表: 存储代码中定义的常量。
这些表对 CLR 的运行至关重要,因为它们提供了执行程序集所需的上下文信息。运行时读取这些表以执行各种任务,如类型实例化、方法调用、安全验证等。
这些元数据表以高度优化的二进制格式编码,运行时可对其进行高效处理。通过反射,还可以以编程方式访问元数据,允许 .NET 应用程序在运行时检查自己的结构或其他程序集的结构。这种自省能力是 .NET 框架的强大功能之一,可以实现一系列动态编程方案。
元数据唯一标识符 (ID)
元数据标记是 CLR 用来引用程序集元数据表中元数据元素的唯一标识符。这些表中的每个条目都会分配一个元数据标记,作为对该特定项目的稳定引用。
元数据标记对于 CLR 与编译代码的交互至关重要,因为它们为运行时提供了一种有效识别和访问元数据的方法。PE 文件中的每个类型、成员、签名或其他元数据描述符都有一个相应的标记。
在 dnSpy 中,每个方法的声明上方都有一个注释,信息包括Token、RID、RVA 和文件偏移量--如下面的截图所示。
让我们来看看这些字段的含义:
1.Token:元数据标记的高字节(big-endian)指定元数据的类型,便于运行时识别。它表示标记指的是元数据中的哪个表(TypeDef、TypeRef、MethodDef 等)。有关标记类型值的更多信息,请参阅附录 A。
2.行索引(RID):元数据标记的其余 24 位用于索引相关的元数据表。它们表示可以找到该元素实际元数据的行号。由于每个表可能有数百万个条目,因此 24 位可以提供足够的索引范围。
3.RVA:RVA(Relative Virtual Address)是方法体(即编译后的IL代码)相对于将汇编加载到内存的基地址的地址。例如,0x00023B28 表示方法的 IL 代码从加载模块基地址的内存偏移量开始。CLR 使用 RVA 在运行时定位并执行方法的代码。在 PE(可移植可执行文件)文件(Windows 上 .NET 程序集使用的格式)中,当文件加载到内存中时,RVA 被广泛用于引用文件的各个部分。
4.文件偏移:该值表示方法的 IL 代码在磁盘上实际 .NET 程序集文件(.dll 或 .exe 文件)中的位置。0x00021D28 表示从文件开头到方法代码开始处的偏移量(以字节为单位)。这对于直接进行二进制分析或操作汇编文件非常有用,因为它可以准确地告诉您在文件的哪个位置可以找到方法的代码。
元数据标记的结构可以提高 CLR 在运行时解析引用的性能。例如,当 JIT 编译器需要将 IL 编译为本地代码时,它会使用元数据标记来查找方法签名、类型信息等。元数据标记系统还支持 CLR 的动态特性,如反射。它使各种运行时服务(如类型安全、安全检查和跨语言互操作性)得以有效执行。
元数据表示例
以下是 SolarWinds 恶意软件的Start函数。
The token and RID field in of the start function in dnspy.
Token值的高位(0x6)对应元数据表编号 6,即方法(MethodDef)表。标记值的低位是 0x5fa (1530),即方法表中的条目编号。
查看start 方法的元数据下部,0x00058D66 是可执行文件开始处的偏移量,偏移量的值(0x1EC15)是 #String 流中的偏移量。
#字符串流中的偏移量,其中将包含方法名称:Start。让我们看看在 dnSpy 的 Hex 编辑器中是如何显示的:
字符串流中的偏移值。DnSpy 会自动检测字符串的值。
要查看字符串流中的数据,我们将执行以下操作:
在新窗口中,转到偏移量(相对于字符串流的起始位置 - RVA),查看我们要查找的字符串 -起始位置。
The offset in the string Start in the Strings stream.
体现
.NET 清单是 .NET 程序集的关键组件,是描述程序集中各元素如何相互关联的元数据中心。无论程序集是静态的还是动态的,它都嵌入在每个程序集中,并包含程序集运行所需的基本数据,包括版本要求、安全标识、作用域定义以及资源和类引用的解析。
.NET 清单的主要功能是提供全面的元数据描述,以方便程序集的识别、版本控制和依赖性管理。它可以确保程序集是自描述的,有助于类型引用的解析以及将这些引用映射到包含其声明和实现的文件。这对于维护版本控制、确保不同程序集和依赖程序集的组件之间的兼容性尤为重要。
NET Manifest 的内容
清单中包含了对于程序集的身份标识和运行至关重要的各种信息:
◆Assembly Name: 指定程序集名称的文本字符串。
◆Version Number: 包括主版本号和次版本号,以及修订号和构建号,由通用语言运行时用来执行版本策略。
◆Culture Information: 指定程序集支持的文化或语言,这对于包含特定文化或语言信息的卫星程序集尤为重要。
◆List of Files in the Assembly: 包括程序集所含每个文件及其名称的哈希值,确保程序集的完整性和完备性。
◆Type Reference Information: 运行时用于将类型引用映射到包含其声明和实现的文件,这对类型安全和正确性至关重要。
◆Information on Referenced Assemblies: 列出当前程序集静态引用的其他程序集,包括它们的名称、元数据(如版本、文化、操作系统)以及公钥(如果它们是强命名的)。
Manifest in dnSpy.
方法主体结构
在.NET中,方法主体结构可以用两种格式编码,分别称为 "Tiny "格式和 "Fat "格式,每种格式都根据方法的复杂性和要求有不同的用途。.NET编译器会根据编译方法的复杂程度在Tiny和Fat头之间做出选择。
Tiny 头文件是两种格式中较为简单的一种。当一个方法满足特定条件时,就会使用 tiny 头文件:
◆方法小于 64 字节。
◆堆栈深度不超过 8 个插槽(栈中每个项目一个插槽,与项目大小无关)。
◆不包含局部变量或结构化异常处理程序(SEH)。
tiny 头更加紧凑,并针对小型方法进行了优化。
tiny 头是一个单字节长的数据,其中低 2 位设置为 0x2(二进制 10),表示这是一个 tiny 头,其余 6 位表示方法主体的大小(字节)。这种紧凑的格式可以高效地存储小方法体,减少简单方法的元数据资源占比。
字段
大小 (Bits)
描述
Header Flag
2
始终设置为 `10`(b),表示 Tiny 标头。
Method Size
6
以字节为单位指定方法主体的大小(最大 63 字节)
The Fat header is used for more complex method bodies that exceed the limitations of the Tiny header:
◆tiny头文件用于不符合小头文件标准的大型方法。
◆它提供了额外的信息,如方法的局部变量和 SEH。
◆当一个方法的大小或复杂度超过 tiny header 的限制时,它就会使用 fat header 格式。
Fat 头文件较大,由多个字段组成,其中包括一个标志字段,用于指示方法主体的其他特征(如异常处理条款或局部变量初始化)。
字段
大小 (Bits)
描述
Flags
2
指定方法主体的属性,包括是否存在局部变量、init locals 等。设置为 1 时,最低位(0x3)表示 Fat 头信息。
Size
2
以 4 字节为单位的头部大小。包括整个头部的大小,而不仅仅是方法主体的大小。
MaxStack
2
方法执行过程中操作数堆栈上任意点的最大项目数。
CodeSize
4
方法主体 IL 代码的大小(以字节为单位)。
LocalVarSigTok
4
局部变量签名的元数据标记,只有在方法有局部变量时才会出现。
More sections
Variable
用于异常处理条款等的附加部分,如果在 Flags 中指定,则会出现。
有关这些标头结构的更多详细信息,请查看此资源(https://www.ntcore.com/files/dot ... 20Method%20format.-,Methods,-Every%20method%20contained)。
方法主体示例
让我们以 DeleteDiscoveryProfileInternal 函数为例:
点击偏移量(或 RVA),我们将在 HEX 视图窗口中看到该方法的标题:
在这里我们可以看到,一旦我们将鼠标悬停在页眉的字节上,DnSpy 就会高亮显示页眉字段。函数的内容--指令紧随头之后,在 dnSpy 中表示为 image_core_ilmethod_fat.instruction[]。为了更好地理解和查看指令值(操作码),我们将在 IDA 中打开恶意软件:
The function in IDA.
左边我们可以看到每个操作码的值,后面是 dnSpy 的前两条指令:
总结
在对.NET可执行文件结构的初步探索中,我们深入研究了.NET框架的复杂性,强调了它在合法开发和恶意软件制作中的双重用途。通过剖析 .NET 编译过程、运行时执行以及元数据和程序集的复杂细节,我们为了解 .NET 应用程序如何运行以及如何被恶意操纵奠定了基础。在深入学习.NET恶意软件逆向工程技术的过程中,这将为您掌握有效分析和应对基于.NET的威胁的技能和知识奠定基础。
附录
标识符类型表
Token 类型
Value (Hex)
描述
Module
0x00
引用模块定义。
TypeRef
0x01
引用另一个模块中的一个类型。
TypeDef
0x02
定义模块中的一个类型。
FieldDef
0x04
在类型中定义字段
MethodDef
0x06
在类型中定义方法
ParamDef
0x08
定义方法的参数
InterfaceImpl
0x09
定义类型的接口实现。
MemberRef
0x0A
引用其他模块中的字段或方法。
CustomAttribute
0x0C
定义自定义属性
Permission
0x0E
定义声明式安全权限
Signature
0x11
定义独立签名
Event
0x14
在类型中定义事件
Property
0x17
在类型中定义属性
ModuleRef
0x1A
引用外部模块
TypeSpec
0x1B
使用签名指定类型
Assembly
0x20
定义程序集元数据
AssemblyRef
0x23
引用另一个程序集
File
0x26
定义与程序集相关的外部文件
ExportedType
0x27
定义从其他程序集导出的类型
ManifestResource
0x28
定义嵌入资源
GenericParam
0x2A
定义类型或方法的通用参数
MethodSpec
0x2B
为通用方法指定方法实例化。
GenericParamConstraint
0x2C
指定泛型参数的约束。
补充资料
非原文内容,只是收集相关知识点的资料方便阅读理解。
泛型
在编程中,泛型允许我们编写可以处理多种数据类型的灵活代码。当我们定义泛型类、结构、接口或方法时,我们可以为泛型参数指定约束,以确保这些参数满足某些特定的要求或条件。例如,在C#中,我们可以使用where关键字来指定泛型参数的约束,比如要求它必须实现某个接口,或者继承自某个基类。这些约束有助于在编译时捕获错误,并确保泛型代码的正确性。
Tiny 头
在.NET等平台的元数据格式中,Tiny 头部是一种用于表示小型方法体的优化手段。由于很多方法体都非常小,使用完整的元数据格式来存储它们会浪费空间。因此,Tiny 头部允许以一种更紧凑的方式来表示这些小型方法体,从而提高了元数据的存储效率。当解析元数据时,如果遇到了Tiny 头部,解析器就知道如何根据这6位来确定方法体的大小,并据此读取和解释方法体的内容。
槽位
栈的使用量被限制在最多8个槽位以内,这通常是为了避免栈溢出(stack overflow)错误,因为当栈深度超过其容量时,就会发生这种错误。限制栈深度为8个槽位可能是出于性能、安全性或设计上的考虑。
需要注意的是,这里的“一个槽位对应栈上的一个项,无论该项的大小如何”表明,每个槽位只能存储一个堆栈项,且槽位的大小是固定的,不会因栈项的大小而改变。这意味着,无论堆栈项是占用少量内存还是大量内存,它们在栈中都只占用一个槽位。
程序集
在计算机科学中,特别是在.NET框架中,程序集(Assembly)是包含编译代码(如MSIL或本机代码)、元数据以及程序集清单的资源文件集合。程序集清单是元数据的一部分,它提供了关于程序集的描述性信息,这些信息对于程序集的识别、版本控制、依赖关系管理以及安全等方面至关重要。
具体来说,程序集清单可能包含以下信息:
1.程序集名称:唯一标识程序集的名称。
2.版本信息:标识程序集的版本,用于版本控制和依赖关系管理。
3.文化信息:指定程序集支持的语言或区域文化。
4.公钥标记:如果程序集是强签名的,则包含用于验证程序集完整性和发布者身份的公钥信息。
5.依赖关系:列出程序集所依赖的其他程序集或库。
6.文件列表:包含程序集中所有文件的列表及其哈希值,用于验证文件的完整性。
这些信息确保了程序集的正确性、一致性和安全性,并使得.NET运行时环境能够正确地加载、执行和管理程序集。
类型属性
在编程中,类型属性通常用于描述一个类型(如类、结构、接口等)的特性或状态。这些属性可以帮助编译器、运行时环境或开发者理解如何与这些类型交互。以下是几个常见的类型属性及其描述:
1.可见性(Visibility):
决定了类型在代码中的可访问性。例如,一个类型可以是公开的(public),意味着它可以从任何其他代码访问;或者它可以是私有的(private),意味着它只能在其定义的范围内被访问。其他可见性级别还包括受保护的(protected)和内部的(internal)。
2.抽象(Abstract):
指示一个类型是不完整的,并且不能被实例化。抽象类型通常包含抽象成员(如抽象方法或属性),这些成员必须在任何非抽象派生类中实现。抽象类通常用作基类,为派生类提供通用的结构和行为。
3.密封(Sealed):
阻止其他类型从该类型继承。密封类不能被继承,这有助于防止对类的意外扩展,从而保持类的行为的一致性。密封也可以应用于方法,以防止它们在派生类中被重写。
4.静态(Static):
指示类型不包含实例成员,并且不能被实例化。静态类型只能包含静态成员,这些成员属于类型本身,而不是类型的任何特定实例。
5.其他属性:
还可能包括其他特定于语言或框架的属性,如是否可序列化、是否安全(在安全上下文中)、是否实现了特定接口等。
这些属性通常在类型定义时通过特定的语法或元数据来指定,并且它们对于确保类型的正确使用、维护代码的一致性和安全性至关重要。
非托管函数
非托管函数(Unmanaged Functions)指的是在操作系统或底层硬件上直接执行的代码,而不依赖于特定运行时环境(如.NET或Java虚拟机)的管理。这些函数通常由C或C++等语言编写,并直接编译成机器码。与托管代码相比,非托管代码具有更高的性能和更直接的系统访问能力,但同时也带来了更大的编程复杂性和安全风险。
非托管函数通常用于执行底层任务,如内存管理、硬件访问和系统调用等。它们不受高级语言运行时环境的限制,因此可以实现更高的执行效率和更精细的控制。然而,这也意味着程序员需要承担更多的责任,包括内存管理、错误处理和线程同步等。
在跨平台或混合编程环境中,非托管函数通常用于与底层系统或第三方库进行交互。例如,在.NET或Java应用程序中,可以通过平台调用(P/Invoke)或Java本地接口(JNI)等技术调用非托管函数。这些技术允许程序员在高级语言中利用非托管代码的功能,同时保持代码的灵活性和可维护性。
需要注意的是,由于非托管函数直接操作底层系统资源,因此在使用时需要格外小心。错误的内存访问、指针操作或系统调用可能导致程序崩溃、数据损坏或安全漏洞等问题。因此,在编写和使用非托管函数时,应遵循最佳实践,并进行充分的测试和验证。
|
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有帐号?立即注册
x
|