LLVM 后端实践笔记 附录A:使用 Simulator 验证编译器
最近家里事情比较多,拖了一段时间。这个笔记已经基本完成了,还剩下最后一章,使用 simulator 来验证我们开发的编译器,我将其放到附录里,是因为它不属于开发工作,只是辅助工具。我们的 Simulator 实现了 Cpu0 的指令功能,从而可以将 hex 格式的代码文件在之上运行,并能在终端中查看运行结果。
- LLVM 后端实践笔记 0:序
- LLVM 后端实践笔记 1:新后端初始化和软件编译
- LLVM 后端实践笔记 2:后端结构
- LLVM 后端实践笔记 3:算术和逻辑指令
- LLVM 后端实践笔记 4:生成目标文件
- LLVM 后端实践笔记 5:全局变量
- LLVM 后端实践笔记 6:更多数据类型
- LLVM 后端实践笔记 7:控制流
- LLVM 后端实践笔记 8:函数调用
- LLVM 后端实践笔记 9:ELF文件支持
- LLVM 后端实践笔记 10:汇编
- LLVM 后端实践笔记 附录A:使用 Simulator 验证编译器
A.1 运行 Simulator
A.1.1 编译 Simulator
我们的 Simulator 使用 Verilog 语言实现,我借用了原作者提供的代码,代码文件是 cpu0.v,需要使用 verilog 编译器将其编译成可执行文件。
在我的 Mac 上,安装 iverilog 软件,它可以用来编译 verilog 代码:
1 | brew install icarus-verilog |
安装后,编译的命令是:
1 | iverilog -D TRACE -o cpu0Is cpu0.v |
通过传参编译出两种不同字节序的机器文件。-D TRACE 是定义一个 verilog 中的宏变量,用来表示需要追踪运行时寄存器和内存的值。
运行我们的代码不能只提供 main 函数,还需要提供一些启动引导代码和辅助函数,比如 print 一类。
我在示例代码中,将编译命令写在了 Makefile 中,可以直接在根目录下执行 make 来完成构建。
A.1.2 启动引导文件
这里要说明,因为我们没有链接器参与,所以需要手动将各段代码按照正常的方式排布,所以你会看到 include 一个 c 文件和汇编文件这种情况。包括在你的子函数中,如果需要多个文件的 c 代码,也需要手动将其编排到一起,并要保证声明顺序,同时还要注意跳转距离(不要写太复杂的程序就没事)。之后可以再做个链接器,让这些步骤更自然。
对于我们的编译器来看,以下的几段代码都存在一个完整的程序块中。
(1) start.h
用来在程序最开头插入一段汇编,用来初始化一些寄存器,比如 $sw 、$fp 和通用寄存器。
(2) boot.c
紧接着 start.h 之后的代码,需要添加启动引导代码,逻辑就是:
设置中断和异常的钩子符号,比如复位、错误、外部中断,因为我们目前没有这些需求,所以这里就定位到自身,做死循环,之后如果有需求再调整。
调整
$gp和$lr寄存器。调整栈指针寄存器
$sp。设置一些系统寄存器,比如
$mfc0。跳转到 main 入口,假设 main 入口就是紧随其后的代码,也可以选择跳转到标号 main。
(3) print.h/c
编写打印输出到终端的函数,思路就是将字符串写入到一段内存,内存起始地址是 OUT_MEM,我们初始化为 0x80000,Simulator 的实现代码中会将这个地址当做与终端交互的 IO。
(4) run.c
驱动程序。我们将运行的代码都当做是子函数,使用这一个地方的 main 代码作为入口来调用。在我的示例代码中,先调用子函数,然后打印输出结果。
A.1.3 构建 hex 文件脚本
我编写了一个构建脚本,自动化的调用各种构建工具,主要步骤为:
使用 clang 生成 IR 文件。
使用 llc 生成目标文件。
使用 llvm-objdump 对目标文件做截取,提取 .text 段,并使用 awk 工具格式化输出。
使用 less 工具对 .text 段的文本做调整,只截取可执行的内容。
最后我们就能得到能够执行的 hex 文件了。执行命令:
1 | bash build.sh cpu032I be |
A.1.4 执行 Simulator
在调用 Simulator 时,需要把生成的 hex 文件命名为 cpu0.hex 文件,并放到和 Simulator 同一个路径下。第 3 步中的脚本已经默认生成了 cpu0.hex 文件。
终端执行:
1 | ./cpu0IIs |
选择二者之一,Simulator 就会打印出执行的每一步的信息。
最后会打印出 total cpu cycles,可以大致评价编译器和处理器的性能,这里只能看到 cpu 的 cycle,而无法查看 IO 的 cycle。理论上说 cpu0IIs 会比 cpu0Is 获得更好的执行性能,这个就体现了不同硬件的性能差异(主要是 slt, beq 指令等提升)。
Simulator 会先将代码加载到内存中,然后从 0 地址开始执行每一条指令。这个 Simulator 不支持流水线并行,如果能支持的话,性能应该会再翻好几倍。因为设计一个处理器也是很复杂的事情,我们这里的需求只是简单测试编译器的运行正确性,所以就不再展开去设计更完备的处理器了。
A.2 结尾
这个系列笔记就到此结束了,我自己用了大概一年的时间去系统的学习了《Tutorial: Creating an LLVM Backend for the Cpu0 Architecture》,并实践了其中的过程,之后又花了半年时间将其内容整理成这份笔记,整理的同时又再一次实践,我自己收获特别多,也再次对原作者表示感谢。这个笔记某种程度上可以作为原作文档的概括和重述,我也鼓励需要接触 LLVM 系统的朋友可以像我一样,去做这样一个事情。
工作后不像在学校里了,没有丰富的时间去学习和探索,所以进展很慢,在此对于急切期待等我更新的朋友们说声抱歉,这个教程中我将几乎所有 C++ 的特性剥离了,是为了简化内容,目前依然没有补充 C++ 功能特性的想法,将来如果补充,可能也只是支持重载、多态和类这几个概念吧。
诚实的说,我在使用 Simulator 测试我们的程序时,有些是过不去的,表现为一个无限循环。因为时间原因,我没有一一去检查问题所在,但必须承认,这里边是存在问题的,这也告诉我自己,一个编译器是一个非常复杂的系统,虽然借助于 LLVM 这样优秀的软件框架,已经可以帮助我们完成很多复杂的任务,但如果想设计一个像商用编译器那样鲁棒性极强,又能兼顾扩展性和灵活性的软件系统,是非常困难的。
这个教程对我自己来说,也是学习、巩固和回顾的过程,在完成教程期间,我得到了很多朋友和网友的支持,谢谢你们,我也知道谦虚谨慎是学习进步必备的素养,所以后续有时间,我会继续完善这个系列的代码,解决已知 bug,测试更复杂的软件用例。
有网友私信问我如何学习熟悉 LLVM 的系统,我大概总结的回答是,不要想很快的掌握所有的东西,最有效的办法就是自己亲自动手,去做一个编译器,去解决实际遇到的问题,探索各种对自己有用的调试开发方法,再去追根溯源,阅读 LLVM 公开的源码,学习了解别人的设计思路,最后辅助以官方文档的解释就可以了。当然,对于完全没有概念的初学者,适当看一些中文教程,也是有帮助的。
本文同步发布在知乎账号下:LLVM 后端实践笔记 附录A:使用 Simulator 验证编译器






















