ARM64内核系统调用详解(基于kernel-4.9)

本文以ARM64为例,介绍如何添加系统调用,首先来介绍一些代码执行流程:

首先来看异常向量表的配置,内核在arch/arm64/kernel/entry.S汇编代码中设置了异常向量表。

/*
 * Exception vectors.
 */
    .pushsection ".entry.text", "ax"
    .align  11
ENTRY(vectors)
    kernel_ventry   1, sync_invalid         // Synchronous EL1t
    kernel_ventry   1, irq_invalid          // IRQ EL1t
    kernel_ventry   1, fiq_invalid          // FIQ EL1t
    kernel_ventry   1, error_invalid        // Error EL1t

    kernel_ventry   1, sync             // Synchronous EL1h
    kernel_ventry   1, irq              // IRQ EL1h
    kernel_ventry   1, fiq_invalid          // FIQ EL1h
    kernel_ventry   1, error_invalid        // Error EL1h

    kernel_ventry   0, sync             // Synchronous 64-bit EL0
    kernel_ventry   0, irq              // IRQ 64-bit EL0
    kernel_ventry   0, fiq_invalid          // FIQ 64-bit EL0
    kernel_ventry   0, error_invalid        // Error 64-bit EL0

#ifdef CONFIG_COMPAT
    kernel_ventry   0, sync_compat, 32      // Synchronous 32-bit EL0
    kernel_ventry   0, irq_compat, 32       // IRQ 32-bit EL0
    kernel_ventry   0, fiq_invalid_compat, 32   // FIQ 32-bit EL0
    kernel_ventry   0, error_invalid_compat, 32 // Error 32-bit EL0
#else
    kernel_ventry   0, sync_invalid, 32     // Synchronous 32-bit EL0
    kernel_ventry   0, irq_invalid, 32      // IRQ 32-bit EL0
    kernel_ventry   0, fiq_invalid, 32      // FIQ 32-bit EL0
    kernel_ventry   0, error_invalid, 32        // Error 32-bit EL0
#endif
END(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模式进行处理。

前面设置了异常向量表,我们来进一步查看SVC mode的处理。当系统调用时CPU会切换到SVC mode,并跳转到对应的地址去运行。

kernel中会配置两个SVC Handler,分别对应这SVC_32/SVC_64两种mode,32bit程序和64bit程序执行系统调用会跳转到两个不同的handler去执行。

内核在arch/arm64/kernel/entry.S汇编代码中设置了SVC异常entry。

64-bit运行模式解析

如下函数设置了64-bit状态下的异常向量表设置,其中红色部分是svc handler配置:

arch/arm64/kernel/entry.S:

    .align  6
el0_sync:
    kernel_entry 0
    mrs x25, esr_el1            // read the syndrome register
    lsr x24, x25, #ESR_ELx_EC_SHIFT // exception class
    cmp x24, #ESR_ELx_EC_SVC64      // SVC in 64-bit state
    b.eq    el0_svc
    cmp x24, #ESR_ELx_EC_DABT_LOW   // data abort in EL0
    b.eq    el0_da
    cmp x24, #ESR_ELx_EC_IABT_LOW   // instruction abort in EL0
    b.eq    el0_ia
    cmp x24, #ESR_ELx_EC_FP_ASIMD   // FP/ASIMD access
    b.eq    el0_fpsimd_acc
    cmp x24, #ESR_ELx_EC_FP_EXC64   // FP/ASIMD exception
    b.eq    el0_fpsimd_exc
    cmp x24, #ESR_ELx_EC_SYS64      // configurable trap
    b.eq    el0_sys
    cmp x24, #ESR_ELx_EC_SP_ALIGN   // stack alignment exception
    b.eq    el0_sp_pc
    cmp x24, #ESR_ELx_EC_PC_ALIGN   // pc alignment exception
    b.eq    el0_sp_pc
    cmp x24, #ESR_ELx_EC_UNKNOWN    // unknown exception in EL0
    b.eq    el0_undef
    cmp x24, #ESR_ELx_EC_BREAKPT_LOW    // debug exception in EL0
    b.ge    el0_dbg
    b   el0_inv

el0_svc的实现如下:

/*
 * SVC handler.
 */
    .align  6
el0_svc:
    adrp    stbl, sys_call_table        // load syscall table pointer
    uxtw    scno, w8            // syscall number in w8
    mov sc_nr, #__NR_syscalls
el0_svc_naked:                  // compat entry point
    stp x0, scno, [sp, #S_ORIG_X0]  // save the original x0 and syscall number
    enable_dbg_and_irq
    ct_user_exit 1
    ldr x16, [tsk, #TSK_TI_FLAGS]   // check for syscall hooks
    tst x16, #_TIF_SYSCALL_WORK
    b.ne    __sys_trace
    cmp     scno, sc_nr                     // check upper syscall limit
    b.hs    ni_sys
    ldr x16, [stbl, scno, lsl #3]   // address in the syscall table
    blr x16             // call sys_* routine
    b   ret_fast_syscall
ni_sys:
    mov x0, sp
    bl  do_ni_syscall
    b   ret_fast_syscall
ENDPROC(el0_svc)

可以看到它会去查找sys_call_table这个数组并找到对应的系统调用函数去执行,注意其中有一个关键函数do_ni_syscall,(no implement syscall),当系统调用遇到一些限制或者问题时会跳转到该函数去执行。

sys_call_table的定义在如下文件中:

arch/arm64/kernel/sys.c:
/*
 * The sys_call_table array must be 4K aligned to be accessible from
 * kernel/entry.S.
 */
void * const sys_call_table[__NR_syscalls] __aligned(4096) = {
    [0 ... __NR_syscalls - 1] = sys_ni_syscall,
#include <asm/unistd.h>
};

这个数组在创建时首先会把所有的数组成员设置为sys_ni_syscall,而后根据asm/unistd.h中的内容做进一步初始化。其实最终该头文件会把include/uapi/asm-generic//unistd.h包含进来,也就是这个头文件会是最终定义数组的地方。

......
__SYSCALL(__NR_epoll_wait, sys_epoll_wait)
#define __NR_ustat 1070
__SYSCALL(__NR_ustat, sys_ustat)
#define __NR_vfork 1071
__SYSCALL(__NR_vfork, sys_vfork)
#define __NR_oldwait4 1072
__SYSCALL(__NR_oldwait4, sys_wait4)
#define __NR_recv 1073
__SYSCALL(__NR_recv, sys_recv)
#define __NR_send 1074
__SYSCALL(__NR_send, sys_send)
#define __NR_bdflush 1075
__SYSCALL(__NR_bdflush, sys_bdflush)
#define __NR_umount 1076
__SYSCALL(__NR_umount, sys_oldumount)
#define __ARCH_WANT_SYS_OLDUMOUNT
#define __NR_uselib 1077
__SYSCALL(__NR_uselib, sys_uselib)
#define __NR__sysctl 1078
__SYSCALL(__NR__sysctl, sys_sysctl)
#define __NR_fork 1079
#ifdef CONFIG_MMU
__SYSCALL(__NR_fork, sys_fork)
#else
__SYSCALL(__NR_fork, sys_ni_syscall)
#endif /* CONFIG_MMU */
......

32-bit运行模式解析

如下函数设置了32-bit状态下的异常向量表设置,其中红色部分是svc handler配置:

arch/arm64/kernel/entry.S

 #ifdef CONFIG_COMPAT
     .align  6
 el0_sync_compat:
     kernel_entry 0, 32
     mrs x25, esr_el1            // read the syndrome register
     lsr x24, x25, #ESR_ELx_EC_SHIFT // exception class
     cmp x24, #ESR_ELx_EC_SVC32      // SVC in 32-bit state
     b.eq    el0_svc_compat
     cmp x24, #ESR_ELx_EC_DABT_LOW   // data abort in EL0
     b.eq    el0_da
     cmp x24, #ESR_ELx_EC_IABT_LOW   // instruction abort in EL0
     b.eq    el0_ia
     cmp x24, #ESR_ELx_EC_FP_ASIMD   // FP/ASIMD access
     b.eq    el0_fpsimd_acc
     cmp x24, #ESR_ELx_EC_FP_EXC32   // FP/ASIMD exception
     b.eq    el0_fpsimd_exc
     cmp x24, #ESR_ELx_EC_PC_ALIGN   // pc alignment exception
     b.eq    el0_sp_pc
     cmp x24, #ESR_ELx_EC_UNKNOWN    // unknown exception in EL0
     b.eq    el0_undef
     cmp x24, #ESR_ELx_EC_CP15_32    // CP15 MRC/MCR trap
     b.eq    el0_undef
     cmp x24, #ESR_ELx_EC_CP15_64    // CP15 MRRC/MCRR trap
     b.eq    el0_undef
     cmp x24, #ESR_ELx_EC_CP14_MR    // CP14 MRC/MCR trap
     b.eq    el0_undef
     cmp x24, #ESR_ELx_EC_CP14_LS    // CP14 LDC/STC trap
     b.eq    el0_undef
     cmp x24, #ESR_ELx_EC_CP14_64    // CP14 MRRC/MCRR trap
     b.eq    el0_undef
     cmp x24, #ESR_ELx_EC_BREAKPT_LOW    // debug exception in EL0
     b.ge    el0_dbg
     b   el0_inv
 el0_svc_compat:
     /*
      * AArch32 syscall handling
      */
     adrp    stbl, compat_sys_call_table // load compat syscall table pointer
     uxtw    scno, w7            // syscall number in w7 (r7)
     mov     sc_nr, #__NR_compat_syscalls
     b   el0_svc_naked

     .align  6
 el0_irq_compat:
     kernel_entry 0, 32
     b   el0_irq_naked
 #endif

el0_svc_compat的实现如下:

el0_svc_compat:
    /*
     * AArch32 syscall handling
     */
    adrp    stbl, compat_sys_call_table // load compat syscall table pointer
    uxtw    scno, w7            // syscall number in w7 (r7)
    mov     sc_nr, #__NR_compat_syscalls
    b   el0_svc_naked
    .align  6
el0_irq_compat:
    kernel_entry 0, 32
    b   el0_irq_naked

可以看到它会去查找compat_sys_call_table这个数组并找到对应的系统调用函数去执行,compat_sys_call_table的定义在如下文件中:

arch/arm64/kernel/sys32.c:
 /*
  * The sys_call_table array must be 4K aligned to be accessible from
  * kernel/entry.S.
  */
 void * const compat_sys_call_table[__NR_compat_syscalls] __aligned(4096) = {
     [0 ... __NR_compat_syscalls - 1] = sys_ni_syscall,
 #include <asm/unistd32.h>
 };

这个数组在创建时首先会把所有的数组成员设置为sys_ni_syscall,而后根据asm/unistd32.h中的内容做进一步初始化。其实最终该头文件会把arch/arm64/include/asm/unistd32.h包含进来,也就是这个头文件会是最终定义函数数组的地方。

arch/arm64/include/asm/unistd32.h:
......
 __SYSCALL(__NR_process_vm_writev, compat_sys_process_vm_writev)
 #define __NR_kcmp 378
 __SYSCALL(__NR_kcmp, sys_kcmp)
 #define __NR_finit_module 379
 __SYSCALL(__NR_finit_module, sys_finit_module)
 #define __NR_sched_setattr 380
 __SYSCALL(__NR_sched_setattr, sys_sched_setattr)
 #define __NR_sched_getattr 381
 __SYSCALL(__NR_sched_getattr, sys_sched_getattr)
 #define __NR_renameat2 382
 __SYSCALL(__NR_renameat2, sys_renameat2)
 #define __NR_seccomp 383
 __SYSCALL(__NR_seccomp, sys_seccomp)
 #define __NR_getrandom 384
 __SYSCALL(__NR_getrandom, sys_getrandom)
 #define __NR_memfd_create 385
 __SYSCALL(__NR_memfd_create, sys_memfd_create)
 #define __NR_bpf 386
 __SYSCALL(__NR_bpf, sys_bpf)
 #define __NR_execveat 387
 __SYSCALL(__NR_execveat, compat_sys_execveat)
 #define __NR_userfaultfd 388
 __SYSCALL(__NR_userfaultfd, sys_userfaultfd)
 #define __NR_membarrier 389
 __SYSCALL(__NR_membarrier, sys_membarrier)
 #define __NR_mlock2 390
 __SYSCALL(__NR_mlock2, sys_mlock2)
 #define __NR_copy_file_range 391
 __SYSCALL(__NR_copy_file_range, sys_copy_file_range)
 #define __NR_preadv2 392
 __SYSCALL(__NR_preadv2, compat_sys_preadv2)
 #define __NR_pwritev2 393
 __SYSCALL(__NR_pwritev2, compat_sys_pwritev2)
......

最后来看一下do_ni_syscall,内核中没有意义的系统调用号都会执行到该函数上面:

asmlinkage long do_ni_syscall(struct pt_regs *regs)
{
#ifdef CONFIG_COMPAT
    long ret;
    if (is_compat_task()) {
        ret = compat_arm_syscall(regs);
        if (ret != -ENOSYS)
            return ret;
    }
#endif
    if (show_unhandled_signals_ratelimited()) {
        pr_info("%s[%d]: syscall %d\n", current->comm
            task_pid_nr(current), (int)regs->syscallno);
        dump_instr("", regs);
        if (user_mode(regs))     
            __show_regs(regs);
    } 
    return sys_ni_syscall();
}

本文基于kernel-4.9版本,原创文章,转载请标注。

猜你喜欢

转载自blog.csdn.net/rikeyone/article/details/79919019