- 语言运行时
- 标准库
- JIT编译
- 编译器
【免费下载链接】runtime
.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
导读
本文介绍 .NET CoreCLR 中 IL 汇编器(ilasm)的语法解析器(Parser)生成流程:如何在 Windows、Unix 以及 Docker 环境中使用 Bison 从 BNF 语法文件asmparse.y生成 C++ 解析器prebuilt/asmparse.cpp,并说明配套的 GrammarExtractor 工具如何从同一语法文件提取面向文档与工具链的 BNF 文法。读完本文,你将掌握 ilasm 解析器的完整再生成方法、语法文件的核心结构、预生成产物在构建系统中的作用,以及如何在容器环境中快速完成解析器重建。
一、背景:ilasm 的语法解析器从何而来
ilasm(IL Assembler)是 .NET CoreCLR 运行时的一部分,负责把 MSIL 汇编文本(.il 文件)汇编为托管程序集。它的核心语法定义存放在 src/coreclr/ilasm/asmparse.y(约 2000 行),这是一个标准的 Yacc(Yet Another Compiler-Compiler)语法文件,采用 BNF(Backus-Naur Form)文法描述 IL 汇编语言的词法单元与语法规则。
与很多使用 Yacc 的项目不同,ilasm并不在每次构建时调用 Bison,而是将生成的解析器源文件预先提交到仓库中:
- src/coreclr/ilasm/prebuilt/asmparse.cpp:Bison 生成的 C++ 解析器主体;
- src/coreclr/ilasm/prebuilt/asmparse.grammar:从同一语法文件提取的纯 BNF 文法文本。
构建系统直接编译这份预生成文件。在 src/coreclr/ilasm/CMakeLists.txt 中可以看到ILASM_SOURCES明确列出了prebuilt/asmparse.cpp作为编译单元,它与assem.cpp、writer.cpp、method.cpp、asmman.cpp、main.cpp、assembler.cpp、portable_pdb.cpp一起组成 ilasm 可执行文件。这意味着:任何对 IL 语法的修改,都必须先修改asmparse.y,再重新运行 Bison 生成新的asmparse.cpp并提交,解析器生成是"离线"完成的。
正因如此,src/coreclr/ilasm/README.md 作为本目录的入口文档,其核心价值就是回答一个问题:如何在不同平台上正确地再生成asmparse.cpp。
二、环境准备:各平台所需的 Bison 工具
生成解析器需要 Yacc/Bison 兼容工具,各平台获取方式如下:
| 平台 | 工具 | 获取方式 |
|---|---|---|
| Windows | win_bison(Win flex-bison) | https://github.com/lexxmark/winflexbison |
| Unix / Linux / macOS | yacc(GNU Bison) | https://www.gnu.org/software/bison |
- Windows 侧推荐使用 Win flex-bison 项目提供的
win_bison可执行文件,它是以 Windows 原生方式移植的 Bison/flex 工具集; - 其他平台使用 GNU 官方发布的 Bison,安装后以
yacc命令调用(Bison 兼容传统的 Yacc 命令行接口)。
说明:Bison 是 Yacc 的 GNU 实现,向下兼容传统 Yacc 语法,因此
asmparse.y既可以被 Bison 处理,也可以被yacc命令调用。仓库中的调用方式统一使用yacc名称。
三、核心操作:再生成 asmparse.cpp
在任意平台上,生成命令的要点都是:输入asmparse.y,指定输出文件为prebuilt/asmparse.cpp,确保生成结果直接落入预构建目录。
3.1 Unix / Linux / macOS
yacc asmparse.y -o prebuilt/asmparse.cpp即调用系统 Bison 生成的yacc命令,读取 src/coreclr/ilasm/asmparse.y,把生成的 C++ 解析器写入 src/coreclr/ilasm/prebuilt/asmparse.cpp。
3.2 Windows
win_bison asmparse.y -o prebuilt\asmparse.cpp在 Windows 上使用 winflexbison 提供的win_bison,注意输出路径使用 Windows 风格反斜杠分隔符prebuilt\asmparse.cpp。命令必须在src/coreclr/ilasm目录下执行,以便相对路径asmparse.y与prebuilt/asmparse.cpp正确解析。
3.3 Docker 一键生成(无需本机安装 Bison)
对于不想在宿主机安装 Bison 的开发者,README 提供了基于 Alpine Linux 的容器化方案:
$ cd runtime # run a throw-away-after-exit container with --rm $ docker run --rm -v$(pwd):/runtime -w /runtime/src/coreclr/ilasm alpine \ sh -c 'apk add bison && yacc asmparse.y -o prebuilt/asmparse.cpp'逐项拆解这条命令:
--rm:容器退出后立即删除,不保留中间层,避免残留垃圾容器;-v$(pwd):/runtime:把当前目录(仓库根目录runtime)挂载到容器内的/runtime;-w /runtime/src/coreclr/ilasm:设置容器工作目录为 ilasm 源码目录,使asmparse.y相对路径可直接使用;alpine:使用轻量级 Alpine Linux 镜像(体积小、启动快);sh -c 'apk add bison && yacc asmparse.y -o prebuilt/asmparse.cpp':先通过 Alpine 包管理器安装 Bison,再执行与 Unix 平台完全相同的生成命令。
由于工作目录被挂载为宿主机的 ilasm 目录,容器内生成的prebuilt/asmparse.cpp会直接落盘到宿主机仓库中,容器销毁不影响产物。这是三种方式中最干净、最可复现的做法,适合 CI 或没有本地工具链的环境。
四、语法文件 asmparse.y 的内部结构
要理解生成流程,有必要了解被处理的输入文件 asmparse.y 的组织方式。它分为标准 Yacc 的三个区段:
4.1 声明区(Prologue + %union/%token)
- 文件头部(
%{ ... %}之间)通过#include "ilasmpch.h"引入 ilasm 预编译头,并#include "grammar_before.cpp"引入解析器辅助代码; - grammar_before.cpp 中定义了解析器所需的全局状态与工具函数,包括:
YYMAXDEPTH 0x80000栈深度上限、FAIL_UNLESS断言宏;- 全局解析指针
PASM/PASMM,指向汇编器与清单对象; - 词法回调函数指针(
PFN_NEXTCHAR、PFN_SYM等),通过SetFunctionPtrs()从解析环境注入; - 预处理指令支持(
DefineVar/UndefVar/FindVarDef/IsVarDefined),对应 IL 源文件中的#define/#ifdef等编译控制指令。
%union定义了语法符号的值类型联合体,覆盖属性标志(类、方法、字段、事件、属性、PInvoke 映射等)、整型/浮点/字符串字面量、BinStr二进制串、IL 指令操作码、mdToken元数据令牌等;%token声明了全部词法单元,例如ID、DOTTEDNAME、QSTRING、INT32_V、FLOAT64,以及按类别分组的 IL 指令令牌INSTR_NONE、INSTR_VAR、INSTR_I、INSTR_BRTARGET、INSTR_METHOD、INSTR_FIELD、INSTR_SWITCH等;- 汇编器伪指令(assembler directives)也以令牌形式声明,如
_CLASS、_METHOD、_FIELD、_TRY、_MAXSTACK、_LOCALS、_ENTRYPOINT、_CUSTOM、_PERMISSION、_ASSEMBLY等,语法规则中它们以.class、.method、.try、.maxstack等形态出现。
4.2 规则区(%start decls 之后)
以%%分隔后进入语法规则。入口为decls,最顶层decl规则定义了 IL 文件的模块级声明集合(asmparse.y 第 189-228 行):
decls : /* EMPTY */ | decls decl ; decl : classHead '{' classDecls '}' { PASM->EndClass(); } | nameSpaceHead '{' decls '}' { PASM->EndNameSpace(); } | methodHead methodDecls '}' { ... PASM->EndMethod(); } | fieldDecl | dataDecl | vtableDecl | vtfixupDecl | extSourceSpec | fileDecl | assemblyHead '{' assemblyDecls '}' { PASMM->EndAssembly(); } | assemblyRefHead '{' assemblyRefDecls '}' { PASMM->EndAssembly(); } | exptypeHead '{' exptypeDecls '}' { PASMM->EndComType(); } | manifestResHead '{' manifestResDecls '}' { PASMM->EndManifestRes(); } | moduleHead | secDecl | customAttrDecl | _SUBSYSTEM int32 { PASM->m_dwSubsystem = $2; } | _CORFLAGS int32 { PASM->m_dwComImageFlags = $2; } | _FILE ALIGNMENT_ int32 { PASM->m_dwFileAlignment = $3; ... } | _IMAGEBASE int64 { PASM->m_stBaseAddress = (ULONGLONG)(*($2)); ... } | _STACKRESERVE int64 { PASM->m_stSizeOfStackReserve = ...; } | languageDecl | typedefDecl | compControl | _TYPELIST '{' classNameSeq '}' | _MSCORLIB { PASM->m_fIsMscorlib = TRUE; } ;每个规则后的{ ... }动作代码在归约时执行,直接调用汇编器对象(PASM)的方法或写入其字段,例如.subsystem、.corflags、.file alignment、.imagebase、.stackreserve等 PE 头选项正是在这里落到PASM的对应成员变量上,后续由 writer 输出到 PE 文件头。
4.3 辅助区(%%,语法规则后的用户代码)
规则区结束后可包含额外的 C++ 代码段,与声明区(grammar_before.cpp)对应,仓库中还有 grammar_after.cpp 存放规则之后的部分辅助实现。Bison 生成的asmparse.cpp会把声明区代码、规则表与这些辅助代码拼接在一起。
五、配套工具:GrammarExtractor 提取 BNF 文法
除了生成 C++ 解析器,仓库还提供一个名为 GrammarExtractor 的 .NET 工具,用于从asmparse.y提取纯 BNF 文法,输出给文档、分析工具与后续工具链使用。其使用说明见 GrammarExtractor/README.md:
cd runtime ./dotnet.sh run --project src/coreclr/ilasm/GrammarExtractor src/coreclr/ilasm/asmparse.y > src/coreclr/ilasm/prebuilt/asmparse.grammar执行流程:
- 通过仓库根目录的 dotnet.sh 运行 GrammarExtractor 工程(src/coreclr/ilasm/GrammarExtractor/GrammarExtractor.csproj);
- 传入
asmparse.y作为唯一命令行参数; - 标准输出重定向到
prebuilt/asmparse.grammar,生成可直接阅读的 BNF 文法文件。
5.1 提取原理:基于正则的规则重写
Program.cs 展示了提取逻辑,它并不真正解析 Yacc 文件,而是通过若干正则变换完成"语法到 BNF"的翻译:
GetRegexExtractMarkers(^(.*)%%(.*)%%):截取规则区(两个%%之间的内容),若找不到标记则报错退出;GetRegexRemoveTextInBraces(\s*([^'])\{[^{}]*\}):反复迭代删除所有{ ... }动作代码块,直到不再变化(因为动作块可以嵌套);GetRegexKeywordIdentifiers(\b([A-Z0-9_]+)_\b):把形如CLASS_、METHOD_的关键字令牌改写为其小写字符串字面量'class'、'method';GetRegexAssemblerDirectives(\b_([A-Z0-9]+)\b):把_CLASS、_TRY等汇编器伪指令改写为带点号的小写形式'.class'、'.try';- 特殊标点处理:
ELLIPSIS→'...',DCOLON→'::'; - 输出固定的词法单元说明头(
Lexical tokens/Auxiliary lexical tokens段),再拼接处理后的规则文本。
5.2 输出文件 asmparse.grammar
生成结果 prebuilt/asmparse.grammar 以 BNF 形式呈现 IL 文法,开头的词法单元清单完整描述了两类记号:
基本词法单元(Lexical tokens):
| 记号 | 含义 | 示例 |
|---|---|---|
ID | C 风格字母数字标识符 | Hello_There2 |
DOTTEDNAME | 点分隔标识符序列 | System.Object |
QSTRING | C 风格双引号字符串 | "hi\n" |
SQSTRING | 单引号字符串 | 'hi' |
INT32 | C 风格 32 位整数 | 235、03423、0x34FFF |
INT64 | C 风格 64 位整数 | -2353453636235234、0x34FFFFFFFFFF |
FLOAT64 | C 风格浮点数 | -0.2323、354.3423、3435.34E-5 |
INSTR_* | 各类 IL 指令 | 参见opcode.def |
HEXBYTE | 1-2 位十六进制数 | A2、F0 |
辅助词法单元(Auxiliary lexical tokens):TYPEDEF_T(类型别名 TypeDef/TypeRef)、TYPEDEF_M(方法别名)、TYPEDEF_F(字段别名)、TYPEDEF_TS(TypeSpec 别名)、TYPEDEF_MR(MemberRef 别名)、TYPEDEF_CA(自定义特性别名)——这些是 ilasm 的.typedef ... as ...语法糖在解析器中的实现载体。
文法正文则从START : decls开始,逐条列出经过动作代码剥离后的纯净 BNF 规则,例如模块级声明、类声明、方法声明、自定义特性 blob、编译控制指令等,可直接作为 IL 语言规范的机器可读参考。
六、预生成产物的构建集成与注意事项
6.1 构建系统如何消费 asmparse.cpp
src/coreclr/ilasm/CMakeLists.txt 展示了生成产物在构建中的角色:
- 编译单元明确包含
prebuilt/asmparse.cpp(见ILASM_SOURCES); - Unix 平台上对该文件做了特殊编译处理(CMakeLists.txt 第 56-64 行):
- 添加
-Wno-register、-Wno-array-bounds、-Wno-unused-label以压制 Bison 生成代码在 Clang/GCC 下的告警; - 通过
COMPILE_FLAGS "-O0"对该文件关闭优化——这是因为该预生成文件在优化下存在已知的 bad-codegen 问题(注释引用了 dotnet/runtime issue #4776);
- 添加
- Windows 上额外把头文件加入
ILASM_SOURCES并定义FX_VER_INTERNALNAME_STR=ilasm.exe。
这解释了为什么建议直接提交重新生成的asmparse.cpp:构建系统针对这份特定产物的行为是经过调优的,任何对语法的改动都应同步更新asmparse.y与prebuilt/asmparse.cpp两份文件,保证源码与生成代码一致。
6.2 实操注意事项
- 保持输出路径为
prebuilt/asmparse.cpp:不要输出到临时目录后手动拷贝,三种平台的命令都已按此设计; - 从
src/coreclr/ilasm目录执行:asmparse.y与prebuilt/的相对路径均以此目录为基准;Docker 方式通过-w参数保证这一点; - Windows 注意反斜杠:
-o prebuilt\asmparse.cpp使用 Windows 路径分隔符; - Docker 需要挂载卷:不要省略
-v$(pwd):/runtime,否则生成的产物只存在于容器内,--rm退出后即丢失; - 语法变更要同步两份产物:修改 asmparse.y 后,应同时重新运行 Bison 生成
asmparse.cpp、运行 GrammarExtractor 生成asmparse.grammar,保持三个文件一致。
七、总结
ilasm 的解析器生成是一条"人工触发 + 产物入库"的离线流程,核心链路为:
asmparse.y(Yacc/BNF 语法源) │ ├── Bison(yacc / win_bison)──► prebuilt/asmparse.cpp(C++ 解析器,编译进 ilasm) │ └── GrammarExtractor(dotnet run)──► prebuilt/asmparse.grammar(纯 BNF 文法文档)无论是 Windows、Unix 还是 Docker 环境,生成命令都极为精简;掌握 src/coreclr/ilasm/README.md 中的这三条命令,配合对 asmparse.y 规则区结构与 CMakeLists.txt 编译集成的理解,即可安全地修改、再生成并验证 ilasm 的语法解析能力,也能借助asmparse.grammar深入研读 .NET IL 汇编语言的完整 BNF 定义。
- 语言运行时
- 标准库
- JIT编译
- 编译器
【免费下载链接】runtime
.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
相关推荐
从 ABI 约束到代码生成:.NET CoreCLR ARM64 JIT 栈帧布局设计详解
从 ABI 约束到代码生成:.NET CoreCLR ARM64 JIT 栈帧布局设计详解 阅读对象:对 RyuJIT 后端(尤其 ARM64)感兴趣的运行时开
语言运行时标准库JIT编译编译器终极Sigil CIL生成器使用指南:从零到精通的.NET动态代码生成
终极Sigil CIL生成器使用指南:从零到精通的.NET动态代码生成 Sigil是一个功能强大的开源CIL生成工具,专为.NET平台的动态代码生成和验证而设计
开发工具.NET CoreCLR amd64 调试器指令解码表生成:从反汇编自动构建 `amd64InstrDecode.h` 的完整方案
.NET CoreCLR amd64 调试器指令解码表生成:从反汇编自动构建 amd64InstrDecode.h 的完整方案 本文围绕 dotnet/runt
语言运行时标准库JIT编译编译器
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考