6、【MOD开发】【骑砍2】跨平台MOD构建环境(六)/:DLL 里到底装了什么——元数据、IL 与 JIT

发布时间:2026/10/20 2:18:54
6、【MOD开发】【骑砍2】跨平台MOD构建环境(六)/:DLL 里到底装了什么——元数据、IL 与 JIT
【声明】本博客所有内容均为个人业余时间创作所述技术案例均来自公开开源项目如GithubApache基金会不涉及任何企业机密或未公开技术如有侵权请联系删除6、【MOD开发】【骑砍2】跨平台MOD构建环境六/DLL 里到底装了什么——元数据、IL 与 JIT背景上篇 【MOD开发】【骑砍2】跨平台MOD构建环境五/元数据查看工具与跨版本适配 讲了一个只读元数据的小工具它不需要启动游戏、也不需要加载依赖就能从参考程序集里查出类型与成员签名。文中提到一个关键前提——参考程序集只含元数据、不含 IL 程序体。这句话背后其实藏着一串基础概念DLL 到底是什么元数据是什么IL 是什么JIT 又是什么这几个词在后面讲参考程序集、GamePath、Privatefalse/Private时还会反复出现所以这一篇先把它们一次讲清。骑砍2 先把四个词放在一起先用一句话把四个词区分开词一句话解释DLL.NET 程序集的文件形式PE 文件是容器不代表一定包含可执行代码元数据类型、成员、签名、特性、引用关系的描述编译器用它做检查IL方法体里的中间指令CPU 无关JIT运行时把 IL编译成当前 CPU 的机器码再压缩成一句编译器看元数据运行时 JIT 编 ILDLL 只是装这两样东西的容器。 DLL 是文件格式不是“代码类型”很多人把 DLL 理解成“一段可执行代码”这在 .NET 里并不准确。DLL 是PEPortable Executable可移植可执行文件格式的一种文件扩展名它可以是一个 .NET 程序集本系列讨论的就是这种一个原生 DLLC/C 编译出来的不含 .NET 元数据甚至只是一个资源容器。在 .NET 语境里DLL 和 EXE 都可能是程序集扩展名只表示“这是库还是应用”的约定不决定里面装了什么。一个运行程序集通常由三块组成Mod.dll ├── 元数据Metadata ← 类型、成员、签名、特性、引用 ├── IL方法体 ← 每个方法的指令 └── 资源Resources ← 字符串、图片等参考程序集只保留第一块以及必要的类型定义第二块被剥离或替换成不可执行的占位所以它通常小很多也不能直接运行。 元数据类型和成员的“目录”元数据描述的是API 的形状包括有哪些类型、继承自谁、实现了哪些接口每个类型有哪些字段、属性、方法每个方法的返回类型、参数类型、是否静态、可见性特性Attribute、程序集引用等。它不包含“方法怎么做”只包含“方法长什么样”。编译器做类型检查时只需要这份目录你调用的方法是否存在参数类型对不对返回类型能不能赋给目标变量第五篇的元数据工具示例名meta.sh之所以能直接读参考程序集就是因为它用的是PEReaderMetadataReader只解析元数据表不去加载依赖、也不读方法体。一句话元数据是“菜单”不是“菜谱”。 IL方法体里的“中间指令”C# 编译器csc/dotnet build不会直接生成机器码而是把源码编译成ILIntermediate Language中间语言也叫CIL / MSIL。IL 有几个特点它是栈式字节码指令围绕一个求值栈工作它是CPU 无关的同一份 IL 可以在 x64、ARM64 等不同 CPU 上运行它不是汇编也不是机器码它存放在运行程序集的方法体里。一个极简的例子假设它是静态方法staticintAdd(inta,intb)ab;编译成 IL 后大意是ldarg.0 // 取第一个参数 a ldarg.1 // 取第二个参数 b add // 相加 ret // 返回结果这几条是 IL 指令不是 CPU 指令。真正把它们变成机器码的是下面的 JIT。⚙️ JIT运行时把 IL 变成机器码JIT Just-In-Time compilation即时编译。.NET 程序运行时CLR 里的 JIT 编译器会在第一次调用某个方法时把该方法的 IL 编译成当前 CPU 的机器码然后缓存起来后续再调用这个方法直接执行缓存好的机器码。所以整条链路是C# 源码 │ 编译器csc / dotnet build ▼ IL 元数据.dll │ 运行时 JIT ▼ 机器码当前 CPU │ ▼ CPU 执行几个容易混淆的点问题答案dotnet build生成的是机器码吗不是生成的是 IL 程序集JIT 是解释器吗不是它把 IL 编译成机器码再执行并缓存结果为什么同一份 DLL 能跨 CPUDLL 里是 ILJIT 在运行时按当前 CPU 生成机器码参考程序集需要 JIT 吗不需要它没有可执行的方法体Bannerlord 运行在 .NET Framework 上走的是 Framework 的 JIT不同运行时.NET Framework / .NET Core在分层编译等细节上略有差异但“运行时把 IL 编译成机器码”这个核心机制是一样的。 AOT / ReadyToRun / NativeAOT另一条路线JIT 是“运行时编译”对应的另一条路线是AOTAhead-Of-Time提前编译在运行之前就把 IL或源码编译成机器码。现代 .NET 里常见的几种方式方式编译时机运行时是否还需要 JIT典型场景JIT运行时首次调用方法需要桌面应用、游戏、.NET FrameworkReadyToRun构建时预编译同时保留 IL可以回退到 JIT.NET Core 应用启动优化NativeAOT发布时完全编译成原生代码不需要没有 IL/JITCLI 工具、容器、移动端几个补充ReadyToRun不是“完全 AOT”它提前编译了一部分机器码但程序集里仍保留 IL遇到未预编译的方法还可以回退到 JIT。NativeAOT生成的是原生可执行文件/库不依赖运行时 JIT代价是平台相关、对反射和动态加载有限制。.NET Framework 时代还有NGEN思路类似 ReadyToRun提前生成原生镜像但 IL 仍然保留运行时可以回退到 JIT。对 Bannerlord MOD 来说这三条路线只需要了解概念游戏本身走 JITMOD 也是以托管程序集DLL的形式被游戏加载的。NativeAOT 生成的是原生二进制不是游戏 MOD 加载器期望的托管程序集所以这里不是它的用武之地。 把整条链路串起来再回头看那几个词它们各自的位置就很清楚了角色负责什么DLL容器装元数据、IL、资源元数据描述 API类型、成员、签名、特性、引用IL描述方法体CPU 无关的中间指令JIT运行时把 IL 翻译成当前 CPU 的机器码编译期编译器只读元数据检查“类型和签名对不对”运行期CLR/JIT 才读 IL把它变成机器码执行。参考程序集和运行程序集的区别本质上就是“只给编译器看”还是“真正拿来运行”——这正是下一篇的主题。⚠️ 常见误区“DLL 一定可执行”不对。参考程序集也是 DLL但它不能运行。“IL 是汇编”不对。IL 是字节码JIT 之后才是机器码。“JIT 是解释器”不对。JIT 会编译并缓存机器码不是逐条解释。“dotnet build直接生成机器码”不对。它生成的是 IL 程序集。“参考程序集不是 DLL”不对。它也是 DLL只是方法体被剥离/替换成了占位。“运行时也需要参考程序集”不需要。运行时用的是游戏自己的运行程序集。 本篇小结DLL 是 .NET 程序集的文件形式是容器里面可能有元数据、IL、资源。元数据描述 API编译器用它做类型检查第五篇的元数据工具读的就是它。IL 是方法体里的中间指令CPU 无关dotnet build生成的是 IL不是机器码。JIT 在运行时把 IL 编译成当前 CPU 的机器码并缓存Bannerlord 走的是 .NET Framework 的 JIT。AOT / ReadyToRun / NativeAOT 是“提前编译”路线了解概念即可MOD 这里仍然以 JIT 为主。一句话记忆DLL 是容器元数据是目录IL 是方法体JIT 是运行时的翻译官。下一篇讲两套 DLL 的分工参考程序集 vs 运行程序集——为什么编译用前者、运行用后者以及它和GamePath、Privatefalse/Private的关系。OK本篇先到这里如有疑问欢迎评论区留言讨论祝各位功力大涨技术更上一层楼更多内容见下篇 blog【MOD开发】【骑砍2】跨平台MOD构建环境七/参考程序集 vs 运行程序集——两套 DLL 的分工