头图

Linux之系统调用

这里我们只讨论:

硬件: Arm64

系统: Linux系统 (Kernel-5.15-rc1)

高级语言: C (glibc-2.34)

模式: 64位 (即未定义CONFIG_COMPAT)

2、什么是系统调用

Linux系统分为内核态和用户态,两者是相互隔离的。为了防止各种应用程序可能对系统资源的破坏,用户态的应用程序是没有权限直接去访问系统资源的,当需要访问时,就需要通过系统调用。

系统调用是内核提供给用户态应用程序的一系列统一接口,标准库或API在系统调用的基础上做了进一步抽象和封装。用户态的应用程序可以直接进行系统调用,也可以通过标准库或API来调用

一个系统调用有很多个步骤,其中一个很重要的就是用户态和内核态相互切换,包括CPU模式的切换, 内核栈、用户栈的保护与处理等

大致的流程为:

-----------------------------------------
                |
    用户态       |           内核态 
                |
标准库或API -> 模式切换 -> 调用准备
                |               \    
                |                 ->  处理
                |                 <-  函数
                |               /
标准库或API <- 模式切换 <- 调用善后   
                |
-----------------------------------------

下面我们就分别讨论用户态、内核态下的一些关键处理

3、用户态的处理

那么如何陷入内核态呢?主要是通过同步异常来实现。ARM64专门定义了svc指令,用于进入同步异常,也就是说,一旦执行了svc指令,cpu立即跳转到同步异常入口地址处,从这个地址进入内核态

下面已glic里面的系统调用为例,简单看看过程:

ARM64相关的代码主要在:sysdeps/unix/sysv/linux/aarch64

比如我们常用的glibc库函数ioctl(), 在arm64下,glibc的实现:

ENTRY(__ioctl)
    mov    x8, #__NR_ioctl
    sxtw    x0, w0
    svc    #0x0
    cmn    x0, #4095
    b.cs    .Lsyscall_error
    ret
PSEUDO_END (__ioctl)

其中#__NR_ioctl就对应的ioctl的系统调用号,其定义是在sysdeps/unix/sysv/linux/aarch64/arch-syscall.h,如下:

...

#define __NR_io_uring_setup 425
#define __NR_ioctl 29   //////
#define __NR_ioprio_get 31

...

这个系统调用号(29)就是上层标准库(API)与内核联系的桥梁,和内核中的定义是对应的(arm64: include/uapi/asm-generic/unistd.h):

...

/* fs/ioctl.c */
#define __NR_ioctl 29
__SC_COMP(__NR_ioctl, sys_ioctl, compat_sys_ioctl)

...

所以相关用户态的基本流程大致为:

1.将系统调用号存放在x8寄存器中

2.执行svc指令,陷入异常,并且从el0切换到el1

4、内核态的处理

当用户态进入同步异常, 便会跳转到同步异常入口地址,从而触发内核相应的处理动作。

在内核中,arm64对应的异常向量表为(arch/arm64/kernel/entry.S):

/*
 * Exception vectors.
 */
    .pushsection ".entry.text", "ax"

    .align    11
SYM_CODE_START(vectors)
    kernel_ventry    1, t, 64, sync        // Synchronous EL1t
    kernel_ventry    1, t, 64, irq        // IRQ EL1t
    kernel_ventry    1, t, 64, fiq        // FIQ EL1h
    kernel_ventry    1, t, 64, error        // Error EL1t

    kernel_ventry    1, h, 64, sync        // Synchronous EL1h
    kernel_ventry    1, h, 64, irq        // IRQ EL1h
    kernel_ventry    1, h, 64, fiq        // FIQ EL1h
    kernel_ventry    1, h, 64, error        // Error EL1h

    kernel_ventry    0, t, 64, sync        // Synchronous 64-bit EL0
    kernel_ventry    0, t, 64, irq        // IRQ 64-bit EL0
    kernel_ventry    0, t, 64, fiq        // FIQ 64-bit EL0
    kernel_ventry    0, t, 64, error        // Error 64-bit EL0

    kernel_ventry    0, t, 32, sync        // Synchronous 32-bit EL0
    kernel_ventry    0, t, 32, irq        // IRQ 32-bit EL0
    kernel_ventry    0, t, 32, fiq        // FIQ 32-bit EL0
    kernel_ventry    0, t, 32, error        // Error 32-bit EL0
SYM_CODE_END(vectors)

SYM_CODE_START 其实就是将其后面()里面的字符展开而已,并在这个展开之前加上一些属性(比如对齐规则),展开后就相当于”vectors:”,表示定义vectors函数,“:”后面就是vectors的具体实现.

设置不同mode下的异常向量表,异常可以分为4组,每组异常有4个,所以这里一共会设置16个entry。4组异常分别对应4种情况下发生异常时的处理。上面的4组,按照顺序分别对应如下4中情况:

(1)运行级别不发生切换,从ELx变化到ELx,使用SP_EL0,这种情况在Linux kernel都是不处理的,使用invalid entry。

(2)运行级别不发生切换,从ELx变化到ELx,使用SP_ELx。这种情况下在Linux中比较常见。

(3)异常需要进行级别切换来进行处理,并且使用aarch64模式处理,比如64位用户态程序发生系统调用,CPU会从EL0切换到EL1,并且使用aarch64模式处理异常。

(4)异常需要进行级别切换来进行处理,并且使用aarch32模式处理。比如32位用户态程序发生系统调用,CPU会从EL0切换到EL1,并且使用aarch32模式进行处理。

这里我们只讨论64位模式,所以系统调用是第3种情况

继续往下看,

展开kernel_ventry, kernel_ventry(arch/arm64/kernel/entry.S)是一个宏,通过.macro和.endm组合定义

.macro kernel_ventry, el:req, ht:req, regsize:req, label:req

...

b    el\el\ht\()_\regsize\()_\label
.endm

里面会跳转到el\el\ht()_\regsize()_\label:

SYM_CODE_START_LOCAL(el\el\ht\()_\regsize\()_\label)
    kernel_entry \el, \regsize
    mov    x0, sp
    bl    el\el\ht\()_\regsize\()_\label\()_handler
    .if \el == 0
    b    ret_to_user
    .else
    b    ret_to_kernel
    .endif
SYM_CODE_END(el\el\ht\()_\regsize\()_\label)

其中便会调用对应的el\el\ht()_\regsize()_\label()_handler函数

通过解析,sync的处理对应的就是el0t_64_sync_handler()函数,跟踪代码,该函数的处理流程:

el0t_64_sync_handler() [arch/arm64/kernel/entry-common.c]
    -> el0_svc() 
        -> do_el0_svc() [arch/arm64/kernel/syscall.c]
            -> el0_svc_common()
                -> invoke_syscall() 
                    -> __invoke_syscall()

其中最主要的流程在el0_svc()函数:

static void noinstr el0_svc(struct pt_regs *regs)
{
    enter_from_user_mode(regs);
    cortex_a76_erratum_1463225_svc_handler();
    do_el0_svc(regs);
    exit_to_user_mode(regs);
}

最终会调用到invoke_syscall(), 该函数会根据传入的系统调用号, 在sys_call_table中找到对应的系统调用函数, 并执行

static void invoke_syscall(struct pt_regs *regs, unsigned int scno,
               unsigned int sc_nr,
               const syscall_fn_t syscall_table[])
{
    long ret;

    add_random_kstack_offset();

    if (scno < sc_nr) {
        syscall_fn_t syscall_fn;
        syscall_fn = syscall_table[array_index_nospec(scno, sc_nr)];
        ret = __invoke_syscall(regs, syscall_fn);
    } else {
        ret = do_ni_syscall(regs, scno);
    }

    syscall_set_return_value(current, regs, 0, ret);

    /*
     * Ultimately, this value will get limited by KSTACK_OFFSET_MAX(),
     * but not enough for arm64 stack utilization comfort. To keep
     * reasonable stack head room, reduce the maximum offset to 9 bits.
     *
     * The actual entropy will be further reduced by the compiler when
     * applying stack alignment constraints: the AAPCS mandates a
     * 16-byte (i.e. 4-bit) aligned SP at function boundaries.
     *
     * The resulting 5 bits of entropy is seen in SP[8:4].
     */
    choose_random_kstack_offset(get_random_int() & 0x1FF);
}

4.1、sys_call_table

sys_call_table的定义:

/// arch/arm64/kernel/sys.c

asmlinkage long __arm64_sys_ni_syscall(const struct pt_regs *__unused)
{
    return sys_ni_syscall();
}

/*
 * Wrappers to pass the pt_regs argument.
 */
#define __arm64_sys_personality        __arm64_sys_arm64_personality

#undef __SYSCALL
#define __SYSCALL(nr, sym)    asmlinkage long __arm64_##sym(const struct pt_regs *);
#include <asm/unistd.h>

#undef __SYSCALL
#define __SYSCALL(nr, sym)    [nr] = __arm64_##sym,

const syscall_fn_t sys_call_table[__NR_syscalls] = {
    [0 ... __NR_syscalls - 1] = __arm64_sys_ni_syscall,
#include <asm/unistd.h>
};

首先会将sys_call_table都初始化为sys_ni_syscall(),这里使用了GCC的扩展语法:指定初始化

sys_ni_syscall()为一个空函数,未做任何操作:

/// kernel/sys_ni.c

asmlinkage long sys_ni_syscall(void)
{
    return -ENOSYS;
}

然后包含asm/unistd.h的进行逐项初始化,asm/unistd.h最终会包含到uapi/asm-generic/unistd.h头文件:

...

#ifdef __SYSCALL_COMPAT
#define __SC_COMP(_nr, _sys, _comp) __SYSCALL(_nr, _comp)
#define __SC_COMP_3264(_nr, _32, _64, _comp) __SYSCALL(_nr, _comp)
#else
#define __SC_COMP(_nr, _sys, _comp) __SYSCALL(_nr, _sys)
#define __SC_COMP_3264(_nr, _32, _64, _comp) __SC_3264(_nr, _32, _64)
#endif

#define __NR_io_setup 0
__SC_COMP(__NR_io_setup, sys_io_setup, compat_sys_io_setup)
#define __NR_io_destroy 1
__SYSCALL(__NR_io_destroy, sys_io_destroy)
#define __NR_io_submit 2
__SC_COMP(__NR_io_submit, sys_io_submit, compat_sys_io_submit)
#define __NR_io_cancel 3
__SYSCALL(__NR_io_cancel, sys_io_cancel)

...

4.2 SYSCALL_DEFINEx

内核中具体的系统调用实现使用SYSCALL_DEFINEx来定义, 其中x代表传入参数的个数,

SYSCALL_DEFINEx相关代码:

/// include/linux/syscalls.h

#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)

#define SYSCALL_DEFINE_MAXARGS    6

#define SYSCALL_DEFINEx(x, sname, ...)                \
    SYSCALL_METADATA(sname, x, __VA_ARGS__)            \
    __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)

而对于ARM64,__SYSCALL_DEFINEx的定义为:

/// arch/arm64/include/asm/syscall_wrapper.h

#define __SYSCALL_DEFINEx(x, name, ...)                        \
    asmlinkage long __arm64_sys##name(const struct pt_regs *regs);        \
    ALLOW_ERROR_INJECTION(__arm64_sys##name, ERRNO);            \
    static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__));        \
    static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));    \
    asmlinkage long __arm64_sys##name(const struct pt_regs *regs)        \
    {                                    \
        return __se_sys##name(SC_ARM64_REGS_TO_ARGS(x,__VA_ARGS__));    \
    }                                    \
    static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__))        \
    {                                    \
        long ret = __do_sys##name(__MAP(x,__SC_CAST,__VA_ARGS__));    \
        __MAP(x,__SC_TEST,__VA_ARGS__);                    \
        __PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__));        \
        return ret;                            \
    }                                    \
    static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))

由上面可以看出,SYSCALL_DEFINEx来定义的函数就和sys_call_table中由__SYSCALL确定的函数对应了,即__arm64_sys##name

例如ioctl()内核态的实现为:

SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)
{
    struct fd f = fdget(fd);
    int error;

    if (!f.file)
        return -EBADF;

    error = security_file_ioctl(f.file, cmd, arg);
    if (error)
        goto out;

    error = do_vfs_ioctl(f.file, fd, cmd, arg);
    if (error == -ENOIOCTLCMD)
        error = vfs_ioctl(f.file, cmd, arg);

out:
    fdput(f);
    return error;
}

相见
6 声望4 粉丝