将Quake 3翻译成锈病(2020)

2021-06-13 02:52:48

由Andrei Homescu& Stephen Crane& Miguel Saldivar Imgunant的耐热队在C2RUST上努力工作,这是一个迁移框架,将欺凌手机迁移到锈蚀。我们的目标是自动对翻译铁锈进行安全改进,并帮助程序员在我们不能的地方做同样的事情。首先,我们必须建立一个摇滚固体翻译,让人们陷入困境。关于小型CLI程序的测试最终变老,因此我们决定尝试将Quake 3转换为Rust。几天后,我们可能是第一个在锈病中玩Quake3的人!

在查看原始地震3源代码和各种叉子之后,我们在Ioquake3上解决了。它是一个Quake 3的社区叉,仍然保持在现代平台上。

$树 - 攻击-i missionpack -p" *。所以| * x86_64"。└─构建└──debug-linux-x86_64──baseq3│───cgamex86_64.so#client│ ├──qagamex86_64.so#游戏服务器│└──uix86_64.so#ui├──1-10qded.x86_64#dedicated服务器二进制├──ioquake3.x86_64#main二进制├──renderer_opengl1_x86_64.so#opengl1渲染器└──renderer_opengl2_x86_64 .so#OpenGL2渲染器

这些库,UI,客户端和服务器库可以作为Quake VM程序集或本机X86共享库构建。我们选择为我们的项目使用这些库的本机版本。将VM转换为RUDER和使用QVM版本的显着更简单,但我们希望彻底测试C2rust。

我们专注于UI,游戏,客户,OpenGL1渲染器和主要二进制我们的翻译。也可以转换OpenGL2渲染器,但我们选择跳过它,因为它可以大量使用构建系统在C源代码中嵌入为文字字符串。虽然我们可以添加自定义构建脚本支持在我们转换后将GLSL代码嵌入到生锈字符串中,但没有一个良好的自动方法来转换这些自动化的,临时文件1.我们刚才翻译了OpenGL1渲染器库并强制游戏使用它而不是默认渲染器。最后,我们决定跳过专用的服务器和任务包文件,因为他们不会难以翻译,但也没有必要的演示。

要保留Quake 3所使用的目录结构,无需更改其源代码,我们需要生成与本机构建完全相同的二进制文件,这意味着四个共享库和一个可执行文件.SINCE C2Rust生成货物构建文件,每个二进制需要它拥有相应的Cargo.Toml文件的锈帽。对于每个输出二进制文件生成一个箱子,它需要一副箱子以及它们的相应对象或源文件,以及用于生成每个二进制的链接器调用(用于确定其他细节等图书馆依赖项)。

但是,我们快速耗尽了C2rust拦截本机构建过程的方式一个限制:C2rust将编译数据库文件作为输入,其中包含在构建期间执行的编译命令列表。但是,此数据库仅包含编译命令,并且不是任何链接器调用。生成此数据库的最具工具具有此故意的限制,例如CMake_export_compile_Commands,Bear和CompiledB.to的CMake,唯一包含链接命令的工具是来自CodeChecker的构建记录器,我们没有使用,因为我们在编写自己的包装器之后才能学习(下面描述)。这意味着我们无法使用任何常用工具生成的compile_commands.json文件来转换多二进制C程序。

相反,我们编写了自己的编译器和链接包包装脚本,将所有编译器和链接器调用转储到数据库,然后将其转换为扩展的compile_commands.json。使用命令代替正常构建,如:

包装器生成一个充满JSON文件的目录,每个调用一个目录。第二个脚本将所有这些脚本聚合到包含编译和链接命令的新编译_commands.json文件中。然后,然后扩展C2才能从数据库中读取链接命令,以及每个链接二进制生成单独的箱子.Aditionally,C2Rust现在还读取每个二进制文件的库依赖性,并自动将它们添加到该箱的构建文件中。

作为一种生活质量改进,所有二进制文件都可以通过在Workspace.c2中使它们产生一个顶级工作空间Cargo.toml文件,因此我们可以使用单个货物构建命令构建项目Quake3-RS目录:

$树-11.├──载荷兰───cargo.toml├── - qagamex86_64──qagamex86_64├──renderer_opengl1_x86_64──锈 - 工具挂接

当我们第一次尝试构建翻译代码时,我们用Quake 3来击中了几个问题,击中了C2Rust无法处理的角壳体(正确或根本)。

在几个地方,原始源代码包含表达式,该表达式指向阵列的最后一个元素。这是C代码的简化示例:

C标准(参见例如C11,第6.5.6节)允许指向指向阵列结束的元素。但是,即使我们只占用元素的地址,RUDES禁止这一点。我们发现AAS_TRACECLIENDBBOX功能中此模式的示例。

Rust编译器还在G_TRYPUSHINGINGY中标记了类似但实际的错误示例,其中条件是>而不是> =。然后在条件后解除界限指针,这是一个实际的内存安全错误。

为了避免将来避免这个问题,我们修复了C2Rust Transpiler以使用指针算法来计算数组元素的地址而不是使用数组索引操作。使用此修复程序,使用此“元素地址越过阵列结束”模式的代码现在将正确转换和运行,无需修改。

我们启动了一个游戏来测试事物,并立即从Rust恐慌:

线程'主要'闹剧'索引超出界限:LEN为4但索引是4&#39 ;,Quake3-Client / SRC / CM_Polylib.rs:973:17

看看CM_Polylib.c,我们注意到它在以下结构中取消了P字段:

该结构中的P个字段是C99仍然接受的“灵活阵列成员”的PE99的非兼容版本,其使用C99语法(VEC3_T P [])识别灵活的阵列成员,并实现简单的启发式检测某些PRE-C99版本的这种模式(结构结束时的0和1大阵列;我们也发现了IOQuake3源代码中的一些。

试图在常规情况下自动修复此模式(尺寸为0或1的尺寸阵列)是非常困难的,因为我们必须区分常规阵列和灵活的任意尺寸的阵列成员.Instead,我们建议原始的c代码是手动修复 - 就像我们为Ioquake3做过的那样。

来自/usr/include/bits/select.h系统标题的另一个崩溃源是此C的内联汇编代码:

#定义__fd_zero(fdsp)\ do {\ int __d0,__d1; \ __asm___volatile__(" cld; rep;" __fd_zero_stos \:" = c"(__d0)," = d"(__d1)\:" A"(0)," 0"(sizeof(fd_set)\ / sizeof(__fd_mask)),\" 1"(& __ fds_bits(fdsp)[0])\ :"记忆"); \}虽然(0)

它定义了__fd_zero宏的内部版本。这个定义命中了gcc内联装配的稀有角色情况:用不同的大小绑定输入/输出操作数。" = d" (__d1)输出操作数将EDI寄存器绑定到__d1变量作为32位值,而#34; 1" (& __ fds_bits(fdsp)[0])将相同的寄存器绑定到fdsp-&gt的地址; fds_bits作为64位指针。 GCC和Clang通过使用64位寄存器RDI修复此不匹配,然后在分配到__d1之前致致突出其值,而rust默认为LLVM的语义,则将这种情况置于未定义。我们在发生调试构建时看到发生什么(但不是发布构建(表现正确)的是,这两个操作数都将被分配给EDI寄存器,导致指针在内联组合之前将指针截断为32位,这会导致崩溃。

由于Rustc通过很少的变化将Rust Inline汇编程序传递给LLVM,因此我们决定在C2Rust中修复此特殊情况.WE实现了一种新的C2Rust-ASM投递箱,该箱将上面修复上面的问题,使用特征和一些辅助功能将上面的问题修复上面的问题它会自动扩展并截断将绑定操作数的值截断到一个足够大的内部大小,以持有两个操作数。上面的代码正确转换为以下内容:

让mut __d0:c_int = 0;让mut __d1:c_int = 0; //引用第一个Operandlet的输出值Fresh5 =& mut __d0; //第一个绑定的操作数的内部存储器为nuckment6; //引用第二个操作数的输出值Fresh7 =& mut __d1; //第二个绑定的Outmandlet的内部存储Nuckle8; // Fresh_(:: std :: mem :: size_of ::< fd_set>()作为c_ulong).wrapping_div(:: std :: mem :: size_of ::< __ fd_mask> //作为c_ulong); //第二operandlet的输入值新鲜10 =& mut * fdset .__ fds_bits.as_mut_ptr ().offset(0)为* mut__fd_mask; asm!(" cld; rep; stosq":" = {cx}"(fresh6)," = { di}"(Fresh8):" {ax}"(0),//将输入操作数投入内部存储类型//,其中可选的零点或签名 - 扩展" 0"(ASMcast :: Cast_in(Fresh5,Fresh9))," 1"(ASMcast :: Cast_in(Fresh7,Fresh10)):"记忆":"挥发性& #34;); //施放操作数(类型是Infulre d)带TruncationAsmcast :: Cast_out(Fresh5,Fresh9,Fresh6); AsCascast :: Cast_out(Fresh7,Fresh10,Fresh8);

请注意,上面的代码不需要装配声明中的任何输入或输出值的代码,而依赖于RUST的类型推断,以解决这些类型(主要是新鲜的类型和上面的新鲜类型8)。

我们遇到的崩溃的最终来源是以下全局变量,存储SSE常量:

Rust目前支持结构类型上的对齐属性,但不是全局变量,即静态项目。我们正在研究在RUST或C2RUST中的一般情况下解决此问题,但已决定为IOQuake3手动修复此问题现在是一个短补丁文件。此修补程序文件替换SSEMASK的RUST等效项:

# ,255,255,0,0,0,0,]);

运行Cargo Build --Release发出二进制文件,但它们都使用IoQuake3二进制未识别的目录结构来排除目标/释放。我们写了一个脚本,它在当前目录中创建符号链接以复制正确的目录结构(包括包含游戏资产的.pk3文件的链接):

现在让我们跑游戏!我们需要通过+设置vm_game 0等,以便我们将这些模块加载为生锈共享库而不是QVM程序集,而CL_RENTERER使用OpenGL1渲染器。

$ ./ioquake3 + set sv_pure 0 + set vm_game 0 + set vm_cgame 0 + set vm_ui 0 + set cl_renderer" OpenGL1"

这是我们替代地震3的视频,加载游戏并播放一下:

您可以浏览存储库的转帐分支中的转帐源。我们还提供包含与预先应用的一些重构命令相同的源的重构分支。

如果您想尝试翻译Quake 3并自己运行它,请注意您需要拥有原始的Quake 3游戏资产或从Web下载演示资产。您还需要安装C2Rust(写作时所需的Rust夜间版本是2019-12-05夜间,但我们建议您查看最新的C2Rust存储库或箱子。

作为使用上述命令安装C2Rust的替代方案,您可以使用货物构建手动构建C2Rust。在任何一种情况下,仍然需要C2Rust存储库,因为它包含要转换IoQuake3所需的编译器包装脚本。

我们提供了一个自动转换C代码并应用SSemask修补程序的脚本。要使用它,请从IOQ3存储库的顶级运行以下命令:

此命令应生成包含生锈代码的Quake3-RS子目录,其中您可以随后运行Cargo Build - 释放和其余步骤。

随着我们继续开发C2Rust,我们很乐意听到你想看到的内容。在[电子邮件受保护]下给我们一条线,让我们知道!如果您有传统的C代码,您需要现代化和翻译,则此处的团队在此处都是帮助。我们可用于从一次性支持到全方位服务代码现代化的一次咨询和签订订婚。

感谢David Dubois来纠正我们对GLSL构建过程的理解。 ↩︎