magiclibc:在macOS上运行Linux程序

引言

oscamp中,有一个方向是将musl中的系统调用改为ArceOS内核中的函数调用,进而实现Linux ELF二进制在ArceOS的Unikernel上运行。当时我了解了这个libc替换方案的巨大潜力,于是近期我试着通过重定向glibc来实现在macOS上运行Linux ELF二进制的功能,并将其命名为magiclibc

函数、动态链接与系统调用

绝大多数计算机科学学生人生中第一个程序大概都是这样的:

1
2
3
4
5
6
#include <stdio.h>

int main() {
    printf("hello, world\n");
    return 0;
}

我们通过调用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的系统调用,也就是下图:

如果你使用纯汇编来实现这个库,那么这确实能够做到,但这并不利于开发效率。我使用Rustextern "C"来实现这个库,但最终编译出来显然遵循的是Darwin ABI,而不是Linux ABI。所以在进入实际函数之前,我们必须写一个汇编的“跳板”,我把这个“跳板”命名为bridge,这个跳板的作用是将Linux ABI的参数转换为Darwin ABI的参数,然后再调用实际的函数实现。实际架构如图:

对于固定参数函数而言,两个ABI还是高度相似的,而对于不定参数二者有显著差异。因此libmagic实际实现的是puts而不是printf。唯一有所不同的是在aarch64-apple-darwin平台上,x18寄存器被保留给平台寄存器而aarch64-unknown-linux-gnu 中没有保留,虽然这可以通过在编译时配置-ffixed-x18来解决,但是我更希望避免重编译。

为了方便解释,我们这里引入虚拟机中hostguest的概念,host是指运行在macOS上的程序,而guest是指运行在Linux上的程序。我们在进入guest前保存hostx18寄存器,然后在guest切换为host时保存guestx18寄存器并恢复hostx18寄存器,反之亦然。这看起来非常像函数调用中的栈帧切换,只不过我们一开始需要caller-saved后面又都是callee-saved

这里只展示部分核心代码,完整代码请查看magiclibc

puts函数的实现如下:

 1
 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 "C" fn puts(text: *const c_char) -> 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"\n")?;
        stdout.flush()
    });

    match result {
        Ok(Ok(())) => 0,
        Ok(Err(_)) | Err(_) => -1,
    }
}

bridge的实现如下:

 1
 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's
// current SP, then install that thread'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来加载。

紧接着到了核心的动态链接重定向部分。在Linux上通常通过PLT来实现动态链接,并通过PT_INTERP(例如/lib/ld-linux-aarch64.so.1)填充GOT表来实现函数调用的重定向。这里我们由运行时来负责PT_INTERP的职责,通过将对libc.so.6的函数入口重定向到libmagic.dylib对应的bridge函数入口来实现动态链接重定向。

在正式进入guest前,我们还需要伪造一端Linux在进入_start前的初始用户栈,它遵循Linux ABI的栈布局,包含argcargvenvpauxv等信息。

万事俱备后,我们只需要切换到伪造的Linux用户栈,进入guest_start即可。

更多

通过对libSystem的包装,magiclibc还实现了pthread等功能,代码已经开源在GitHub上。

结语

magiclibc展示了不通过虚拟机也不通过捕获系统调用在macOS上运行Linux ELF二进制的可能性。不过由于其泛用性上远不如捕获系统调用的方式,如果要做成一个完整的Linux兼容层,还需要更多的工作。

Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy