首先了解几个概念和内存结构
一:
1.函数描述符MethodDesc,包含了函数是否被编译标志,函数当前在函数描述符块的索引,以及函数在函数表的索引,以及Token
2.函数描述符块MethodDescChunk,主要是包含了当前函数描述符的个数
3.函数入口点FixupPreCode 包含了函数描述符的索引,FixupPreCode的索引
二:
1.内存结构排列
FixupPreCode->MethodDescChunk->MethodDesc
假如说有一块连续内存:1-2-3-4-5-6实际上可以把上面三个结构体看做1,2,3连续内存存储模式。
注意看下图(不必理解为什么意思,只需要知道他们的内存排列结构就可以了)
红色和黄色框出来的就是fixupprecdoe也就是地址为0x00007fff6D79A7E0以及地址0x00007fff6D79A848,仔细看这两个地址的值都是00007fff6d7a0828也就是fixupPreCode的地址值。跳转到这个地址,它的值为00005e5ed11113e8。
蓝色框出来的就是MethodDescChunk ,紫色的为MethodDesc。
CLR在运行的时候,首先会在方法描述符块地址-1的地方放置FixUpPrecode的地址值,也就是通过&MethodDescChunk-1就可以访问当前所有类的函数入口点。
在设置函数描述符的时候,CLR会把非虚函数的函数入口点放在MethodDesc的结尾,也就是上面黄色框的地方,地址为0x00007fff6D79A848
通过上面内存结构,其实我们可以看到只要知道函数入口点,函数描述符,以及函数描述符块其中一个的内存地址,就可以推导出其它几个结构体地址以及存储的值。
三:
1.当我们的函数被编译之后,函数描述符结尾的函数入口点就会改为被编译后的地址。也就是地址0x00007fff6D79A848会被改动。而在函数编译之前的地址,实质上就是PrecodeFixupThunk,改动之后,就是机器码的地址。
2.在改动函数入口点之后,他会修改函数描述符,也就是MethodDesc结构体里面的地址为&MethodDesc+0x08的值。它会标记当前函数已经被编译,以及编译后的地址是多少。
3.在以上两步完成之后,就会修改函数入口点的值,注意第一步是修改函数入口点的跳转值。也就是FixupPreCode的本身值。
关于以上我们可以通过代码来验证。
四:
1.假设我们有以下代码:
using System;
namespace ConsoleApp3
{
class Program
{
static void Main(string[] args)
{
Test.testmethod(1);
}
}
public static class Test
{
public static void testmethod(int i)
{
}
}
}
反汇编可以看到它调用Test.testmethod(1);函数之后,它会跳转到地址0x07FFF67040640
查看下此地址的值
我们可以看到fixuppreocde值后面接着MethodDesc的地址。通过其地址就可以找到TestMethod1函数的内存结构
注意看,有两个fixupprecode地址,也就是两个紫色线条指向的地方。为啥,看上面内存结构排序的那张图,跟这张图一模一样。都是头尾为fixupprecode地址,后面跟着methoddeschunk,然后再跟着methoddesc。
我们F5把断点直接运行testmethod函数。
看函数被编译后的下面两张图
fixupprecode图:
methoddesc图:
我们看到fixupprecode的值变了,从5e5f5eb29be8变成了5f000035cbe9.但是同时我们也看到methoddesc没有改变,那么问题来了,我们是如何知道mehtoddesc是被编译过了,它那个编译标记是如何被设置的呢,这个问题先放一放。
回到上面
我们看到,当一个函数被编译前后,它的入口点地址是不变的也就是恒值,而变的是入口点的值。编译之前入口点值被设置成调用PrecodeFixupThunk函数的值,而编译之后被设置成编译后机器码的值。
以上大致就是CLR编译函数的过程了。