[{"content":"引言 在oscamp中，有一个方向是将musl中的系统调用改为ArceOS内核中的函数调用，进而实现Linux ELF二进制在ArceOS的Unikernel上运行。当时我了解了这个libc替换方案的巨大潜力，于是近期我试着通过重定向glibc来实现在macOS上运行Linux ELF二进制的功能，并将其命名为magiclibc。\n函数、动态链接与系统调用 绝大多数计算机科学学生人生中第一个程序大概都是这样的：\n1 2 3 4 5 6 #include \u0026lt;stdio.h\u0026gt; int main() { printf(\u0026#34;hello, world\\n\u0026#34;); return 0; } 我们通过调用printf函数来输出一段文本，这个函数是C标准库中的一个函数。如果你使用gcc来编译，那么这个标准库实现就是glibc，它是Linux系统中最常用的C标准库实现。printf函数实现最终会调用一个系统调用来将数据写入终端设备，在Linux上这个系统调用是write(2)。\n如果你学过操作系统，不然理解下图：二进制之间通过平台ABI来进行交互，而进程与内核之间通过系统调用来进行交互。\nflowchart LR A[printf] -- Linux ABI --\u003e B[glibc] B -- write(2) --\u003e C[Linux Kernel]libmagic：Mach-O格式的glibc兼容库 从上图我们不难想到一种可能，那就是我们可以将glibc替换为一个Mach-O格式的库，这个库实现了Linux ABI（AArch64上为aarch64-unknown-linux-gnu），并最终使用Darwin的系统调用，也就是下图：\nflowchart LR A[printf] -- Linux ABI --\u003e B[Mach-O glibc-compatible library] B -- write(2) --\u003e C[Darwin/XNU]如果你使用纯汇编来实现这个库，那么这确实能够做到，但这并不利于开发效率。我使用Rust的extern \u0026quot;C\u0026quot;来实现这个库，但最终编译出来显然遵循的是Darwin ABI，而不是Linux ABI。所以在进入实际函数之前，我们必须写一个汇编的“跳板”，我把这个“跳板”命名为bridge，这个跳板的作用是将Linux ABI的参数转换为Darwin ABI的参数，然后再调用实际的函数实现。实际架构如图：\nflowchart LR A[printf] -- Linux ABI --\u003e B[libmagic bridge] B -- Darwin ABI --\u003e C[libmagic implementation] C -- write(2) --\u003e D[Darwin/XNU]对于固定参数函数而言，两个ABI还是高度相似的，而对于不定参数二者有显著差异。因此libmagic实际实现的是puts而不是printf。唯一有所不同的是在aarch64-apple-darwin平台上，x18寄存器被保留给平台寄存器而aarch64-unknown-linux-gnu 中没有保留，虽然这可以通过在编译时配置-ffixed-x18来解决，但是我更希望避免重编译。\n为了方便解释，我们这里引入虚拟机中host和guest的概念，host是指运行在macOS上的程序，而guest是指运行在Linux上的程序。我们在进入guest前保存host的x18寄存器，然后在guest切换为host时保存guest的x18寄存器并恢复host的x18寄存器，反之亦然。这看起来非常像函数调用中的栈帧切换，只不过我们一开始需要caller-saved后面又都是callee-saved。\n这里只展示部分核心代码，完整代码请查看magiclibc\nputs函数的实现如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #[unsafe(no_mangle)] pub unsafe extern \u0026#34;C\u0026#34; fn puts(text: *const c_char) -\u0026gt; c_int { if text.is_null() { return -1; } let result = std::panic::catch_unwind(|| { // SAFETY: The ABI contract above is enforced by the trusted guest. let bytes = unsafe { CStr::from_ptr(text) }.to_bytes(); let stdout = io::stdout(); let mut stdout = stdout.lock(); stdout.write_all(bytes)?; stdout.write_all(b\u0026#34;\\n\u0026#34;)?; stdout.flush() }); match result { Ok(Ok(())) =\u0026gt; 0, Ok(Err(_)) | Err(_) =\u0026gt; -1, } } bridge的实现如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 .text .p2align 2 // Locate the active context whose registered stack contains the bridge\u0026#39;s // current SP, then install that thread\u0026#39;s Darwin x18. Arguments x0-x8 are // left untouched for the eventual host function. _magic_install_host_x18: adrp x9, _MAGIC_THREAD_CONTEXTS@PAGE add x9, x9, _MAGIC_THREAD_CONTEXTS@PAGEOFF mov x10, #64 1: ldar x11, [x9] cmp x11, #2 b.ne 2f ldr x12, [x9, #8] cmp sp, x12 b.lo 2f ldr x13, [x9, #16] cmp sp, x13 b.hs 2f ldr x18, [x9, #24] ret 2: add x9, x9, #32 subs x10, x10, #1 b.ne 1b brk #0x18 .globl _magic_bridge_puts _magic_bridge_puts: stp x18, x30, [sp, #-16]! bl _magic_install_host_x18 bl _puts ldp x18, x30, [sp], #16 ret ELF加载器与动态链接重定向 macOS并不能直接运行ELF，它只支持Mach-O。因此我们需要一个ELF Loader来完成中这件事。这里我使用elf_loader crate来实现，由于其在macOS上并不支持从文件系统加载ELF，因此我使用mmap将ELF文件映射到内存中，然后传递给elf_loader来加载。\n紧接着到了核心的动态链接重定向部分。在Linux上通常通过PLT来实现动态链接，并通过PT_INTERP（例如/lib/ld-linux-aarch64.so.1）填充GOT表来实现函数调用的重定向。这里我们由运行时来负责PT_INTERP的职责，通过将对libc.so.6的函数入口重定向到libmagic.dylib对应的bridge函数入口来实现动态链接重定向。\n在正式进入guest前，我们还需要伪造一段Linux在进入_start前的初始用户栈，它遵循Linux ABI的栈布局，包含argc、argv、envp、auxv等信息。\n万事俱备后，我们只需要切换到伪造的Linux用户栈，进入guest的_start即可。\n更多 通过对libSystem的包装，magiclibc还实现了pthread等功能，代码已经开源在GitHub上。\n结语 magiclibc展示了不通过虚拟机也不通过捕获系统调用在macOS上运行Linux ELF二进制的可能性。不过由于其泛用性上远不如捕获系统调用的方式，如果要做成一个完整的Linux兼容层，还需要更多的工作。\n","date":"2026-09-03T10:30:00+08:00","permalink":"/zh-cn/p/magiclibc/","title":"magiclibc：在macOS上运行Linux程序"}]