系统调用

到目前为止,如果我们不调用系统库的函数,我们写出来的绝大部分的程序都是无状态的。也就是说,无论我们调用多少次这个程序,程序的结果都永远相同(当然也有例外,大家不妨想想有怎样的程序,不调用外部函数的情况下,每次调用的输出不同)。事实上,如果想要程序变成有状态的,包括读取用户输入、读写文件、发送网络请求等等,都需要内核的配合。

但是,我们在用户态的程序不能直接通过bl来调用内核的函数。我们在操作系统中提到,AArch64有不同的异常级别。一般来说,用户态程序的异常级别是EL0,内核处于EL1。低异常级别的程序是不能调用高异常级别的函数的。

为了调用内核的函数,我们需要使用特殊的指令,切换异常等级。这就是svc(Supervisor Call)指令。其使用方法类似于:

svc    #0x80

这个命令会向EL1生成一个异常,系统将根据后面跟着的数,这里就是0x80,来判断怎样处理这个异常。在macOS中,大部分的系统调用都是可以通过0x80这个数来调用。

操作系统内核提供的系统调用有非常多,比如说readwrite等。这里的0x80只是告诉内核,我发起的异常是为了调用系统调用。那么具体是哪个系统调用,则需要使用系统调用号。

系统调用号我们可以在xnu源码bsd/kern/syscalls.master文件中查看。例如:

3	AUE_NULL	ALL	{ user_ssize_t read(int fd, user_addr_t cbuf, user_size_t nbyte); }
4	AUE_NULL	ALL	{ user_ssize_t write(int fd, user_addr_t cbuf, user_size_t nbyte); }

就代表:read系统调用的系统调用号是3,write是4。

也就是说,我们在使用svc进行系统调用的时候,通过某些途径让内核知道我们的系统调用号,就可以执行相应的系统调用了。

但是,通过什么途径能让内核知道我们想要的系统调用号呢?不仅如此,正如上面的代码片段所显示的,大部分系统调用也都有参数,我们不能使用bl,只能使用svc,那又如何传参获取返回值呢?

这些其实也是一种ABI,但这种ABI并没有稳定,也就是说并没有什么官方文档中规定了这种ABI。只是目前采用了这种ABI,之后会不会变并没有给出保证。

macOS的xnu内核规定,系统调用号传入r16寄存器(位于xnu源码的osfmk/arm64/proc_reg.hARM64_SYSCALL_CODE_REG_NUM宏)。而参数传递则和普通函数的类似,传入对应的r0r7的寄存器即可。

因此,我们终于可以用C语言写一个Hello world了(源代码位于codes/13-hello-world.s):

    .text
    .globl  _main
    .p2align    2
_main:
    sub    sp, sp, #16
    stp    x29, x30, [sp]

    mov    w0, #0                       ; fd: STDOUT_FILENO
    adrp   x1, my_str@PAGE
    add    x1, x1, my_str@PAGEOFF       ; cbuf: "Hello world"
    mov    w2, #12                      ; nbyte: 12
    mov    w16, #4                      ; Syscall number: write
    svc    #0x80                        ; write(STDOUT_FILENO, "Hello world", 12);

    ldp    x29, x30, [sp]
    add    sp, sp, #16
    ret

    .data
my_str:
    .asciz    "Hello world"

我们刚才提到,系统调用的ABI并不稳定。并且,系统调用号事实上也没有保证是不变的。因此,我们如果像上面一样,写出的代码就不具有可移植性。事实上,libc会对绝大多数常用的系统调用做一个封装,我们也可以直接调用libc对应的函数。也就是说,上面的mov w16, #4svc #0x80两行,可以换成bl _write,其中_write是libc提供的函数。