Skip to content

Latest commit

 

History

History
 
 

ILIntepreter

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

ILRuntime的实现原理

ILRuntime借助Mono.Cecil库来读取DLL的PE信息,以及当中类型的所有信息,最终得到方法的IL汇编码,然后通过内置的IL解译执行虚拟机来执行DLL中的代码。

IL托管栈和托管对象栈

为了高性能进行运算,尤其是栈上的基础类型运算,如int,float,long之类类型的运算,直接借助C#的Stack类实现IL托管栈肯定是个非常糟糕的做法。因为这意味着每次读取和写入这些基础类型的值,都需要将他们进行装箱和拆箱操作,这个过程会非常耗时并且会产生巨量的GC Alloc,使得整个运行时执行效率非常低下。

因此ILRuntime使用unsafe代码以及非托管内存,实现了自己的IL托管栈。

ILRuntime中的所有对象都是以StackObject类来表示的,他的定义如下:

    struct StackObject
    {
        public ObjectTypes ObjectType;
        public int Value; //高32位
        public int ValueLow; //低32位
    }
    enum ObjectTypes
    {
        Null,//null
        Integer,
        Long,
        Float,
        Double,
        StackObjectReference,//引用指针,Value = 指针地址, 
        StaticFieldReference,//静态变量引用,Value = 类型Hash, ValueLow= Field的Index
        Object,//托管对象,Value = 对象Index
        FieldReference,//类成员变量引用,Value = 对象Index, ValueLow = Field的Index
        ArrayReference,//数组引用,Value = 对象Index, ValueLow = 元素的Index
    }

通过StackObject这个值类型,我们可以表达C#当中所有的基础类型,因为所有基础类型都可以表达为8位到64位的integer。对于非基础类型而言,我们额外需要一个List来储存他的object引用对象,而Value则可以存储这个对象在List中的Index。由此我们就可以表达C#中所有的类型了。

托管调用栈

ILRuntime在进行方法调用时,需要将方法的参数先压入托管栈,然后执行完毕后需要将栈还原,并把方法返回值压入栈。

具体过程如下图所示

调用前:                                调用完成后:
|---------------|                     |---------------|
|     参数1     |     |-------------->|   [返回值]    |
|---------------|     |               |---------------|
|      ...      |     |               |     NULL      |
|---------------|     |               |---------------|
|     参数N     |     |               |      ...      |
|---------------|     |
|   局部变量1   |     |
|---------------|     |
|      ...      |     |
|---------------|     |
|   局部变量1   |     |
|---------------|     |
|  方法栈基址   |     |
|---------------|     |
|   [返回值]    |------
|---------------|

函数调用进入目标方法体后,栈指针(后面我们简称为ESP)会被指向方法栈基址那个位置,可以通过ESP-X获取到该方法的参数和方法内部申明的局部变量,在方法执行完毕后,如果有返回值,则把返回值写在方法栈基址位置即可(上图因为空间原因写在了基址后面)。

当方法体执行完毕后,ILRuntime会自动平衡托管栈,释放所有方法体占用的栈内存,然后把返回值复制到参数1的位置,这样后续代码直接取栈顶部就可以取到上次方法调用的返回值了。