Linux - 系统调用(syscall)

说明

  • 基于riscv64 soc + linux_5.10.4平台,通过新增一个系统调用深入了解下系统调用实现原理。

简介

  • Linux 软件运行环境分为用户空间和内核空间,默认情况下,用户进程无法访问内核,既不能访问内核所在的内存空间,也不能调用内核中的函数。
  • 为了给应用层提供系统支持,Linux提供了一组系统调用接口,用户可以通过调用它们访问linux内核的数据和函数。
  • Linux系统调用实现原理是固定,不同平台(arm64,riscv)只是切换至内核态的汇编指令不同,大致原理如下:
  1. 程序将系统调用参数填充到对应的平台通用寄存器。
  2. 调用平台特定的汇编指令,触发同步异常,切换至内核态运行。
  3. 内核初始化时已设置异常向量表,应用层触发同步异常后,CPU会跳到异常向量表对应的异常处理执行(通常是一段平台相关的汇编代码)。
  4. 异常处理代码会检查系统调用号是否超出,未超出,再根据定义的系统调用表(sys_call_table)找到相应的系统调用函数入口地址,执行后,再通过汇编指令返回应用层。
  • 新增系统调用,实现步骤,如下:
  1. 修改系统调用表(syscall_table),新增一项。
  2. 系统调用声明。
  3. 系统调用实现。

修改系统调用表

  • 系统调用表(syscall_table)定义如下:
// file: arch/riscv/kernel/syscall_table.c
#undef __SYSCALL
#define __SYSCALL(nr, call)     [nr] = (call),

const void *sys_call_table[__NR_syscalls] = {
        [0 ... __NR_syscalls - 1] = sys_ni_syscall,
#include  //通过unistd.h导入实际定义
};
  • asm/unistd.h最终定义,如下:
//file: include/uapi/asm-generic/unistd.h 
...
#define __NR_openat2 437     //系统调用openat2 编号
__SYSCALL(__NR_openat2, sys_openat2) //系统调用openat2 syscall_table项定义
#define __NR_pidfd_getfd 438
__SYSCALL(__NR_pidfd_getfd, sys_pidfd_getfd)
#define __NR_faccessat2 439
__SYSCALL(__NR_faccessat2, sys_faccessat2)
#define __NR_process_madvise 440
__SYSCALL(__NR_process_madvise, sys_process_madvise)

#undef __NR_syscalls
#define __NR_syscalls 441 //系统调用表 项个数
...
  • 新增一项系统调用(mytest)
diff --git a/include/uapi/asm-generic/unistd.h b/include/uapi/asm-generic/unistd.h
index 15279e8d8..7df066dc5 100644
--- a/include/uapi/asm-generic/unistd.h
+++ b/include/uapi/asm-generic/unistd.h
@@ -860,8 +860,11 @@ __SYSCALL(__NR_faccessat2, sys_faccessat2)
 #define __NR_process_madvise 440
 __SYSCALL(__NR_process_madvise, sys_process_madvise)
 
+#define __NR_mytest 441
+__SYSCALL(__NR_mytest, sys_mytest)
+
 #undef __NR_syscalls
-#define __NR_syscalls 441
+#define __NR_syscalls 442

系统调用声明

  • 新增系统调用需要先声明,否则内核编译时会报错(找不到新增系统调用声明)。
//file: include/linux/syscalls.h
....
asmlinkage long sys_madvise(unsigned long start, size_t len, int behavior);
asmlinkage long sys_process_madvise(int pidfd, const struct iovec __user *vec,
                        size_t vlen, int behavior, unsigned int flags);
....
+asmlinkage long sys_mytest(int id); //新增系统调用声明

系统调用实现

  • 实现系统调用时,不能像实现普通函数一样,需要使用SYSCALL_DEFINE宏,如:系统调用madvise,定义时使用SYSCALL_DEFINE3宏,宏展开后就是sys_madvise。
//file: mm/madvise.c
SYSCALL_DEFINE3(madvise, unsigned long, start, size_t, len_in, int, behavior)
{                       
    return do_madvise(current->mm, start, len_in, behavior);
}

int do_madvise(struct mm_struct *mm, unsigned long start, size_t len_in, int behavior)
{        
    ....
    //实际功能实现
    ....
}
  • SYSCALL_DEFINE 宏定义
//file: include/linux/syscalls.h
#ifndef SYSCALL_DEFINE0         
#define SYSCALL_DEFINE0(sname)                                  \
        SYSCALL_METADATA(_##sname, 0);                          \
        asmlinkage long sys_##sname(void);                      \
        ALLOW_ERROR_INJECTION(sys_##sname, ERRNO);              \
        asmlinkage long sys_##sname(void)
#endif /* SYSCALL_DEFINE0 */

#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__)

#define __PROTECT(...) asmlinkage_protect(__VA_ARGS__)
  1. 系统调用最多支持6个参数,1个参数使用SYSCALL_DEFINE1,2个参数使用SYSCALL_DEFINE2,以此类推。
  • 新系统调用
// file: mm/madvise.c ,随便找了一个文件保存代码
SYSCALL_DEFINE1(mytest, int, id)
{
    return id; //测试将id返回
}

应用层测试

  • 编译并运行新内核后,可运行应用层程序验证。
int main(void) 
{
    int id = 0;

    id = syscall(441, 100);
    printf("result : %d\n", id);
    return 0;
}

~# ./mytest 
result : 100

标准C库

  • 程序中调用的syscall来自标准C库,根据源码可知:应用层系统调用接口是封装的syscall。
  • 当前使用的标准C库(musl)syscall源码如下:
//file: musl-1.2.1/arch/riscv64/syscall_arch.h
...
#define __asm_syscall(...) \
    __asm__ __volatile__ ("ecall\n\t" \
    : "=r"(a0) : __VA_ARGS__ : "memory"); \
    return a0; \

static inline long __syscall0(long n)
{
    register long a7 __asm__("a7") = n;
    register long a0 __asm__("a0");
    __asm_syscall("r"(a7))
}
...
static inline long __syscall6(long n, long a, long b, long c, long d, long e, long f)
{
    register long a7 __asm__("a7") = n;
    register long a0 __asm__("a0") = a;
    register long a1 __asm__("a1") = b;
    register long a2 __asm__("a2") = c;
    register long a3 __asm__("a3") = d;
    register long a4 __asm__("a4") = e;
    register long a5 __asm__("a5") = f;
    __asm_syscall("r"(a7), "0"(a0), "r"(a1), "r"(a2), "r"(a3), "r"(a4), "r"(a5))
}
  • 可知:
  1. riscv64最终使用汇编指令ecall,触发同步异常,切换至内核态执行。
  2. 使用通用寄存器a7 存储系统调用编号
  3. 和内核定义一致,syscall支持0 ~ 6个参数(__syscall0 ~ __syscall6 ),使用寄存器a0 ~ a5传递参数。
  • ARM64实现原理也是一样,不同的只是触发异常的指令(svc)以及通用寄存器的使用,如下:
#define __asm_syscall(...) do { \
    __asm__ __volatile__ ( "svc 0" \
    : "=r"(x0) : __VA_ARGS__ : "memory", "cc"); \
    return x0; \
    } while (0)

static inline long __syscall0(long n)
{
    register long x8 __asm__("x8") = n;
    register long x0 __asm__("x0");
    __asm_syscall("r"(x8));
}
...
static inline long __syscall6(long n, long a, long b, long c, long d, long e, long f)
{
    register long x8 __asm__("x8") = n;
    register long x0 __asm__("x0") = a;
    register long x1 __asm__("x1") = b;
    register long x2 __asm__("x2") = c;
    register long x3 __asm__("x3") = d;
    register long x4 __asm__("x4") = e;
    register long x5 __asm__("x5") = f;
    __asm_syscall("r"(x8), "0"(x0), "r"(x1), "r"(x2), "r"(x3), "r"(x4), "r"(x5));
}

总结

  1. 系统调用是安全的,执行时,应用层没有访问内核空间。
  2. 系统调用执行时,应用层暂停,切换至内核空间执行。
  3. 系统调用执行时,是通过平台相关的特定汇编指令触发同步异常,riscv64是使用ecall,aarch64是使用svc 0,Intel CPU由中断0x80实现。
  • CPU会跳转到对应的异常处理,源码如下:
//file: arch/riscv/kernel/entry.S
....
ENTRY(handle_exception) //对应的异常处理
        ....
check_syscall_nr:
        /* Check to make sure we don't jump to a bogus syscall number. */
        li t0, __NR_syscalls
        la s0, sys_ni_syscall
        /*
         * Syscall number held in a7.
         * If syscall number is above allowed value, redirect to ni_syscall.
         */
        bgeu a7, t0, 3f
#ifdef CONFIG_COMPAT
        REG_L s0, PT_STATUS(sp)
        srli s0, s0, SR_UXL_SHIFT
        andi s0, s0, (SR_UXL >> SR_UXL_SHIFT)
        li t0, (SR_UXL_32 >> SR_UXL_SHIFT)
        sub t0, s0, t0
        bnez t0, 1f

        /* Call compat_syscall */
        la s0, compat_sys_call_table
        j 2f
1:
#endif
        /* Call syscall */
        la s0, sys_call_table
2:
        slli t0, a7, RISCV_LGPTR
        add s0, s0, t0
        REG_L s0, 0(s0)
3:
        jalr s0

ret_from_syscall:
....
  1. 内核态调用对应的系统调用函数,执行完后,会退出内核态切换至用户态,如上 ret_from_syscall。
  • 此过程 aarch64平台是由eret汇编指令实现,和arm trustzone机制 bl31切换至非安全world(REE)以及切换至安全world(bl32)实现流程是一样的,riscv64 平台,具体指令暂不明。

你可能感兴趣的:(#,Linux,内核知识,linux)