解释器总览
前言
在本系列的前两章源码分析中,我们详细剖析了 HybridCLR 的元数据模块(如何存储和查找程序集信息)和编译器模块(如何将 IL 变换为 IR 指令序列)。现在到了第三步——解释器模块,它是 HybridCLR 混合执行模型的心脏。
如果说编译器是"翻译官",将 IL 翻译为一种更适合解释执行的中间表示(IR),那么解释器就是"执行官"——它逐条执行编译器生成的 IR 指令,驱动方法调用、控制流跳转、对象分配、异常处理等所有运行时行为。
解释器模块位于 hybridclr/interpreter/ 目录,它与 hybridclr/transform/(编译器)和 hybridclr/metadata/(元数据)紧密协作,构成了 HybridCLR 运行时三角。
一、解释器模块的文件结构
1.1 文件清单
hybridclr/interpreter/ 目录包含以下关键文件:
| InterpreterModule.h/.cpp | 解释器模块初始化和管理 | 全局初始化、线程管理、调用入口 |
| Engine.h/.cpp | MachineState—解释器运行时状态 | 管理三条运行时栈(评估栈、帧栈、异常流栈) |
| Interpreter_Execute.cpp | 解释器执行主循环 | IR 指令的 dispatch 和逐条执行 |
| InterpreterDefs.h | 类型定义和常量 | InterpFrame、ExceptionFlowInfo、常量 × 约束 |
| Instruction.h | IR 指令定义 | 所有 IR 指令的结构体定义 |
| ResolveData.h | 解析数据 | 运行时解析 Token 的辅助结构 |
每个文件承担一个明确的角色,整个解释器模块的设计清晰地体现了关注点分离(Separation of Concerns)原则。
二、InterpretModule:解释器的全局管理者
2.1 初始化流程
InterpreterModule 是一个静态类,负责解释器模块的全局初始化和管理。它在 HybridCLR 运行时启动时被初始化:
// 来源:hybridclr/interpreter/InterpreterModule.h
class InterpreterModule
{
public:
// 初始化解释器模块,在 HybridCLR 启动时调用一次
static void Initialize();
// 线程特定的初始化
static void InitializeThread();
// 解释执行方法的入口点
static Il2CppObject* Execute(InterpFrame* frame);
// 获取当前线程的 MachineState
static MachineState& GetCurrentThreadMachineState();
// 解释器内部的 main 函数(实际 dispatch 循环)
static Il2CppObject* ExecuteMain(InterpFrame* frame);
};
Initialize() 在 HybridCLR 初始化阶段执行以下工作:
InitializeThread() 在每个使用解释器的线程上调用,为该线程创建独立的 MachineState 实例。这是线程安全的保证——每个线程有独立的评估栈、帧栈和异常流栈。
2.2 调用入口
当一个热更新方法需要被执行时,IL2CPP 运行时会调用 InterpreterModule::Execute():
Il2CppObject* InterpreterModule::Execute(InterpFrame* frame)
{
MachineState& state = GetCurrentThreadMachineState();
// 保存现场:上一个帧的执行状态
uint8_t* prevIp = state.ip;
InterpFrame* prevFrame = state.currentFrame;
// 设置当前帧
state.currentFrame = frame;
state.ip = frame->method->irBody->instructions;
// 执行主循环
Il2CppObject* ret = ExecuteMain(frame);
// 恢复现场
state.ip = prevIp;
state.currentFrame = prevFrame;
return ret;
}
Execute() 是一个重入函数——当解释器方法调用另一个解释器方法时,Execute() 会被递归调用。MachineState 利用 InterpFrame 链表来维护调用栈,因此递归调用是安全的。
三、MachineState:解释器的运行时状态
3.1 三条运行时栈
MachineState 是解释器运行时状态的核心,它管理了三条并行的栈:
为什么需要三条独立的栈?因为评估栈、帧栈和异常流栈承担了完全不同的职责,各自有不同的生命周期和访问模式:
- 评估栈是方法执行期间的工作区,生命周期与基本块或单条指令一致,压入的值通常很快被弹出使用
- 帧栈是方法调用链的骨架,生命周期与整个方法调用过程一致,从方法进入一直存在到方法返回
- 异常流栈是异常处理期间的状态存储,生命周期与异常传播过程一致,在异常抛出时创建、在异常处理完成时销毁
这三种不同的生命周期要求三条独立的栈来管理。如果合并在一个栈中,管理逻辑会变得极其复杂且容易出错。
// 来源:hybridclr/interpreter/Engine.h
struct MachineState
{
// === 评估栈(Eval Stack)===
StackObject* evalStackBase; // 评估栈基地址
StackObject* evalStackTop; // 评估栈顶指针
int32_t evalStackSize; // 当前评估栈深度
// === 帧栈(Frame Stack)===
InterpFrame* frameStackBase; // 帧栈基地址
InterpFrame* currentFrame; // 当前帧指针
int32_t frameStackSize; // 帧栈深度
// === 异常流栈(Exception Flow Stack)===
ExceptionFlowInfo* exFlowStackBase; // 异常流栈基地址
ExceptionFlowInfo* exFlowStackTop; // 异常流栈顶指针
int32_t exFlowStackSize; // 异常流栈深度
// === 执行指针 ===
uint8_t* ip; // 当前 IR 指令指针
// === 解析数据缓存 ===
ResolveData* resolveDatas; // 当前方法对应的解析数据数组
// === 常量 ===
static constexpr int32_t MAX_EVAL_STACK_SIZE = 512; // 评估栈最大深度
static constexpr int32_t MAX_FRAME_STACK_SIZE = 1024; // 帧栈最大深度
static constexpr int32_t MAX_EX_FLOW_STACK_SIZE = 64; // 异常流栈最大深度
};
这三条栈各有不同的用途:
评估栈(Eval Stack)——虚拟的 .NET 评估栈。在 IR 指令执行期间,localVarBase 数组的一部分区域充当评估栈。编译器生成的 LoadVar / StoreVar / DupVar / PopVar 等 IR 指令直接操作这个栈。它是 CLR 虚拟机模型中最核心的栈结构。
帧栈(Frame Stack)——管理方法调用链。每个方法调用在入栈时创建一个 InterpFrame,在返回时销毁。InterpFrame 通过 previous 指针形成一个单向链表,用于栈回溯和异常传播。
异常流栈(Exception Flow Stack)——跟踪异常处理和 leave 指令的执行状态。当异常抛出或 leave 指令触发 finally 链时,生成 ExceptionFlowInfo 条目推入此栈。
3.2 InterpFrame 结构
InterpFrame 是解释器方法帧的核心数据结构:
// 来源:hybridclr/interpreter/InterpreterDefs.h
struct InterpFrame
{
InterpFrame* previous; // 上一个帧(链表指针)
MachineState* machine; // 所属 MachineState
const MethodInfo* method; // 当前执行的方法
void* localVarBase; // 局部变量基址(参数 + 局部变量 + 临时栈)
uint32_t localCount; // 局部变量数量(包含参数)
int32_t returnOffset; // 返回指令的 IR 偏移
uint8_t* returnIp; // 返回地址(调用方的 IR 指令指针)
};
关键字段说明:
- previous——指向上一个调用帧。当方法 A 调用方法 B 时,B 的 previous 指向 A。这个单向链表使得栈回溯(stack trace)可以直接遍历所有解释器帧
- localVarBase——指向 StackObject 数组的起始位置。数组的布局是:[参数区 | 局部变量区 | 临时栈区]
- localCount——局部变量的总数(包含参数)。编译器在 ComputeLocalVarCount() 中计算这个值
- returnIp——当被调用方法返回时,解释器根据这个地址恢复调用方的 IR 指令执行
3.3 局部变量数组的内存布局
localVarBase 对应的 StackObject 数组布局如下:
┌──────────────────────────────┐
│ 参数 arg0(this 指针) │ ← localVarBase[0]
│ 参数 arg1 │ ← localVarBase[1]
│ 参数 arg2 │ ← localVarBase[2]
│ … │
│ 参数 argN-1 │ ← localVarBase[N-1]
├──────────────────────────────┤
│ 局部变量 local0 │ ← localVarBase[N]
│ 局部变量 local1 │ ← localVarBase[N+1]
│ … │
│ 局部变量 localM-1 │ ← localVarBase[N+M-1]
├──────────────────────────────┤
│ 临时栈(编译器分配的虚拟寄存器) │ ← localVarBase[N+M]
│ … │
│ 临时栈第 K 个槽 │ ← localVarBase[N+M+K-1]
└──────────────────────────────┘
数组的总长度 = 参数数量 + 局部变量数量 + 编译器计算的临时变量数量。编译器在 ComputeLocalVarCount() 中计算这个值(详见第 22 篇),它基于 IL 分析的最大评估栈深度加上局部变量参数数量。
这个设计的重要特点是:评估栈不是独立分配的,而是 localVarBase 数组的一部分。IL 指令中的评估栈操作(压栈/弹栈)直接映射到数组索引的增减。编译器将 IL 操作数栈位置映射为 localVarBase 的偏移,解释器执行时不做额外的评估栈管理——所有值都基于索引访问。
3.4 StackObject 类型
StackObject 是解释器中所有值的通用表示:
// 来源:hybridclr/interpreter/InterpreterDefs.h
union StackObject
{
int64_t s8; // 8 字节有符号整数(long)
int32_t s4; // 4 字节有符号整数(int)
int16_t s2; // 2 字节有符号整数(short)
int8_t s1; // 1 字节有符号整数(sbyte)
uint64_t u8; // 8 字节无符号整数
uint32_t u4; // 4 字节无符号整数
uint16_t u2; // 2 字节无符号整数
uint8_t u1; // 1 字节无符号整数
float f4; // 4 字节浮点数(float)
double f8; // 8 字节浮点数(double)
void* ptr; // 指针(用于对象引用和值类型地址)
};
注意 StackObject 是一个联合体(union),所有字段占用相同的内存空间(8 字节)。这正是第 21-23 篇中反复提到的"所有 IR 指令操作 8 字节定长值"的基础。在解释器执行时,所有值都以 StackObject 存储在 localVarBase 数组中,类型信息由上下文(IR 指令的 opcode 和操作数类型)决定。
四、ExecuteMain:解释器主循环
ExecuteMain() 是解释器的核心函数,它包含 IR 指令分发和执行的主循环:
Il2CppObject* InterpreterModule::ExecuteMain(InterpFrame* frame)
{
MachineState& state = GetCurrentThreadMachineState();
uint8_t* ip = frame->method->irBody->instructions;
// 解析数据缓存
ResolveData* resolveDatas = frame->method->irBody->resolveDatas;
state.resolveDatas = resolveDatas;
// 进入方法帧
Engine::EnterFrame(frame);
// 主执行循环
while (true)
{
// 获取当前 IR 指令
IRCommon* inst = (IRCommon*)ip;
// 根据 opcode 分发到对应的处理逻辑
switch (inst->opcode)
{
case HiOpcodeEnum::LoadConstI4Var:
{
auto* loadInst = (IRLoadConstI4Var*)ip;
localVarBase[loadInst->dst] = StackObject::MakeI4(loadInst->value);
ip += 8;
continue;
}
case HiOpcodeEnum::AddVarVar:
{
auto* addInst = (IRAddVarVar*)ip;
localVarBase[addInst->dst].s4 =
localVarBase[addInst->src1].s4 + localVarBase[addInst->src2].s4;
ip += 8;
continue;
}
case HiOpcodeEnum::CallInterp_void:
{
auto* callInst = (IRCallInterp_void*)ip;
const MethodInfo* method = resolveDatas[callInst->methodIdx].method;
// 创建子方法帧
InterpFrame* childFrame = CreateFrame(method, …);
// 递归执行子方法
ExecuteMain(childFrame);
ip += 8;
continue;
}
// … 900+ 个 opcode 的 case …
case HiOpcodeEnum::RetVar:
{
auto* retInst = (IRRetVar*)ip;
StackObject retVal = localVarBase[retInst->src];
Engine::LeaveFrame(frame);
return retVal.obj;
}
}
}
}
4.1 主循环的关键特征
顺序执行——IR 指令按顺序执行,每次 ip += 8(所有 IR 指令固定 8 字节)
单循环、多 case——所有 924 个 HiOpcodeEnum 值在同一个 switch 语句中处理
localVarBase 的直接访问——解释器不使用独立的操作数栈,所有值通过 localVarBase[offset] 直接访问
无基本块概念——IR 指令已经是扁平序列,控制流(分支、跳转)通过修改 ip 指针实现
递归调用——解释器方法的调用通过递归的 ExecuteMain() 实现,每次调用创建新的 InterpFrame
4.2 循环控制
while(true) 循环的退出路径只有两种:
- RetVar / RetVar_void 指令——返回值(如果有)后调用 LeaveFrame,退出循环
- 异常抛出未处理——当异常在当前方法中没有被捕获,异常传播逻辑会跳出当前方法的执行
4.3 循环的开销
ExecuteMain 的主循环有一个重要的性能特征——switch 分发的分支预测开销。在 924 个 case 的 switch 语句中,CPU 的分支预测器(Branch Predictor)很难准确预测下一条 IR 指令的 opcode。这意味着每次循环迭代都可能出现分支预测失败(Branch Misprediction),导致流水线冲刷(Pipeline Flush)。
不同的 IR 指令在实际程序中的执行频率差异巨大——LoadVar、StoreVar、LoadConstI4Var 等简单指令的执行频次远高于 CallInterp、NewClassVar 等昂贵指令。编译器通过将高频指令在 switch 中放在靠前位置(利用编译器优化倾向),或者使用 computed goto(GCC 扩展)替代 switch,来缓解分支预测问题。
4.4 指令长度的统一性
所有 IR 指令固定为 8 字节是主循环高效运行的关键设计约束。这个设计意味着:
五、EnterFrame 与 LeaveFrame:帧管理
5.1 EnterFrame
EnterFrame 在方法开始执行时被调用:
// 来源:hybridclr/interpreter/Engine.cpp(简化)
void Engine::EnterFrame(InterpFrame* frame)
{
MachineState& state = *frame->machine;
// 1. 将新帧链入帧栈
frame->previous = state.currentFrame;
state.currentFrame = frame;
state.frameStackSize++;
// 2. 初始化局部变量为零值
StackObject* localVars = (StackObject*)frame->localVarBase;
for (uint32_t i = 0; i < frame->localCount; i++)
{
localVars[i].u8 = 0;
}
// 3. 检查帧栈深度
if (state.frameStackSize > MAX_FRAME_STACK_SIZE)
{
// 栈溢出——报告错误
RaiseStackOverflowException();
}
}
EnterFrame 的关键操作:
5.2 LeaveFrame
LeaveFrame 在方法返回时被调用,负责清理当前方法执行期间创建的资源,并将控制权交还给调用方:
void Engine::LeaveFrame(InterpFrame* frame)
{
MachineState& state = *frame->machine;
// 1. 清空异常流栈
// 方法返回前,必须确保所有异常处理已经完成
state.exFlowStackTop = state.exFlowStackBase;
state.exFlowStackSize = 0;
// 2. 恢复帧栈
state.currentFrame = frame->previous;
state.frameStackSize–;
// 3. 恢复 ip(如果存在调用方帧)
if (state.currentFrame != nullptr)
{
state.ip = frame->returnIp;
}
}
LeaveFrame 的关键操作:
清空异常流栈——方法返回时,该方法的异常处理状态不再有效。如果在方法返回时异常流栈上还有未完成的 finally 或 filter 执行,说明存在异常处理逻辑错误(如缺少 endfinally 指令),但 LeaveFrame 不会检查这一点——正确的 IR 指令序列由编译器保证
恢复帧栈——帧栈指针指回调用方的帧。frame->previous 指向调用方的 InterpFrame,state.currentFrame 更新后,MachineState 就回到了调用方的帧上下文中
恢复 ip——IR 指令指针回到调用方的上下文。frame->returnIp 在 CallInterp 指令执行时设置,指向调用方方法中 CallInterp 指令的下一条指令。这样当递归的 ExecuteMain() 返回后,调用方从正确的位置继续执行
5.3 EnterFrame 与 LeaveFrame 的配对
EnterFrame 和 LeaveFrame 严格配对出现——每次方法执行都恰好调用一次 EnterFrame 和一次 LeaveFrame。这个配对关系是解释器帧栈正确性的基本保证:
方法 A 执行:
EnterFrame(A)
├── 方法 A 的指令执行
├── 调用方法 B:
│ ├── EnterFrame(B)
│ │ ├── 方法 B 的指令执行
│ │ └── LeaveFrame(B) ← 匹配 EnterFrame(B)
│ └── 继续方法 A 执行
└── LeaveFrame(A) ← 匹配 EnterFrame(A)
如果 EnterFrame 和 LeaveFrame 不配对(例如异常传播时跳过 LeaveFrame),帧栈将处于不一致状态,后续的方法调用可能使用错误的帧指针。异常处理需要特别注意这一点——异常传播可能跳过正常路径上的 LeaveFrame,因此异常处理逻辑必须独立管理帧栈状态。
六、解释器的重入与线程安全
6.1 重入机制
解释器的递归调用是最重要的设计约束之一。当解释器方法 A 调用解释器方法 B 时,执行流程为:
ExecuteMain(A_frame)
├── … A 的指令执行 …
├── 遇到 CallInterp_void
│ ├── 创建 B_frame(B_frame->previous = A_frame)
│ ├── ExecuteMain(B_frame)
│ │ ├── … B 的指令执行 …
│ │ └── 遇到 RetVar → LeaveFrame(B_frame)
│ └── 返回到 A 的 ip 继续执行
└── … 继续 A 的指令执行 …
递归深度等于解释器的方法调用链深度。每次递归调用都会在 C++ 调用栈上分配一个完整的 ExecuteMain 栈帧,但 MachineState 的栈(通过 InterpFrame 链表管理)是在堆上分配的。因此解释器的调用深度受 C++ 栈限制,而非 MachineState 帧栈限制。
6.2 线程安全
每个线程有独立的 MachineState 实例,这是通过线程本地存储(Thread Local Storage, TLS)实现的:
MachineState& InterpreterModule::GetCurrentThreadMachineState()
{
// 线程本地存储——每个线程一个 MachineState
static thread_local MachineState tlsMachineState;
return tlsMachineState;
}
使用 thread_local 关键字确保每个线程获取到的是自己的 MachineState。解释器方法不需要加锁来访问 MachineState——因为每个线程只能访问自己的实例。
但是,解释器代码仍然需要线程安全地访问:
- 托管堆(Managed Heap)——对象分配可能触发 GC,GC 需要同步所有线程
- 全局元数据缓存——多个线程可能同时读取缓存,需要线程安全的读取
- 静态字段——多线程访问静态字段需要互斥
这些线程安全问题由底层的 IL2CPP 运行时处理,解释器代码不直接管理。
七、解释器与编译器的协作
7.1 数据流
解释器与编译器之间的数据流是单向的——编译器生成 IR 指令序列,解释器执行它们:
编译器(hybridclr/transform/)
│
│ 编译方法 IL → IR 指令序列
│ HiTransform::Transform(MethodInfo)
│
▼
IRBody 结构体
├── instructions: IR 指令序列(8 字节 × N)
├── resolveDatas: 解析数据数组
├── exceptionClauses: 异常处理子句
└── localVarCount: 局部变量数量
│
│ Execute() 调用
▼
解释器(hybridclr/interpreter/)
│ ExecuteMain(frame)
│ 逐条执行 IR 指令
│
▼
运行时效果
├── 对象分配和初始化
├── 字段读写和数组操作
├── 方法调用(解释器/原生/AOT)
├── 控制流跳转和循环
└── 异常处理
7.2 编译时机
方法体在何时被编译?
在 HybridCLR 中,编译器采用懒编译(Lazy Compilation)策略——方法在第一次被执行前才被编译:
// 当方法需要被解释执行时
const MethodInfo* method = resolveDatas[methodIdx].method;
// 检查是否已编译
if (method->irBody == nullptr)
{
// 懒编译:现在编译
HiTransform::Transform(method);
}
// 创建帧并执行
InterpFrame* frame = CreateFrame(method, …);
ExecuteMain(frame);
这种策略的优点:
7.3 编译器与解释器的角色权衡
HybridCLR 的架构选择了一个两阶段设计:编译器将 IL → IR,解释器执行 IR。为什么不直接解释执行 IL?
| 指令解析成本 | 每次执行需解析 IL 操作码和操作数 | 一次编译,执行时无解析成本 |
| 评估栈管理 | 需要运行时栈模拟 | 编译器已将栈映射为直接索引 |
| 控制流 | 需要基本块边界处理 | 编译器已将复杂控制流展开 |
| 性能 | 慢(每次重复解析) | 快(接近原生 AOT 性能的 30-50%) |
| 内存 | 低(不需要 IR 存储) | 高(需要存储 IR 指令序列) |
HybridCLR 选择了"编译一次,执行多次"的路径,这与经典 JIT(Just-In-Time)的哲学一致——编译开销摊分到多次执行中。
八、解释器模块的启动流程
当一个混合运行时应用启动时,解释器模块的完整启动流程如下:
1. Unity 引擎启动
2. IL2CPP Runtime 初始化
3. HybridCLR 初始化
├── InterpreterModule::Initialize()
│ ├── 分配解释器全局内存池
│ ├── 注册解释器回调到 IL2CPP 运行时
│ ├── 初始化线程本地存储设置
│ └── 预计算 IR 指令属性
│
4. 热更新 DLL 加载
├── 元数据模块加载程序集元数据
├── 注册热更新方法到运行时
│
5. 用户代码执行
├── AOT 代码调用热更新方法
│ └── IL2CPP 运行时检测到该方法是解释执行
│ └── InterpreterModule::Execute() 被调用
│ ├── 检查方法是否已编译(IR 是否存在)
│ ├── 如未编译:HiTransform::Transform() 编译
│ ├── EnterFrame()
│ ├── ExecuteMain() 主循环
│ └── LeaveFrame()
│
6. 运行时持续
├── 新的热更新方法被调用 → 按需编译 → 执行
└── AOT <-> 解释器双向调用
九、解释器模块的演进
9.1 字节码解释 vs IR 解释
早期版本的 HybridCLR 可能采用直接解释 IL 字节码的方式。后续版本演进到了当前的 IR 两阶段架构。这个演进的核心推动力是性能:
直接解释 IL 时,每条 IL 指令都需要:
IR 解释时,每条 IR 指令只需要:
9.2 与后续优化
IR 架构也为后续优化打开了空间:
- inline caching——在 CallInterp IR 指令中缓存目标方法地址,减少方法查找开销。例如在 CallInterp_void 指令中,可以对同一个调用点的多次调用进行缓存,避免重复的方法查找和类型检查
- tail call dispatch——使用 computed goto(GCC 扩展)替代 switch,提升 dispatch 效率。在 switch 语句中,CPU 需要将 opcode 的值与每个 case 标签进行比较;而 computed goto 使用跳转表直接跳转到对应处理代码的地址,消除了比较和分支的开销
- 热点方法标识——统计执行频率,标记热点方法供编译器优化参考。如果解释器发现某个方法被执行了足够多次,可以将其标记为"热点",后续版本的优化策略可能会将其升级到更高效的执行路径
9.3 解释器的"轻量"设计哲学
HybridCLR 的解释器被刻意设计为"轻量"的执行引擎——它的职责仅限于执行 IR 指令,不做运行时优化(无 JIT 风格的动态编译升级),不做运行时类型分析(无动态反虚拟化),不做 profile-guided 优化。所有的复杂分析和变换都在编译器阶段(IL → IR)完成。这种"编译期做重活、运行期做轻活"的设计哲学保证了解释器主循环的简单性和可维护性,也使得性能特征更可预测——不存在 JIT 中常见的"预热期"和"稳定期"的区分。这种编译期重、运行期轻的策略让 HybridCLR 在启动速度和峰值性能之间取得了实用的平衡。
总结
本文全面分析了 HybridCLR 解释器模块的整体架构。核心要点:
解释器模块的文件结构——InterpreterModule(全局管理)、Engine(MachineState 运行时状态)、Interpreter_Execute(指令执行)、InterpreterDefs(类型定义)、Instruction(IR 指令定义)
InterpreterModule 的角色——解释器全局初始化、线程本地 MachineState 管理、方法调用的 Execute 入口点
MachineState 的三条运行时栈——评估栈(Eval Stack)用于中间值存储、帧栈(Frame Stack)管理方法调用链、异常流栈(Exception Flow Stack)跟踪异常处理状态
InterpFrame 的链表结构——previous 指针形成单向链表,支持栈回溯和异常传播。帧中关键的 localVarBase 指向 StackObject 数组(参数 + 局部变量 + 临时栈)
局部变量数组的布局——参数区在最前、然后是局部变量区、最后是临时栈区。评估栈不是独立分配,而是 localVarBase 数组的一部分,编译器在编译时已将评估栈位置映射为数组索引
ExecuteMain 主循环——while(true) + switch(inst->opcode) 模式,所有 924 个 opcode 在同一个 switch 中处理。所有 IR 指令固定 8 字节,执行后 ip += 8
EnterFrame/LeaveFrame 的帧管理——链入帧栈、零初始化局部变量、栈溢出检测(EnterFrame);清空异常流栈、恢复帧栈和 IP(LeaveFrame)
重入与线程安全——ExecuteMain 递归调用支持解释器方法链;thread_local MachineState 保证线程安全
编译器与解释器的协作——懒编译策略(方法首次执行前编译)、IR 两阶段架构(一次编译多次执行)的性能优势
参考资源
- hybridclr/interpreter/InterpreterModule.h/.cpp — 解释器模块初始化和 Execute 入口点
- hybridclr/interpreter/Engine.h/.cpp — MachineState 结构和 EnterFrame/LeaveFrame
- hybridclr/interpreter/Interpreter_Execute.cpp — ExecuteMain 主循环和 IR 指令执行
- hybridclr/interpreter/InterpreterDefs.h — InterpFrame、StackObject 和常量定义
- hybridclr/interpreter/Instruction.h — 所有 IR 指令的结构体定义
- hybridclr/transform/TransformModule.h — 编译器模块初始化(HiTransform::Transform)
- 第 21-25 篇 — 编译器模块(变换流程、IR 生成、异常/GC 的 IR 支持)
- ECMA-335 Partition III — CIL 指令集规范


