☰
04 降级与 IL 代码生成(后端)
2026/11/3 5:41:46 网站建设 项目流程

04 降级与 IL 代码生成(后端)

本册目标:看清 BoundTree 如何一步步被改写成"只含基础构造"的形式(Lowering),再如何逐节点翻译为 IL 指令(CodeGen),最终被写成带元数据的 PE 文件(Emit)。


一、要点速览

  1. 降级管线有严格顺序,驱动函数是MethodCompiler.LowerBodyOrInitializer(Compiler/MethodCompiler.cs:1222):
    LocalRewriter → AsyncExceptionHandlerRewriter → LambdaRewriter → IteratorRewriter → AsyncRewriter。
  2. 闭包、迭代器、async 是三大"重写器":lambda 变显示类、迭代器变状态机、async 变状态机 + builder 调用——C# 的"魔法"在这层被拆穿。
  3. CodeGenerator(CodeGen/)用栈式 IL 生成:构造时先跑Optimizer,再按 Debug/Release 风格发射指令。
  4. Emitter(Emitter/Model/)把编译器符号适配成元数据模型(PEModuleBuilder),实际 PE/元数据写出在语言无关的Core/Portable/PEWriter/。
  5. 整条链路上任何一步发现错误就提前返回(HasErrors短路),坏代码不会走到发射。

二、Lowering:语法糖的拆解车间

2.1 管线总览(源码实证)

Compiler/MethodCompiler.cs:1222的LowerBodyOrInitializer是每个方法体的降级总调度,顺序如下(行号为 2.10 源码实测):

步骤调用行号做什么
1LocalRewriter.Rewrite(...)L1250通用降级:foreach→while+枚举器、using→try/finally、lock→Monitor、字符串插值→string.Format、模式匹配→条件判断、表达式树包装、事件访问→访问器调用…
2AsyncExceptionHandlerRewriter.Rewrite(...)L1280仅当 catch/finally 里出现 await:换成 async 友好的合成 handler(必须在闭包重写前,见 L1272–1278 注释)
3LambdaRewriter.Rewrite(...)L1301有 lambda/局部函数时:把被捕获变量提升为显示类字段,lambda 变显示类上的方法
4IteratorRewriter.Rewrite(...)L1322含yield的方法 → 迭代器状态机(MoveNext+ 状态字段)
5AsyncRewriter.Rewrite(...)L1330含await的方法 → async 状态机(MoveNext+ AsyncTaskMethodBuilder)

每步之后都检查HasErrors短路(L1267、L1288、L1316、L1324…),并捕获CancelledByStackGuardException防深递归栈溢出(L1337)。

2.2 LocalRewriter:一个文件一种语法糖

Lowering/LocalRewriter/目录按语法构造分文件,文件名即功能清单:

LocalRewriter_ForEachStatement.cs foreach → GetEnumerator/MoveNext/Dispose 循环 LocalRewriter_AssignmentOperator.cs 复合赋值、属性/索引器赋值 → 调用 setter LocalRewriter_BinaryOperator.cs string 拼接、null 合并、运算符提升 LocalRewriter_ConditionalAccess.cs a?.b → 临时变量判空 LocalRewriter_Conversion.cs 装箱/拆箱/用户定义转换的显式化 LocalRewriter_Event.cs ev += handler → add 访问器调用 LocalRewriter_ExpressionStatement.cs / _IfStatement.cs / _DoStatement.cs …

2.3 LambdaRewriter:闭包的实现

被 lambda 捕获的局部变量被"提升"(hoist)为合成显示类(display class)的字段:

// 源代码 // 降级后(示意)intn=0;class<>c__DisplayClass0{publicintn;}Actiona=()=>n++;vard=new<>c__DisplayClass0();a();d.n=0;Actiona=newAction(d.<M>b__0);a();// 方法体里操作的是 d.n

Lowering/LambdaRewriter/实现这一转换;闭包诊断信息(ClosureDebugInfo)同步记录,供调试器把显示类字段映射回源码变量。

2.4 状态机:迭代器与 async 的共同骨架

Lowering/StateMachineRewriter/是两者的公共基座(StateMachineRewriter.cs、StateMachineTypeSymbol.cs、StateMachineStates.cs…):

  • 方法体被搬进合成的MoveNext();
  • 原局部变量 → 状态机字段(跨 yield/await 存活的才需要,CapturedSymbol.cs);
  • 每个yield/await点对应一个状态号,MoveNext开头用 switch 跳转恢复;
  • 迭代器特有:IteratorRewriter/IteratorMethodToStateMachineRewriter.cs+YieldsInTryAnalysis.cs(yield 在 try 中的特殊处理);
  • async 特有:AsyncRewriter/AsyncMethodToStateMachineRewriter.cs+AwaitExpressionSpiller.cs(await 表达式拆值为临时)+AsyncMethodBuilderMemberCollection.cs(对接 TaskLike builder)。

学这章最快的方式:写一个 20 行的迭代器方法,用它编译出的程序集在 MonoDevelop/ILSpy 里"反汇编",对照合成的MoveNext状态机读源码——你会立刻明白StateMachineStates、字段提升、恢复点跳转各自对应什么。


三、CodeGen:BoundTree → IL

3.1 入口与结构

CodeGen/CodeGenerator.cs:16:

internalsealedpartialclassCodeGenerator

方法体的 IL 生成由MethodCompiler.GenerateMethodBody(Compiler/MethodCompiler.cs:1344)驱动:

ILBuilderbuilder=newILBuilder(moduleBuilder,localSlotManager,optimizations);// L1367varcodeGen=newCodeGen.CodeGenerator(method,block,builder,moduleBuilder,diagnosticsForThisMethod,optimizations,emittingPdb);// L1373

CodeGenerator是分部类,按职责拆文件(CodeGen/):

文件职责
CodeGenerator.cs构造、风格决策(Debug/Release/DebugFriendlyRelease,L95–108)
EmitStatement.cs语句发射(if/loop/try/return…)
EmitExpression.cs表达式发射(值入栈)
EmitAddress.cs地址发射(ref/赋值目标/ldflda)
EmitConversion.cs转换指令(conv 指令、装箱、用户定义 op_Implicit 调用)
EmitOperators.cs算术/逻辑/比较运算符
EmitArrayInitializer.cs、EmitStackAllocInitializer.cs初始化器
Optimizer.cs生成前对 BoundTree 的优化

3.2 构造函数里的两件事(源码 L67–135)

  1. 确定 IL 风格:OptimizationLevel.Debug→ILEmitStyle.Debug(保守、可调试);Release →ILEmitStyle.Release;DebugPlusMode→DebugFriendlyRelease(L95–108)。合成方法(如接口桥)总是Release(L88–96 注释:peverify 对 ref 返回的桥方法有要求)。
  2. 先优化再发射:L121_boundBody = Optimizer.Optimize(boundBody, debugFriendly: ..., stackLocals: out _stackLocals);—— 把"值只赋给一个局部"的变量直接留在求值栈上(stack local),省掉stloc/ldloc对。这就是为什么 Release IL 里经常看不到中间局部变量。

3.3 生成模型:单遍栈式发射

CodeGenerator不建显式 IR——直接向ILBuilder发指令(EmitOpCode、ILBuilder负责标签、分支补丁、本地槽管理)。表达式发射 =把值压栈;语句发射 = 表达式发射 + 弹栈副作用。序列点(sequence point)在语句边界插入(_emitPdbSequencePoints,L117),这就是调试器"逐语句"的依据。


四、Emit:PE / 元数据 / PDB 的写出

4.1 分工

层目录职责
C# 适配CSharp/Portable/Emitter/Model/PEModuleBuilder.cs(C# 版模块模型)、各*SymbolAdapter.cs把编译器符号适配成面向元数据的视图
语言无关写出Core/Portable/PEWriter/ExtendedPEBuilder.cs、FullMetadataWriter.cs——真正写 PE 头、表、IL、资源、强名
公共模型Core/Portable/Emit/CommonPEModuleBuilder.cs、EmitOptions.cs、EditAndContinue 支持
NoPiaCSharp/Portable/Emitter/NoPia/无 PIA(类型等价)的元数据处理

4.2 为什么要有 Adapter

编译器内部符号(03 册)携带大量源码信息(语法节点、绑定状态),而元数据只需要签名视图。Emitter/Model/的几十个*Adapter就是这层"只暴露元数据所需"的包装——发射器因此与符号系统解耦,VB 编译器共享同一套 PEWriter。


五、把五阶段再串一次(收束 01 册的数据流)

MethodCompiler(Compiler/MethodCompiler.cs,每个方法体的流水线) ├─ 绑定:BindMethodBody → BoundStatement (03 册) ├─ 降级:LowerBodyOrInitializer (本册 §2) │ LocalRewriter → AsyncExceptionHandler → Lambda │ → Iterator → Async Rewriter ├─ 生成:GenerateMethodBody → CodeGenerator → ILBuilder (本册 §3) └─ 模块汇总:PEModuleBuilder 收集全部 MethodBody/符号 Emitter(由 compilation.Emit 触发) └─ Core/PEWriter 写出 .dll/.exe + .pdb (本册 §4)

MethodCompiler因此是读后端源码的第一入口:从它的CompileMethod/LowerBodyOrInitializer/GenerateMethodBody三个函数往下追即可。


六、实验:亲眼看降级结果

  1. 写static IEnumerable<int> E() { for (int i = 0; i < 3; i++) yield return i; }与一个捕获局部变量的 lambda;
  2. 用本地构建的 Roslyn 编译它:
    mono <csc> /nologo /out:t.exe /debug t.cs(或用 06 册的构建产物);
  3. 在 MonoDevelop 里打开t.exe(程序集浏览器,本工作区已带 ILSpy 反编译)或monodis --code t.exe > t.il;
  4. 找到:<E>d__0迭代器状态机类、<>c__DisplayClass闭包类、MoveNext开头的状态 switch——与 2.4 节描述逐条对上;
  5. 加/optimize+再编译一遍,对比 IL 中局部变量槽的减少(Optimizer+ILEmitStyle.Release的效果,§3.2)。

七、代码位置索引

内容位置(相对roslyn-src/src/Compilers/CSharp/Portable/)
降级总调度LowerBodyOrInitializerCompiler/MethodCompiler.cs:1222
LocalRewriter 调用Compiler/MethodCompiler.cs:1250
闭包重写调用Compiler/MethodCompiler.cs:1301
迭代器/async 重写调用Compiler/MethodCompiler.cs:1322/:1330
IL 生成调度GenerateMethodBodyCompiler/MethodCompiler.cs:1344(CodeGenerator 实例化:1373)
LocalRewriter 家族Lowering/LocalRewriter/LocalRewriter*.cs
闭包重写器Lowering/LambdaRewriter/
迭代器/async 重写器Lowering/IteratorRewriter/、Lowering/AsyncRewriter/
状态机公共骨架Lowering/StateMachineRewriter/StateMachineRewriter.cs
CodeGeneratorCodeGen/CodeGenerator.cs:16(构造:67,优化:121)
表达式/语句发射CodeGen/EmitExpression.cs、CodeGen/EmitStatement.cs
优化器CodeGen/Optimizer.cs
C# 模块模型Emitter/Model/PEModuleBuilder.cs、Emitter/Model/MethodSymbolAdapter.cs等
PE/元数据写出(语言无关)../Core/Portable/PEWriter/ExtendedPEBuilder.cs、FullMetadataWriter.cs
公共发射抽象../Core/Portable/Emit/CommonPEModuleBuilder.cs

下一册:05-编译器驱动与宿主环境.md——回到入口:csc 命令行如何驱动这一切,以及 MSBuild/IDE 如何调用 csc。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询