1.前言
任何编程语言最后运行在计算机上的一定是机器码,只有机器码才能被计算机底层硬件识别并执行,这里通过.Net8 PreView JIT的机器码生成来看下一些过程。
2.概述
一:概念
首先了解一些概念,比如地址,机器码,汇编等。我们先来看一段汇编代码:
00007FF85EAC0022 48 81 EC 88 00 00 00 sub rsp,88h
00007FF85EAC0029 48 8D AC 24 90 00 00 00 lea rbp,[rsp+90h]
00007FF85EAC0031 C5 D8 57 E4 vxorps xmm4,xmm4,xmm4
以第一行代码为例,最前面的00007FF85EAC0022这种就是地址了
后面跟着的
48 81 EC 88 00 00 00
它就是机器码,代表了在硬件上指向的数据。再后面跟着的就是汇编代码了,也就是机器码的形容词,以方便人来观察这条机器码到底执行的是啥,因为如果人直接观察这种十六进制的机器码,根本分不清楚。
sub rsp,88h
OK了解以上简单的概念,我们下面继续。
二:生成
我们从MSIL开始,我们知道MSIL是有前端编译器Roslyn生成的,它里面包含了IL代码和一些元数据。JIT就根据这些数据生成机器码,然后在机器上运行。根据上面所说:
一条汇编的生成,必然是有地址,所需填充的机器码。我们只需要传递所需填充的机器码到相应的地址上,就可以生成一条汇编代码。
比如往地址:00007FF85EAC0022里面填充了
48 81 EC 88 00 00 00
那么代表了就是以下汇编代码
sub rsp,88h
我们可以确认的是,在CLR里面一条机器码的生成跟MSIL息息相关。由于过程非常复杂,这里主要看下它最终生成机器码的过程。
微软官方源码地址:
https://github.com/dotnet/runtime/blob/main/src/coreclr/jit/emit.cpp:4384行
我们看到这行的代码是:
is = emitOutputInstr(ig, id, dp);
emitOutputInstr函数里面包含了三个参数,第一个参数ig是需要生成的机器码的链表合集。id是当前需要生成的机器码的伪代码,dp也就是上面所说的需要被机器码填充的地址。
有了这个三个参数,其实非常好搞了。
思路就是:循环遍历ig,然后逐一取出机器码伪代码id,通过相应的操作,往dp地址里面填充机器码,这样就会一行行生成机器码。最终的结果就是被机器码执行,也就是.Net最终生成的形态。
参考代码:
https://github.com/dotnet/runtime/blob/main/src/coreclr/jit/emitxarch.cpp:16147
size_t emitter::emitOutputInstr(insGroup* ig, instrDesc* id, BYTE** dp)
{
assert(emitIssuing);
BYTE* dst = *dp;
size_t sz = sizeof(instrDesc);
instruction ins = id->idIns();
unsigned char callInstrSize = 0;
//这里省略十万行
}
以上就是JIT机器码生成的大致过程。