04 降级与 IL 代码生成(后端)
本册目标:看清 BoundTree 如何一步步被改写成"只含基础构造"的形式(Lowering),再如何逐节点翻译为 IL 指令(CodeGen),最终被写成带元数据的 PE 文件(Emit)。
一、要点速览
- 降级管线有严格顺序,驱动函数是
MethodCompiler.LowerBodyOrInitializer(Compiler/MethodCompiler.cs:1222):LocalRewriter → AsyncExceptionHandlerRewriter → LambdaRewriter → IteratorRewriter → AsyncRewriter。 - 闭包、迭代器、async 是三大"重写器":lambda 变显示类、迭代器变状态机、async 变状态机 + builder 调用——C# 的"魔法"在这层被拆穿。
- CodeGenerator(
CodeGen/)用栈式 IL 生成:构造时先跑Optimizer,再按 Debug/Release 风格发射指令。 - Emitter(
Emitter/Model/)把编译器符号适配成元数据模型(PEModuleBuilder),实际 PE/元数据写出在语言无关的Core/Portable/PEWriter/。 - 整条链路上任何一步发现错误就提前返回(
HasErrors短路),坏代码不会走到发射。
二、Lowering:语法糖的拆解车间
2.1 管线总览(源码实证)
Compiler/MethodCompiler.cs:1222的LowerBodyOrInitializer是每个方法体的降级总调度,顺序如下(行号为 2.10 源码实测):
| 步骤 | 调用 | 行号 | 做什么 |
|---|---|---|---|
| 1 | LocalRewriter.Rewrite(...) | L1250 | 通用降级:foreach→while+枚举器、using→try/finally、lock→Monitor、字符串插值→string.Format、模式匹配→条件判断、表达式树包装、事件访问→访问器调用… |
| 2 | AsyncExceptionHandlerRewriter.Rewrite(...) | L1280 | 仅当 catch/finally 里出现 await:换成 async 友好的合成 handler(必须在闭包重写前,见 L1272–1278 注释) |
| 3 | LambdaRewriter.Rewrite(...) | L1301 | 有 lambda/局部函数时:把被捕获变量提升为显示类字段,lambda 变显示类上的方法 |
| 4 | IteratorRewriter.Rewrite(...) | L1322 | 含yield的方法 → 迭代器状态机(MoveNext+ 状态字段) |
| 5 | AsyncRewriter.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.nLowering/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);// L1373CodeGenerator是分部类,按职责拆文件(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)
- 确定 IL 风格:
OptimizationLevel.Debug→ILEmitStyle.Debug(保守、可调试);Release →ILEmitStyle.Release;DebugPlusMode→DebugFriendlyRelease(L95–108)。合成方法(如接口桥)总是Release(L88–96 注释:peverify 对 ref 返回的桥方法有要求)。 - 先优化再发射: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 支持 |
| NoPia | CSharp/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三个函数往下追即可。
六、实验:亲眼看降级结果
- 写
static IEnumerable<int> E() { for (int i = 0; i < 3; i++) yield return i; }与一个捕获局部变量的 lambda; - 用本地构建的 Roslyn 编译它:
mono <csc> /nologo /out:t.exe /debug t.cs(或用 06 册的构建产物); - 在 MonoDevelop 里打开
t.exe(程序集浏览器,本工作区已带 ILSpy 反编译)或monodis --code t.exe > t.il; - 找到:
<E>d__0迭代器状态机类、<>c__DisplayClass闭包类、MoveNext开头的状态 switch——与 2.4 节描述逐条对上; - 加
/optimize+再编译一遍,对比 IL 中局部变量槽的减少(Optimizer+ILEmitStyle.Release的效果,§3.2)。
七、代码位置索引
| 内容 | 位置(相对roslyn-src/src/Compilers/CSharp/Portable/) |
|---|---|
降级总调度LowerBodyOrInitializer | Compiler/MethodCompiler.cs:1222 |
| LocalRewriter 调用 | Compiler/MethodCompiler.cs:1250 |
| 闭包重写调用 | Compiler/MethodCompiler.cs:1301 |
| 迭代器/async 重写调用 | Compiler/MethodCompiler.cs:1322/:1330 |
IL 生成调度GenerateMethodBody | Compiler/MethodCompiler.cs:1344(CodeGenerator 实例化:1373) |
| LocalRewriter 家族 | Lowering/LocalRewriter/LocalRewriter*.cs |
| 闭包重写器 | Lowering/LambdaRewriter/ |
| 迭代器/async 重写器 | Lowering/IteratorRewriter/、Lowering/AsyncRewriter/ |
| 状态机公共骨架 | Lowering/StateMachineRewriter/StateMachineRewriter.cs |
| CodeGenerator | CodeGen/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。