引言
在oscamp中,有一个方向是将musl中的系统调用改为ArceOS内核中的函数调用,进而实现Linux ELF二进制在ArceOS的Unikernel上运行。当时我了解了这个libc替换方案的巨大潜力,于是近期我试着通过重定向glibc来实现在macOS上运行Linux ELF二进制的功能,并将其命名为magiclibc。
函数、动态链接与系统调用
绝大多数计算机科学学生人生中第一个程序大概都是这样的:
| |
我们通过调用printf函数来输出一段文本,这个函数是C标准库中的一个函数。如果你使用gcc来编译,那么这个标准库实现就是glibc,它是Linux系统中最常用的C标准库实现。printf函数实现最终会调用一个系统调用来将数据写入终端设备,在Linux上这个系统调用是write(2)。
如果你学过操作系统,不然理解下图:二进制之间通过平台ABI来进行交互,而进程与内核之间通过系统调用来进行交互。
libmagic:Mach-O格式的glibc兼容库
从上图我们不难想到一种可能,那就是我们可以将glibc替换为一个Mach-O格式的库,这个库实现了Linux ABI(AArch64上为aarch64-unknown-linux-gnu),并最终使用Darwin的系统调用,也就是下图:
如果你使用纯汇编来实现这个库,那么这确实能够做到,但这并不利于开发效率。我使用Rust的extern "C"来实现这个库,但最终编译出来显然遵循的是Darwin ABI,而不是Linux ABI。所以在进入实际函数之前,我们必须写一个汇编的“跳板”,我把这个“跳板”命名为bridge,这个跳板的作用是将Linux ABI的参数转换为Darwin ABI的参数,然后再调用实际的函数实现。实际架构如图:
对于固定参数函数而言,两个ABI还是高度相似的,而对于不定参数二者有显著差异。因此libmagic实际实现的是puts而不是printf。唯一有所不同的是在aarch64-apple-darwin平台上,x18寄存器被保留给平台寄存器而aarch64-unknown-linux-gnu 中没有保留,虽然这可以通过在编译时配置-ffixed-x18来解决,但是我更希望避免重编译。
为了方便解释,我们这里引入虚拟机中host和guest的概念,host是指运行在macOS上的程序,而guest是指运行在Linux上的程序。我们在进入guest前保存host的x18寄存器,然后在guest切换为host时保存guest的x18寄存器并恢复host的x18寄存器,反之亦然。这看起来非常像函数调用中的栈帧切换,只不过我们一开始需要caller-saved后面又都是callee-saved。
这里只展示部分核心代码,完整代码请查看magiclibc
puts函数的实现如下:
| |
bridge的实现如下:
| |
ELF加载器与动态链接重定向
macOS并不能直接运行ELF,它只支持Mach-O。因此我们需要一个ELF Loader来完成中这件事。这里我使用elf_loader crate来实现,由于其在macOS上并不支持从文件系统加载ELF,因此我使用mmap将ELF文件映射到内存中,然后传递给elf_loader来加载。
紧接着到了核心的动态链接重定向部分。在Linux上通常通过PLT来实现动态链接,并通过PT_INTERP(例如/lib/ld-linux-aarch64.so.1)填充GOT表来实现函数调用的重定向。这里我们由运行时来负责PT_INTERP的职责,通过将对libc.so.6的函数入口重定向到libmagic.dylib对应的bridge函数入口来实现动态链接重定向。
在正式进入guest前,我们还需要伪造一端Linux在进入_start前的初始用户栈,它遵循Linux ABI的栈布局,包含argc、argv、envp、auxv等信息。
万事俱备后,我们只需要切换到伪造的Linux用户栈,进入guest的_start即可。
更多
通过对libSystem的包装,magiclibc还实现了pthread等功能,代码已经开源在GitHub上。
结语
magiclibc展示了不通过虚拟机也不通过捕获系统调用在macOS上运行Linux ELF二进制的可能性。不过由于其泛用性上远不如捕获系统调用的方式,如果要做成一个完整的Linux兼容层,还需要更多的工作。