ilgenerator是直接向jit喂指令的底层管道,用错参数、栈不平衡或局部变量索引越界会导致运行时崩溃;其emit三重载必须严格区分:带localbuilder、type或无参,混淆将引发invalidprogramexception等难以调试的异常。

ILGenerator 不是“写 IL 的快捷方式”,它是直接向 JIT 编译器喂指令的底层管道。用错参数、压栈顺序不对、局部变量索引越界,程序不会报错,但会在运行时崩溃或返回垃圾值——这点和写 C++ 指针一样危险。
ILGenerator.Emit 的三个关键重载必须分清
你调用 Emit 时传什么参数,决定 IL 流里生成哪条指令序列。最常用的是这三种:
-
Emit(OpCode, LocalBuilder):用于ldloc.0、stloc.1这类操作局部变量的指令。注意LocalBuilder必须由同一个MethodBuilder创建,否则抛ArgumentException -
Emit(OpCode, Type):用于newobj、call等需要类型元数据令牌的指令。传null直接触发ArgumentNullException -
Emit(OpCode)(无参):用于ret、pop、dup等不带操作数的指令。多写一个参数就会编译失败
混淆这三者会导致生成非法 IL,RuntimeBinderException 或 InvalidProgramException 是常见结果,且堆栈里看不到具体哪行出问题。
局部变量索引超过 255 就不能用 Emit(OpCode, LocalBuilder)
.NET IL 规范中,ldloc.s 和 stloc.s 指令只支持 1 字节局部变量索引(0–255)。如果你用 MethodBuilder.GetILGenerator() 定义了 300 个 LocalBuilder,再对第 256 个调用 Emit(OpCodes.Ldloc, local),会直接抛 InvalidOperationException。
解决办法只有两个:
- 改用
Emit(OpCodes.Ldloc, int)重载(需手动传索引),它会自动选ldloc(4 字节)而非ldloc.s - 重构逻辑,减少局部变量数量——比如把临时计算合并进表达式树,或复用已有变量
这个限制在 .NET Framework 4.8 和 .NET 6+ 全部生效,不是版本差异,是 IL 格式硬约束。
EmitWriteLine 不是调试神器,而是陷阱高发区
ILGenerator.EmitWriteLine(string) 看起来方便,但它背后会自动插入 ldstr + call void [System.Console]System.Console::WriteLine(string)。问题在于:
- 它不检查当前模块是否引用了
System.Console,若动态程序集没显式引用该程序集,运行时报MissingMethodException - 它强制绑定到静态
Console.WriteLine,无法替换成自定义日志器(比如 Serilog 的Log.Information) - 字符串字面量会被固化进动态方法的元数据,无法参数化,调试时看不到变量实时值
真要调试,推荐手写三行:ldarg.0 → call void [System.Console]System.Console::WriteLine(object) → pop,这样可控、可替换、可断点。
IL 评估栈必须手动平衡,没人帮你校验
所有 Emit 操作本质是往栈上推/拉值。比如 Emit(OpCodes.Ldc_I4, 42) 压入一个 int32,接着 Emit(OpCodes.Call, method) 会从栈顶取参数——如果 method 需要两个 int32,而你只压了一个,JIT 编译时不会报错,但运行时可能静默返回 0、抛 ExecutionEngineException,或在某些 CPU 架构上直接访问违规内存。
验证栈平衡的唯一可靠方式是:
- 用
ILGenerator.GetILAsByteArray()提取字节码 - 用
ildasm或dnSpy反编译查看栈高度变化 - 重点关注
ld*/st*/call/ret周围的栈状态注释
别信 IDE 插件或“IL 语法检查器”——它们无法模拟真实 JIT 的栈校验逻辑。










