.net core底层入门学习笔记(一-介绍与IL)

.net core底层入门学习笔记(一)

今天开始有空开启一篇基础的.net core学习笔记


文章目录

  • .net core底层入门学习笔记(一)
  • 前言
  • 一、公共语言运行时
  • 二、Intermediate Language
  • 三、IL指令格式
  • 四、属性访问
  • 总结


前言

最近两年微软大力发展.net core,看重了跨平台与开源的潜力,是时候学习一波.net core的基础了。


一、公共语言运行时

基础概念:

  • CLI(Common Language Infrastructure):通用中间语言,在.net环境中我个人认为就是IL语言,注意区分CLI(Command-Line Interface),容易混淆,这里是命令行接口,一般叫.net cli。
  • CLR(Common Language Runtime):公共语言运行时,算是.net里面最为核心的部分了,主要的能力是解析中间代码,把中间代码转换成对应平台的机器码,并驱动当前计算机执行这些代码。所以有了这个能力,才算是有了跨平台的能力,与JAVA中的JVM比较像。

基础概念:CLR的组成部分:

  • 中间代码解析:解析IL代码,解析IL代码,找到对应的模块,类型,成员,方法,并把方法翻译成对应平台的机器码,同时生成元数据(个人理解为描述代码的一种数据结构)用于支持一些特殊的机制,比如:反射,类型安全,GC,异常处理等。
  • 中间代码编译:上面解析之后,会生成元数据,而此部分则直接利用.net中的JIT编译器编译中间代码为能够被对应平台执行的机器码(叫做托管代码)
  • 类型安全:类型安全是指.net能保证对象的类型一定是正确的,不论对它进行了何种操作。比如把.net对象转换为基础对象object,传给其他.net程序,其他.net程序依然可以通过元数据支持的机制,getType方法得到该对应的真实类型。或者某些转换会直接触发异常,这样能保证类型一定是安全的。CLR会在每个引用类型的对象中直接保存类型信息,对象转换时会根据这个信息,判断转换是否合法,这些由CLR管理的对象又叫托管对象。
  • 异常处理:传统的机制,使用函数返回值通知与处理错误(这也太麻烦了),.net可以在任意函数中抛出任何异常,开发者可以在范围内捕捉这些异常并处理。
  • 线程管理:现代操作系统都提供了线程机制,CLR封装了不同平台与操作系统的线程(又叫托管线程),使得在不同平台上可以用相同的方式使用多线程。
  • 垃圾回收:CLR通过GC帮助开发者自动回收内存,不需要开发者手动去管理内存的释放。当然CLR只能释放托管对象占用的内存,非托管对象在CLR中需要开发者手动释放,比如一些IO操作等。当然.net对一些常用的非托管资源进行了封装,用.net封装好的方式,就不需要大家手动管理了,比如文件读写的句柄,可以用原生方式,更好的是用.net提供的System.IO.File类,这样就不用手动管理释放了。

二、Intermediate Language

IL是.net中间代码的称呼,学习中间代码对.net能够有更加深入的了解。

学习方法:对比C#代码与生成好的IL代码
学习工具:反编译代码,我用的是dotpeek。

举例:C#代码如下:

using System;

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

相关解析图:
.net core底层入门学习笔记(一-介绍与IL)_第1张图片

1.首先依赖的程序集:System.Console,System.Runtime
2.定义的程序集与程序集相关属性:
.net core底层入门学习笔记(一-介绍与IL)_第2张图片
3.定义程序集模块,与模块中的元数据:包含了各种类型定义,方法定义等。
.net core底层入门学习笔记(一-介绍与IL)_第3张图片
4.类型定义IL代码如下:

// Type: develop.Program 
// Assembly: develop, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
// MVID: C8D04018-0EE5-4568-B55E-992F5FC9C40D
// Location: D:\develop\bin\Debug\net5.0\develop.dll
// Sequence point data from D:\develop\bin\Debug\net5.0\develop.pdb

.class private auto ansi beforefieldinit
  develop.Program
    extends [System.Runtime]System.Object
{

 省略部分代码
 }

.class private auto ansi beforefieldinit:

  • private :类型可访问性,此处为私有,即同一程序集内可见
  • auto:布局风格,此处为自动,理解为字段等数据结构在类中的布局方式,方便后续读取处理类中各种字段数据等。
  • ansi:与非托管代码交互时,字符串转换模式,此处为ansi
  • beforefieldinit:类型中的方法执行时,不要求静态构造方法先执行。有些类中的静态字段是默认值,或者就没有静态字段,那么就可以用这个标识。个人理解出一个意思是,静态字段的非默认值相当于在静态构造函数中执行的?
  • extends [System.Runtime]System.Object:继承于System.Runtime中的System.Object类。

5.方法定义

 .method private hidebysig static void
    Main(
      string[] args
    ) cil managed
  {
    .entrypoint
    .maxstack 8

    // [8 9 - 8 10]
    IL_0000: nop

    // [9 13 - 9 47]
    IL_0001: ldstr        "Hello World!"
    IL_0006: call         void [System.Console]System.Console::WriteLine(string)
    IL_000b: nop

    // [10 9 - 10 10]
    IL_000c: ret

  } // end of method Program::Main

  .method public hidebysig specialname rtspecialname instance void
    .ctor() cil managed
  {
    .maxstack 8

    IL_0000: ldarg.0      // this
    IL_0001: call         instance void [System.Runtime]System.Object::.ctor()
    IL_0006: nop
    IL_0007: ret

  } // end of method Program::.ctor

主要注意的关键字:

  • hidebysig:隐藏方法签名
  • cil managed:实现方式,此处表示是在IL中实现的,对应还有:native(本机),runtime(运行时内部),optil(优化的IL)
  • .cto 与.cctor方法,是自动生成的,构造函数与静态构造函数
  • entrypoint:入口方法标志
  • maxstack:评价堆栈中最多保存元素数量,后续介绍评价堆栈

三、IL指令格式

格式组成:标签 操作 参数
例子: IL_001:(标签) ldc.i4.(操作) 1(参数)
功能:将常量1放入评价堆栈
IL中有许多类似的操作命令,其中有个曾经困惑我的fld相关的指令,比如:ldfld。意思是:从评价堆栈取出一个值,读取这个值的指定字段。
解惑:结合了非静态方法会利用ldarg,将方法的第一个参数放入评价堆栈,非静态方法第一个参数是this,指向对象,ldfld会从评价堆栈取出这个对象,然后读取他的字段值。
测试代码:

using System;

namespace dotnet_test
{
    class Program
    {
        static void Main(string[] args)
        {
           
            var obj = new MyClass();
            Console.WriteLine("Hello World!");
            Console.WriteLine(obj.testIncrement());
        }
    }

    class MyClass
    {
        private int testCount;

        public int testIncrement()
        {
            return ++testCount;
        }
    }
}

对应的IL代码:

.class private auto ansi beforefieldinit
  dotnet_test.MyClass
    extends [System.Runtime]System.Object
{

  .field private int32 testCount

  .method public hidebysig instance int32
    testIncrement() cil managed
  {
    .maxstack 3
    .locals init (
      [0] int32 V_0,
      [1] int32 V_1
    )

    // [21 9 - 21 10]
    IL_0000: nop

    // [22 13 - 22 32]
    IL_0001: ldarg.0      // this
    IL_0002: ldarg.0      // this
    IL_0003: ldfld        int32 dotnet_test.MyClass::testCount
    IL_0008: ldc.i4.1
    IL_0009: add
    IL_000a: stloc.0      // V_0
    IL_000b: ldloc.0      // V_0
    IL_000c: stfld        int32 dotnet_test.MyClass::testCount
    IL_0011: ldloc.0      // V_0
    IL_0012: stloc.1      // V_1
    IL_0013: br.s         IL_0015

    // [23 9 - 23 10]
    IL_0015: ldloc.1      // V_1
    IL_0016: ret

  } // end of method MyClass::testIncrement

有几个问题:
1.为啥会调用两次 ldarg.0就是将非静态方法的第一关参数(this,注释里面也有标注出来)放入评价堆栈。为啥会有两次呢,因为++testCount,首先会从this中读取一次testCount值,然后add指令之后,将结果会再次存入this的testCount中去。至于为啥这么智能知道先就调用两次将this放进去评价堆栈呢,这个就是.net的编辑器牛逼之处了,再书的最后两章有给出解释。

2.它调用ldfld怎么知道是哪个字段呢,我们再加一个字段进去看看,后面的int32 dotnet_test.MyClass::testCount指定了字段的完整路径

 class MyClass
    {
        private int testCount;
        private int testCountA;

        public int testIncrement()
        {
            testCountA++;
            return ++testCount;
        }
    }
.method public hidebysig instance int32
    testIncrement() cil managed
  {
    .maxstack 3
    .locals init (
      [0] int32 V_0,
      [1] int32 V_1
    )

    // [22 9 - 22 10]
    IL_0000: nop

    // [23 13 - 23 26]
    IL_0001: ldarg.0      // this
    IL_0002: ldarg.0      // this
    IL_0003: ldfld        int32 dotnet_test.MyClass::testCountA
    IL_0008: ldc.i4.1
    IL_0009: add
    IL_000a: stfld        int32 dotnet_test.MyClass::testCountA

    // [24 13 - 24 32]
    IL_000f: ldarg.0      // this
    IL_0010: ldarg.0      // this
    IL_0011: ldfld        int32 dotnet_test.MyClass::testCount
    IL_0016: ldc.i4.1
    IL_0017: add
    IL_0018: stloc.0      // V_0
    IL_0019: ldloc.0      // V_0
    IL_001a: stfld        int32 dotnet_test.MyClass::testCount
    IL_001f: ldloc.0      // V_0
    IL_0020: stloc.1      // V_1
    IL_0021: br.s         IL_0023

    // [25 9 - 25 10]
    IL_0023: ldloc.1      // V_1
    IL_0024: ret

  } // end of method MyClass::testIncrement

从上面可以看到指定的字段,包含字段类型,命名空间,类名,字段名。
IL命令具体可以查看微软的官方文档:链接地址

此外,需要理解评价堆栈,是一个栈结构,先进后出,市面上很多代码执行器,都会用这种方式使得指令之间能够互相交互值。特别指出,如果想要自己写一个代码执行器(比如:qyscript,ILRuntime)都可以参考这种方式。
再则就是注意方法内的本地变量的定义如下:
.locals init (
[0] int32 V_0,
[1] int32 V_1
)
可以看到C#代码中++testCount,并没有使用任何临时变量,但是IL代码中会自动根据需要,增加本地变量的定义,++testCount,利用本地变量进行add指令操作,注意返回值是如何通过本地变量返回的。

四、属性访问

using System;

namespace dotcore_test
{
    class Program
    {
        static void Main(string[] args)
        {
            var obj = new MyClass();
            obj.Test = "5555";
            Console.WriteLine("Hello World!");
            Console.WriteLine(obj.Test);
        }
    }

    class MyClass
    {
        public string Test { get; set; }
    }
}
/ Type: dotcore_test.MyClass 
// Assembly: dotcore_test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
// MVID: 9AD8FB1B-C055-4CB0-B5AE-701BCCD0F0FB
// Location: E:\dotcore_test\bin\Debug\net5.0\dotcore_test.dll
// Sequence point data from E:\dotcore_test\bin\Debug\net5.0\dotcore_test.pdb

.class private auto ansi beforefieldinit
  dotcore_test.MyClass
    extends [System.Runtime]System.Object
{

  .field private string '<Test>k__BackingField'
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
      = (01 00 00 00 )
    .custom instance void [System.Runtime]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [System.Runtime]System.Diagnostics.DebuggerBrowsableState)
      = (01 00 00 00 00 00 00 00 ) // ........
      // int32(0) // 0x00000000

  .method public hidebysig specialname instance string
    get_Test() cil managed
  {
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
      = (01 00 00 00 )
    .maxstack 8

    // [18 30 - 18 34]
    IL_0000: ldarg.0      // this
    IL_0001: ldfld        string dotcore_test.MyClass::'<Test>k__BackingField'
    IL_0006: ret

  } // end of method MyClass::get_Test

  .method public hidebysig specialname instance void
    set_Test(
      string 'value'
    ) cil managed
  {
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
      = (01 00 00 00 )
    .maxstack 8

    // [18 35 - 18 39]
    IL_0000: ldarg.0      // this
    IL_0001: ldarg.1      // 'value'
    IL_0002: stfld        string dotcore_test.MyClass::'<Test>k__BackingField'
    IL_0007: ret

  } // end of method MyClass::set_Test

  .method public hidebysig specialname rtspecialname instance void
    .ctor() cil managed
  {
    .maxstack 8

    IL_0000: ldarg.0      // this
    IL_0001: call         instance void [System.Runtime]System.Object::.ctor()
    IL_0006: nop
    IL_0007: ret

  } // end of method MyClass::.ctor

  .property instance string Test()
  {
    .get instance string dotcore_test.MyClass::get_Test()
    .set instance void dotcore_test.MyClass::set_Test(string)
  } // end of property MyClass::Test
} // end of class dotcore_test.MyClass

属性是C#中比较特别的,可以将其看做特别的方法。
属性定义:

.property instance string Test()
  {
    .get instance string dotcore_test.MyClass::get_Test()
    .set instance void dotcore_test.MyClass::set_Test(string)
  } // end of property MyClass::Test

指明get与set方法对应的实际方法

总结

.net编译器转换C#等代码为IL代码,CLR执行时将其转换为对应的机器码,这样能够做到跨平台。学习IL代码相关知识,能够更好的帮助我们认识具体底层的机制与原理。

你可能感兴趣的:(.NET,c#)