前言
你写的C#源代码是如何变成机器码,在特定的平台上运行的呢?比如X64,Arm64,Risc-V64这些平台。本篇来一步步剖析下,以下以最广泛的X64为例。上一篇:.Net8的顶级技术JIT机器码生成
概括
1.C#源码
static void Main()
{
Console.WriteLine("Hello World");
}
假设有以上简单的C#源码,它的第一步是通过Roslyn前端编译把它编译成MSIL代码
2.IL代码
IL代码分为两类
一.动态链接库的IL代码
.method private hidebysig static void Main() cil managed
{
.entrypoint
// 代码大小 11 (0xb)
.maxstack 8
IL_0000: ldstr "Hello World"
IL_0005: call void [System.Console]System.Console::WriteLine(string)
IL_000a: ret
} // end of method Program::Main
二.导入JIT的IL代码
IL to import:
IL_0000 72 01 00 00 70 ldstr 0x70000001
IL_0005 28 0b 00 00 0a call 0xA00000B
IL_000a 2a ret
为什么有两种类型的ILd代码?因为动态链接库里面的是完全的IL代码。它需要的是随时提供完整的信息,而通过CLR导入到JIT的IL代码,则是需要被编译的代码。所以信息量就比较少。
3.IR(中间表示)
这个IR是所有编译器必备的表达方式,也就是把IL代码转换成IR中间表示,此后通过IR转换成响应平台的机器码
INS_push REG_RBP
INS_push REG_RDI
INS_sub REG_RSP, 0
INS_lea //此上的四个IR中间表示,的意思是分配栈空间,且保存RBP和RDI寄存器
INS_cmp
INS_je //这两句IR表示,是否调用调用调试器
INS_call //是的话就调用调试器
INS_nop
INS_mov REG_RCX, 1 //传递给Console.WriteLine的参数
INS_call //Console.WriteLine
INS_nop
INS_nop //返回的ret指令
4.机器码
push rbp
push rdi
sub rsp,28h
lea rbp,[rsp+30h]
cmp dword ptr [2878031C100h],0
je 00000287801399E1 //很明显这个地方是调试信息
call 00000287DEAD7068
nop
mov rcx,28780209C88h
call qword ptr [287807C53F8h] //这里就是调用Console.WriteLine
nop
ret
可以看到机器码与IL一一对应。以上就是完整的源码变成机器码的过程。这里面细节非常多,成千上万。
结尾
其实看到,从C#源代码到机器码,中间经历了一些过程。尤其是IR中间表示这一层,是比较重要的节点。比如JIT的优化,PGO,OSR,GDV,IH,LC等都是在一层进行的。
作者:江湖评谈