提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
使用CIL反编译代码时,经常会发现有些类(class)被标记了beforefieldinit,另外一些类则没有此标记。这个beforefieldinit修饰符到底有什么作用?本文将尝试一探究竟。
一、实例构造器.ctor和类构造器.cctor
介绍beforefieldinit修饰符之前,有必要先介绍一下实例构造器(instance constructor)和类构造器(class constructor)。
先看一个我们再熟悉不过的例子:
//解决方案:CILTest
public class MyClass
{
public static int age; //静态(类)字段
public string name; //普通(实例)字段
static MyClass() {
age = 18; } //静态构造函数 class constructor
public MyClass(string name) //实例构造函数 instance constructor
{
this.name = name;
}
}
class FunctionTest
{
static void Main()
{
MyClass myClass = new MyClass("Robert");
}
}
这里例子中,有两点需要引起注意:
- 首先,静态构造器必须是无参的,不能使用类型修饰符(如int, string, void等),也不能使用访问修饰符(如public, private)。因为无返回值,无参数,方法名称唯一,所以签名已经固定,不会存在重载,一个类中最多只能有一个静态构造函数。实例构造函数没有这些限制,所以可以有1或多个重载(一般通过使用不同参数列表来实现)。
- 其次,实例构造函数不仅可以初始化实例成员,也可以初始化静态成员;但而静态构造函数只能初始化静态成员,否则会报语法错误。
一、C#与CIL
C#源程序经过编译器csc编译后(Visual Studio实际是使用MsBuild调用的csc.exe),会生成以dll为扩展名的二进制字节码文件,比如本例代码会被编译成CILTest.dll(注意:对于.NET Framework应用程序来说,如果针对Windows平台,Visual Studio会将其编译成exe。但对于.NET,因为是跨平台框架,所以所有应用都是编译成dll。当然Windows环境下也会生成一个exe,但它其实只是一个dll文件启动器,真实代码还是在dll中)。托管dll文件格式是标准PE/COFF文件格式的扩展,其中包含了C#被编译后生成的CIL字节码。
备注:CIL语言是.NET平台的底层语言,无论C#, Visual Basic抑或F#,都是被编译成CIL语言而非本机代码,这些CIL字节码最后会被CLR的即时编译器Jitter在运行时编译成本机代码执行。所以,CIL这种中间语言是平台(操作系统)及架构(CPU)无关的,基于栈的非常简单易用的中间语言,在它的加持下,跨平台才成为可能。
使用反编译工具(ILSpy)可以查看dll文件中的CIL语言代码,比如用ILSpy打开上面的CILTest.dll示例,选中MyClass类,就可以看到CIL代码格式定义的MyClass(为方便不熟悉CIL的同学,简单加了些许注释):
.class public auto ansi MyClass extends [System.Runtime]System.Object
{
.custom instance void [System.Runtime]System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = (
01 00 01 00 00
)
.custom instance void [System.Runtime]System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = (
01 00 00 00 00
)
// Fields
.field public static int32 age
.field public string name
// Methods
.method private hidebysig specialname rtspecialname static
void .cctor () cil managed
{
// Method begins at RVA 0x2050
// Header size: 1
// Code size: 9 (0x9)
.maxstack 8
/* 0x00002051 00 */ IL_0000: nop
/* 0x00002052 1F12 */ IL_0001: ldc.i4.s 18 //将整数18压如求值栈
/* 0x00002054 8001000004 */ IL_0003: stsfld int32 MyClass::age //将求值栈中的18保存到age字段
/* 0x00002059 2A */ IL_0008: ret
} // end of method MyClass::.cctor
.method public hidebysig specialname rtspecialname
instance void .ctor (
string name
) cil managed
{
// Method begins at RVA 0x205a
// Header size: 1
// Code size: 16 (0x10)
.maxstack 8
/* 0x0000205B 02 */ IL_0000: ldarg.0 //将第1个参数(this指针)压入求值栈
/* 0x0000205C 280E00000A */ IL_0001: call instance void [System.Runtime]System.Object::.ctor() //调用父类构造函数
/* 0x00002061 00 */ IL_0006: nop //空操作
/* 0x00002062 00 */ IL_0007: nop
/* 0x00002063 02 */ IL_0008: ldarg.0
/* 0x00002064 03 */ IL_0009: ldarg.1 //将第2个参数(name)压入求值栈
/* 0x00002065 7D02000004 */ IL_000a: stfld string MyClass::name //将参数值保存到name字段
/* 0x0000206A 2A */ IL_000f: ret
} // end of method MyClass::.ctor
} // end of class MyClass
从上面的CIL代码中,我们可以找到MyClass类,也可以找到name和age字段,唯独找不到两个构造函数static MyClass()和public MyClass(string name)。这是因为,CIL语言规定:实例构造函数名称一律使用.ctor(constructor的简写),静态构造函数名称一律使用.cctor(class constructor的简写)。因此,在C#中使用类型名作为类的构造函数是C#语法的规定,一旦被编译器编译成CIL字节码,就必须遵循CIL语言规定了。
二、最简单的类
为了研究的目的,我们首先放弃上述示例,重新创建一个最简单的类,如下所示:
public class MyClass{
}
显然,这个MyClass类中什么内容都没有写。接下来我们看看它会对应什么模样的CIL:
.class public auto ansi beforefieldinit MyClass
extends [System.Runtime]System.Object
{
// Methods
.method public hidebysig specialname rtspecialname
instance void .ctor () cil managed
{
// Method begins at RVA 0x2050
// Header size: 1
// Code size: 8 (0x8)