.Net8顶级技术--C#源码是如何一步步变成机器码的(二)?

前言

你写的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        0x70000001IL_0005  28 0b 00 00 0a    call         0xA00000BIL_000a  2a                ret

为什么有两种类型的ILd代码?因为动态链接库里面的是完全的IL代码。它需要的是随时提供完整的信息,而通过CLR导入到JIT的IL代码,则是需要被编译的代码。所以信息量就比较少。

3.IR(中间表示)
这个IR是所有编译器必备的表达方式,也就是把IL代码转换成IR中间表示,此后通过IR转换成响应平台的机器码

INS_push REG_RBPINS_push REG_RDIINS_sub REG_RSP, 0INS_lea  //此上的四个IR中间表示,的意思是分配栈空间,且保存RBP和RDI寄存器
INS_cmpINS_je //这两句IR表示,是否调用调用调试器
INS_call //是的话就调用调试器
INS_nopINS_mov REG_RCX, 1  //传递给Console.WriteLine的参数INS_call //Console.WriteLineINS_nopINS_nop  //返回的ret指令

4.机器码

push        rbppush        rdisub         rsp,28hlea         rbp,[rsp+30h]
cmp  dword ptr [2878031C100h],0 je   00000287801399E1  //很明显这个地方是调试信息
call  00000287DEAD7068
nopmov rcx,28780209C88hcall qword ptr [287807C53F8h] //这里就是调用Console.WriteLinenopret

可以看到机器码与IL一一对应。以上就是完整的源码变成机器码的过程。这里面细节非常多,成千上万。

结尾

其实看到,从C#源代码到机器码,中间经历了一些过程。尤其是IR中间表示这一层,是比较重要的节点。比如JIT的优化,PGO,OSR,GDV,IH,LC等都是在一层进行的。