探索CLR原理系列(3):方法元数据和IL(适合老鸟,新人勿沉迷其中)

时间:2021-12-19 14:55:14

前一篇我们探索了类型的第一种成员:字段。字段在IL编译时,会生成MdToken和偏移量,因为对于类型来说,一个类型在编译时就已经确定了字段的个数,所以偏移量对于编译器来说是已知的,字段和偏移量分别由元数据表(Field和ClassLayout)来记录。

本篇我们来讨论类型中的另一种成员:方法.在本系列的第一篇探索CLR原理系列(1):类型 中我们说到类型中只有两种成员:字段和方法.字段是用来描述类型的状态,而方法则提供了类型所具备的功能.首先我们来看看方法在IL中是如何描述的.先来定义一个类型.

 

  public  class MethodClass
    {
         private  string name= " xulei ";
         private int age= 10;
         public static int  Add(int a,ref int b)  // 静态
        {
            return a + b;    
        }
         private static int  Sub(out int a,out  int b)  // 私有静态
        {
            a =  10;
            b =  5;
            return a - b;
        }
         public  void WriteName()  // 公有
        {
            Console.Write(name);
        }
         protected int GetAge()   // 受保护
        {
            return age;
        }
         private  void WriteNameAndAge()  // 私有
        {
            Console.Write(name + age.ToString());
        }

这个类型中我们定义了2个不同访问级别静态方法,3个不同访问级别的实例方法.下面我们来看看这些方法的元数据。

TypeDef # 2 ( 02000003)
-------------------------------------------------------
     TypDefName: TestDemo1.Type.MethodClass  ( 02000003)  //类型标识

     Method # 1 ( 06000003)   //方法标识
    -------------------------------------------------------
        MethodName: Add (06000003)
        Flags     : [ P ublic]  [ Static]
        RVA       : 0x00002064
        ReturnType: I4
        2 Arguments  //参数列表
             Argument # 1 : I4    //值传递
             Argument # 2 ByRef    I4   //引用传递
         2 Parameters 
            ( 1) ParamToken : ( 08000001) Name : a  flags: [none]
            ( 2) ParamToken : ( 08000002) Name : b  flags: [none]

     Method # 2 ( 06000004
    -------------------------------------------------------
         MethodName:  Sub ( 06000004)
        Flags     : [ Private[Static]
        RVA       :  0x0000207c
          ReturnType:  I4       这部分黄色的就是方法签名

        2 Arguments
            Argument #1:ByRef   I4   
            Argument #2:ByRef   I4
        2 Parameters       这部分是参数元数据表的内容
            (1) ParamToken : (08000003) Name : a flags: [out
            (2) ParamToken : (08000004) Name : b flags: [out

    Method #3 (06000005
    -------------------------------------------------------
        MethodName: WriteName (06000005)
        Flags     : [Public] 
        RVA       : 0x00002091
        hasThis    方法签名
        ReturnType: Void
        No arguments.

    Method #4 (06000006
    -------------------------------------------------------
        MethodName: GetAge (06000006)
        Flags     : [Family]
        RVA       : 0x000020a0
        hasThis    方法签名
        ReturnType: I4   
        No arguments.

    Method #5 (06000007
    -------------------------------------------------------
        MethodName: WriteNameAndAge (06000007)
        Flags     : [Private]
        RVA       : 0x000020b8
        hasThis   方法签名
        ReturnType: Void
        No arguments.

       

  可以看到在方法的原数据中包括了对方法的完整的描述,代码中高亮部分有二部分内容,方法返回值,方法参数.这二部分内容称之为方法签名,大家注意静态方法和实例方法有什么区别?仔细看一下,多了一个HasThis,关于原因我们在后面讨论.我们前一篇说过字段在Dll中有一个字段元数据表来描述所有的字段,那么方法也同样有一个方法的元数据表.来看看它是什么样子的.

探索CLR原理系列(3):方法元数据和IL(适合老鸟,新人勿沉迷其中) 

  方法的元数据表的Token是以0X06开头的,Token是IL中的标识数据,例如静态方法Add的标识为06000003,flags中标注了可见性,以及抽象,虚,静态,密封等方法的标记.Signature就是方法的签名,由返回值和参数组成。RVA是方法体在Dll文件中的相对地址,也就是方法的代码所在的相对地址。

除此之外,还有方法参数的起始位置.在类型那一篇中,我们讨论过类型中包含了字段起始位置,和方法的起始位置,那么这里的参数起始位置和前面二者的原理是相同的,用来查找方法的参数.来看看方法参数的元数据表吧

 探索CLR原理系列(3):方法元数据和IL(适合老鸟,新人勿沉迷其中)

  在上面的IL代码中Add方法的参数分别为一个值传递,一个引用传递,那么他们在IL中被标示为Byref,并且Out参数的Flags被标注为out,而Ref则与值传递参数的Flags没有任何区别,都为None,那么CLR在运行时如何判断呢?关键在于IL代码中,我们可以看到一个类似于C++中的去地址操作符&.

.method  public  hidebysig static  int32   Add( int32 a,
                                            int32 & b) 

  这就是引用传递和值传递的区别了,Out参数也一样,只不过在参数原数据表中的Flags中标记了一个Out。那么参数原数据表中Flags的另一个标记Opt是什么意思呢??呵呵,大家可以自己试验一下,定义一个可变参数 params int a 看看。

Sequence是什么意思?他代表参数的位置,也就是第几个参数。所以Sequence的值不能大于方法的参数个数。 

  也许大家会问,那么参数的类型呢,怎么不在参数元数据表中?这个秘密就是在方法的签名中。。。懂了吗?

 


总结

我们了解了方法是由元数据和IL代码组成。元数据中包括了方法的签名,方法的参数,以及参数的描述,IL代码中包含了方法的执行逻辑 。每个方法在方法元数据表中都有一条记录,该记录包括方法在编译时的标识Token,以及方法的可见性,契约的特征Flags,还有方法的参数和返回值组成的签名,乃至引用了方法的每一个参数的特征,由参数元数据表来记录这些信息。

 


 

 

下一篇我们来聊聊方法在类型继承中的情况。。。