自拍
自拍是奥地利萨尔茨堡大学计算机科学系计算系统组的一个项目。
自拍项目为本科生和研究生提供了一个教育平台,用于教授编程语言和运行时系统的设计和实现。重点是构建编译器、库、操作系统和虚拟机监控器。共同的主题是识别和解决系统代码中的自引用,这被视为教授系统工程时的关键挑战,因此得名。
自拍是一个自包含的64位、12KLOC C语言实现,包括:
- 一个自编译编译器,名为starc,它将一个小但仍然快速的C语言子集称为C Star(C*)编译成一个小型且易于教授的RISC-V子集,称为RISC-U,
- 一个自执行仿真器,名为mipster,它可以执行RISC-U代码,包括用starc编译的自身,
- 一个自托管虚拟机监控器,名为hypster,它提供可以托管整个自拍的RISC-U虚拟机,即starc、mipster和hypster本身,以及
- 一个由自拍使用的tiny C*库,名为libcstar。
自拍在一个(!)文件中实现,并保持最小化以简化。还有一个简单的内存链接器、RISC-U反汇编器、垃圾收集器、L1指令和数据缓存、分析器,以及带有重放功能的调试器,以及以RISC-V系统调用形式内置于仿真器和虚拟机监控器中的最小操作系统支持。垃圾收集器是保守的,甚至是自收集的。它可以作为库在与变异器相同的地址空间中运行,和/或作为内核地址空间中仿真器的一部分运行。
自拍生成的ELF二进制文件可以在真实的RISC-V硬件和QEMU上运行,并与官方RISC-V工具链兼容,特别是spike仿真器和pk内核。
自拍被设计为64位系统,因此需要64位系统才能运行(LP64数据模型)。然而,自拍也可以在支持编译和执行32位二进制文件的系统上编译(ILP32数据模型)。在这种情况下,自拍成为一个32位系统,可以直接生成和执行32位二进制文件。这是可能的,因为自拍的实现在整个系统中都小心避免了32位溢出。
支持
- Slack:在cksystemsteaching.slack.com的#selfie频道加入对话
- 幻灯片:有提供全面介绍自拍设计和实现的课堂幻灯片。
- 自动评分器:有一个带有编译器和操作系统作业的自动评分器。
- 论文:有一篇介绍自拍的Onward! 2017论文。
- 书籍:有一本基于自拍的早期草稿形式的免费书籍,名为基础计算机科学:从比特和字节到计算的普遍性,面向所有对学习计算机科学感兴趣的人。
- 代码:自拍代码是开源的,可在github.com/cksystemsteaching/selfie获取
- X:在x.com/christophkirsch关注我们
- 网站:自拍主页在selfie.cs.uni-salzburg.at
附加内容
-
垃圾回收:除了selfie中保守但时间复杂度为O(n^2)的垃圾回收器外,还有一个针对小内存块的O(n)时间复杂度的Boehm垃圾回收器实现,对于大内存块则回退使用selfie中的垃圾回收器。
-
模糊测试:有一个基于selfie的简单但自我模糊测试的模糊器,名为buzzr,用于对RISC-U代码(包括selfie的所有代码和自身)进行模糊测试。
-
符号执行:有一个基于selfie的自执行符号执行引擎,名为monster,它将RISC-U代码(包括selfie的所有代码和自身)转换为SMT-LIB公式。当且仅当存在使代码以非零退出代码退出或在给定的机器指令数内执行除零操作的输入时,这些公式是可满足的。
-
有界模型检查:有一个基于selfie的自翻译建模引擎,名为beator,它将RISC-U代码(包括selfie的所有代码和自身)转换为BTOR2公式。当且仅当存在使代码以非零退出代码退出、执行除零操作或访问已分配内存块之外的内存的输入时,这些公式是可满足的。
-
位精确代码分析和合成:有一个基于selfie的自翻译建模引擎,名为rotor,它将完整的RISC-V代码(包括selfie的所有代码和自身)转换为BTOR2和SMT-LIB公式。当且仅当存在使代码以非零退出代码退出、执行除零操作或访问内存段之外的内存的输入时,这些公式是可满足的。Rotor还生成支持RISC-V代码合成的模型。
-
BTOR2可视化:有一个名为beatle的可视化工具,它将从RISC-U二进制文件生成的BTOR2公式显示为有向无环图。
-
SAT求解:有一个基于selfie的暴力SAT求解器,名为babysat,用于计算DIMACS CNF格式的SAT公式的可满足性。
-
二进制翻译:有一个基于selfie的自翻译二进制翻译器,它将RISC-U代码(包括selfie的所有代码和自身)翻译成x86二进制代码。
安装Selfie
Selfie可以在Linux、macOS和Windows机器上原生运行,可能还支持其他安装了终端和C编译器的系统。即使您的机器上没有安装C编译器,或者您只能访问网络浏览器,您也可以运行selfie。安装和运行selfie至少有三种方式:
-
在您的机器上原生安装(推荐):下载并解压selfie。然后,打开终端运行selfie,具体见下文。为此,您需要在机器上安装C编译器。我们推荐使用clang或gcc(在Windows上使用cygwin)。
-
在您的机器上使用docker(高级):下载并安装docker。然后,打开终端并输入
docker run -it cksystemsteaching/selfie
。使用docker的优点是您可以在机器上直接运行selfie,而无需安装任何工具,如C编译器。所有必要的甚至可选的工具都预先安装在selfie docker镜像中。但是,您需要知道如何使用docker。 -
在云端(简单但需要互联网连接):如果您只能访问网络浏览器,只需点击这里。或者,创建一个github账户(如果您还没有),然后将selfiefork到您的github账户中。接着,创建一个cloud9学生账户,将其连接到您的github账户,验证您的电子邮件地址并设置密码(重要!),最后将您fork的selfie克隆到一个新的cloud9工作空间中。
此时,我们假设您已经有一个支持运行selfie的系统。以下我们使用make
命令,假设它已安装在您的系统上,这通常是默认的。但是,我们也会展示make
调用的命令,这样如果您的系统没有安装make
,您也可以手动执行该命令。
下一步是生成selfie二进制文件。为此,在终端中cd
到selfie文件夹,然后输入make
。使用docker时,系统会回应make: 'selfie' is up to date
,因为已经预先安装了selfie二进制文件。不使用docker时,make
将在您的机器上或cloud9工作空间中调用C编译器:
cc -Wall -Wextra -O3 -D'uint64_t=unsigned long' selfie.c -o selfie
然后将selfie.c
编译成一个名为selfie
的可执行文件,如-o
选项所指定。这个可执行文件包含C*编译器、mipster模拟器和hypster虚拟机监控器。-Wall
和-Wextra
选项启用所有编译器警告,这在selfie的进一步开发中很有用。-O3
选项指示编译器生成优化代码。-D'uint64_t=unsigned long'
选项用于引导代码。它定义了数据类型uint64_t
,否则由于C*不包含必要的定义,该类型将是未定义的。如果您的系统支持编译和执行32位二进制文件,您也可以尝试make selfie-32
,这将生成一个32位系统,然后生成和执行32位二进制文件。
运行Selfie
一旦您成功编译了selfie.c
,您可以不带任何参数调用selfie
,如下所示:
$ ./selfie
./selfie { -c { source } | -o binary | [ -s | -S ] assembly | -l binary } [ ( -m | -d | -r | -y ) 0-4096 ... ]
在这种情况下,selfie
会回应其使用模式。
为了充分利用自指性,提供选项的顺序很重要。
-c
选项调用C*编译器处理给定的source
文件列表,将它们编译并链接成内部存储的RISC-U代码。例如,可以使用selfie
来编译它自己的源代码selfie.c
,如下所示:
$ ./selfie -c selfie.c
-o
选项将最近一次编译器调用生成的RISC-U代码写入指定的binary
文件。例如,可以指示selfie
编译自身,然后将生成的RISC-U代码输出到名为selfie.m
的RISC-U二进制文件:
$ ./selfie -c selfie.c -o selfie.m
-s
选项将最近一次编译器调用生成的RISC-U代码的RISC-U汇编写入指定的assembly
文件,而-S
选项还会包括近似的行号和指令的二进制表示。类似地,可以指示selfie
编译自身,然后将生成的RISC-U代码输出到名为selfie.s
的RISC-U汇编文件:
$ ./selfie -c selfie.c -s selfie.s
-l
选项从给定的binary
文件加载RISC-U代码。-o
和-s
选项也可以在-l
选项之后使用。但在这种情况下,-s
选项不会生成近似的源代码行号。例如,可以按如下方式加载之前生成的RISC-U二进制文件selfie.m
:
$ ./selfie -l selfie.m
-m
选项调用mipster模拟器执行最近加载或由编译器调用生成的RISC-U代码。模拟器创建一个具有0-4096
MB内存的机器实例。RISC-U代码的source
或binary
名称以及任何剩余的...
参数都会传递给代码的main函数。例如,以下调用使用mipster执行selfie.m
:
$ ./selfie -l selfie.m -m 1
这在语义上等同于不带任何参数执行selfie
:
$ ./selfie
-d
选项类似于-m
选项,但mipster会输出每条执行的指令、其近似源代码行号(如果可用)以及相关的机器状态。另外,-r
选项通过让mipster仅在发生运行时错误(如除以零)时重放代码执行,来限制-d
选项创建的输出量。在这种情况下,mipster只输出错误发生前刚刚执行的指令。
如果你使用docker,也可以直接在spike和pk上执行selfie.m
,如下所示:
$ spike pk selfie.m
这在语义上再次等同于不带任何参数执行selfie
。
-y
选项调用hypster虚拟机监控器执行RISC-U代码,类似于mipster模拟器。与mipster的区别在于,hypster创建RISC-U虚拟机而不是RISC-U模拟器来执行代码。请参见下面的示例。
自编译
以下是如何执行selfie.c
的自编译,然后检查通过执行./selfie
二进制文件为selfie.c
生成的RISC-U代码selfie1.m
是否等同于通过执行刚刚生成的selfie1.m
二进制文件生成的代码selfie2.m
的示例:
$ ./selfie -c selfie.c -o selfie1.m -m 2 -c selfie.c -o selfie2.m
$ diff -s selfie1.m selfie2.m
Files selfie1.m and selfie2.m are identical
注意,这需要至少2MB的内存才能工作。
自执行
以下示例展示了如何执行mipster模拟器的自执行。在这种情况下,我们调用mipster来调用自身以执行selfie
:
$ ./selfie -c selfie.c -o selfie.m -m 2 -l selfie.m -m 1
这在语义上再次等同于不带任何参数执行selfie
,但这次selfie
打印其使用模式的速度要慢得多,因为有一个mipster运行在另一个mipster之上。
自托管
前面的示例也可以通过在mipster上运行hypster来完成。这显著更快,并且需要更少的内存,因为hypster不会在第一个模拟器实例之上创建第二个模拟器实例。相反,hypster创建一个虚拟机来执行selfie,该虚拟机与第一个模拟器实例上的hypster并行运行:
$ ./selfie -c selfie.c -o selfie.m -m 1 -l selfie.m -y 1
我们甚至可以在mipster上运行hypster上的hypster,这仍然相当快,因为仍然只涉及一个模拟器实例,而hypster本身并不增加太多开销:
$ ./selfie -c selfie.c -o selfie.m -m 2 -l selfie.m -y 1 -l selfie.m -y 1
工作流程
要编译任何C*源代码并立即执行它,而不生成RISC-U二进制文件,可以在单次调用selfie
时使用:
$ ./selfie -c any-cstar-file.c -m 1 "arguments for any-cstar-file.c"
同样,你也可以使用selfie编译的selfie
版本,让mipster模拟器执行selfie来编译任何C*源代码,然后立即用hypster在同一模拟器实例上执行它:
$ ./selfie -c selfie.c -m 1 -c any-cstar-file.c -y 1 "arguments for any-cstar-file.c"
你也可以用这两种方式生成RISC-U二进制文件,它们将是相同的:
$ ./selfie -c any-cstar-file.c -o any-cstar-file1.m
$ ./selfie -c selfie.c -m 1 -c any-cstar-file.c -o any-cstar-file2.m
$ diff -s any-cstar-file1.m any-cstar-file2.m
Files any-cstar-file1.m and any-cstar-file2.m are identical
这也可以在单次调用selfie
时完成:
$ ./selfie -c any-cstar-file.c -o any-cstar-file1.m -c selfie.c -m 1 -c any-cstar-file.c -o any-cstar-file2.m
$ diff -s any-cstar-file1.m any-cstar-file2.m
Files any-cstar-file1.m and any-cstar-file2.m are identical
然后可以按如下方式加载和执行生成的RISC-U二进制文件:
$ ./selfie -l any-cstar-file1.m -m 1 "arguments for any-cstar-file1.m"
链接
要从多个源文件编译和链接任何C*源代码,请使用:
$ ./selfie -c any-cstar-file1.c any-cstar-file2.c ... -m 1
例如,要将 selfie.c
的源代码作为库代码在任何 C* 源代码中使用,请执行:
$ ./selfie -c any-cstar-file.c selfie.c -m 1
请注意,编译器会忽略多重符号定义,并发出警告。
调试
Selfie 的控制台消息始终以当前运行的源文件或二进制文件的名称开头。Mipster 模拟器还会显示为其机器实例分配的内存量以及执行如何终止(退出代码)。
如前所述,selfie
和任何其他 C* 文件的 RISC-U 汇编代码可以通过以下方式生成:
$ ./selfie -c selfie.c -s selfie.s
如果汇编代码是由编译器生成的二进制文件生成的(而不是从文件加载的),汇编文件中会包含大致的源代码行号。
使用 -d
选项可以打印详细的调试信息,例如:
$ ./selfie -c selfie.c -d 1
同样,如果执行的二进制文件是由编译器生成的(而不是从文件加载的),调试信息中会包含大致的源代码行号。