系统调用
这篇文章介绍 linux 操作系统中的系统调用。有一个新增系统调用的实验,实验下来还是非常有趣的。
系统调用是操作系统为应用程序提供的一种接口,使得应用程序可以利用这个接口请求操作系统为其提供服务。在现代操作系统中,内核空间和用户空间是分开的。内核空间可以访问硬件资源和所有内存,而用户空间的权限则较低,不能直接访问硬件资源和内核内存。这种设计可以保护硬件资源和内核内存不被恶意或者错误的用户程序破坏。
内核空间和用户空间的区分是为了保护系统的安全,以及抽象管理。
“保护系统”体现在各种特权指令只能在内核空间执行。
“抽象管理”体现在进程管理、内存管理。当然这些管理也保证了隔离和安全性。
操作系统依赖于硬件设计,操作系统的成熟设计也是会“反哺”硬件体系设计的。
系统调用步骤
在 ARM 上,执行系统调用会大体经历以下步骤:
1. 调用系统调用指令。ARM 上是 SVC #0 指令。该指令会触发一个同步异常。
2. 触发异常处理。触发同步异常后,会根据异常向量表调用对应的异常处理函数。
3. 分派系统调用。同步异常处理函数中会根据发生的异常类型分派具体的处理函数,此处为 SVC 异常。
4. 执行系统调用。系统调用处理函数中会通过寄存器中的值获取系统调用号,然后通过查找系统调用表来找到对应的系统调用函数,并执行。
5. 返回用户空间:系统调用执行完毕后,恢复上下文环境并返回到用户空间,系统调用结束。
下面我们在 ARM Linux 源码中,找寻一下以上步骤的关键点信息。
异常向量表
异常向量表在 arch/arm64/kernel/entry.S 中定义。SVC 指令对应的异常处理函数为 el0_sync。
- 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 // 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 // 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_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)
el0_sync 也同样定义在 arch/arm64/kernel/entry.S 中。
异常发生后,异常的具体信息会被存放在一个叫做 ESR_ELx 的寄存器中。
在 ESR_ELx 寄存器的值中,有一部分位被用来表示异常的类别,即“异常类”。这部分位在 ESR_ELx 寄存器中的偏移,即是源码中指定的 #ESR_ELx_EC_SHIFT。
我们比较“异常类”的值,如果为 #ESR_ELx_EC_SVC64,即为 ARM64 中的 SVC 异常。会跳转到 el0_svc 处理程序。
- 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
系统调用表
el0_svc 会调用 el0_svc_handler,el0_svc_handler 在 arch/arm64/kernel/syscall.c 中定义。
从源码中我们可以看到,SVC 处理函数会从 regs->regs[8] 中获取系统调用号,并根据 sys_call_table 系统调用表查找系统调用并执行。
- asmlinkage void el0_svc_handler(struct pt_regs* regs)
- {
- sve_user_discard();
- el0_svc_common(regs, regs->regs[8], __NR_syscalls, sys_call_table);
- }
系统调用表 sys_call_table 定义在 arch/arm64/kernel/sys.c 中。如源码所示,第一次包含 asm/unistd.h,会声明系统函数;第二次包含会声明系统调用表 sys_call_table 中的各个表项。
- #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] = (syscall_fn_t)__arm64_##sym,
- const syscall_fn_t sys_call_table[__NR_syscalls] = {
- [0 ... __NR_syscalls - 1] = (syscall_fn_t)sys_ni_syscall,
- #include <asm/unistd.h>
linux 对宏的操作太具技巧性了。
以上例子中,#undef 和 #define 被用来改变 __SYSCALL 宏的定义,然后 #include <asm/unistd.h> 被用来展开这个新的宏定义。
头文件 arch/arm64/include/asm/unistd.h 会引用到 arch/arm64/include/uapi/asm/unistd.h,最终引用到 include/uapi/asm-generic/unistd.h。
我们看其中对 __SYSCALL 宏的使用。以 read 系统调用为例,__NR_read 63 是它的系统调用号,结合 sys_read 用于 __SYSCALL 宏的展开。
- /* fs/readdir.c */
- #define __NR_getdents64 61
- __SYSCALL(__NR_getdents64, sys_getdents64)
- /* fs/read_write.c */
- #define __NR3264_lseek 62
- __SC_3264(__NR3264_lseek, sys_llseek, sys_lseek)
- #define __NR_read 63
- __SYSCALL(__NR_read, sys_read)
- #define __NR_write 64
- __SYSCALL(__NR_write, sys_write)
调用函数
还是以 read 系统调用为例,从前面的内容我们知道,最终 read 系统调用对应的处理函数为 __arm64_sys_read。__arm64_sys_read 通过 SYSCALL_DEFINE3 宏定义在 fs/read_write.c 中。
- SYSCALL_DEFINE3(read, unsigned int, fd, char __user*, buf, size_t, count)
- {
- return ksys_read(fd, buf, count);
- }
SYSCALL_DEFINE3 宏定义在 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__)
SYSCALL_DEFINE3 宏展开的实际内容非常繁琐,我们直接查看编译器最终展开的内容。以下内容我对展开内容做了格式规范化,便于查看。可以看到上部分是各种元数据的定义,下半部分是系统调用函数函数。此处的 __arm64_sys_read 会调用 __se_sys_read,然后调用 __do_sys_read,最终调用到 ksys_read。
这么多函数嵌套,应该是出于分层设计的考虑。可以看到 __se_ 函数是专门做参数类型检查的。
- static const char* types__read[] = { "unsigned int", "char *", "size_t" };
- static const char* args__read[] = { "fd", "buf", "count" };
- static struct syscall_metadata __syscall_meta__read;
- static struct trace_event_call __attribute__((__used__)) event_enter__read = {
- .class = &event_class_syscall_enter,
- {.name = "sys_enter""_read", },
- .event.funcs = &enter_syscall_print_funcs,
- .data = (void*)&__syscall_meta__read,
- .flags = TRACE_EVENT_FL_CAP_ANY,
- };
- static struct trace_event_call __attribute__((__used__)) __attribute__((section("_ftrace_events")))* __event_enter__read = &event_enter__read;;
- static struct syscall_metadata __syscall_meta__read;
- static struct trace_event_call __attribute__((__used__)) event_exit__read = {
- .class = &event_class_syscall_exit,
- {.name = "sys_exit""_read", },
- .event.funcs = &exit_syscall_print_funcs,
- .data = (void*)&__syscall_meta__read,
- .flags = TRACE_EVENT_FL_CAP_ANY,
- };
- static struct trace_event_call __attribute__((__used__)) __attribute__((section("_ftrace_events")))* __event_exit__read = &event_exit__read;;
- static struct syscall_metadata __attribute__((__used__)) __syscall_meta__read = {
- .name = "sys""_read",
- .syscall_nr = -1,
- .nb_args = 3,
- .types = 3 ? types__read : ((void*)0),
- .args = 3 ? args__read : ((void*)0),
- .enter_event = &event_enter__read,
- .exit_event = &event_exit__read,
- .enter_fields = { &(__syscall_meta__read.enter_fields), &(__syscall_meta__read.enter_fields) },
- };
- static struct syscall_metadata __attribute__((__used__)) __attribute__((section("__syscalls_metadata")))* __p_syscall_meta__read = &__syscall_meta__read;
- long __arm64_sys_read(const struct pt_regs* regs);
- static long __se_sys_read(
- __typeof(
- __builtin_choose_expr(
- (__builtin_types_compatible_p(typeof((unsigned int)0), typeof(0LL)) || __builtin_types_compatible_p(typeof((unsigned int)0), typeof(0ULL))),
- 0LL, 0L
- )
- ) fd,
- __typeof(
- __builtin_choose_expr(
- (__builtin_types_compatible_p(typeof((char*)0), typeof(0LL)) || __builtin_types_compatible_p(typeof((char*)0), typeof(0ULL))),
- 0LL, 0L
- )
- ) buf,
- __typeof(
- __builtin_choose_expr(
- (__builtin_types_compatible_p(typeof((size_t)0), typeof(0LL)) || __builtin_types_compatible_p(typeof((size_t)0), typeof(0ULL))),
- 0LL, 0L
- )
- ) count
- );
- static inline __attribute__((__always_inline__)) __attribute__((__gnu_inline__)) __attribute__((__unused__)) __attribute__((patchable_function_entry(0))) long __do_sys_read(unsigned int fd, char* buf, size_t count);
- long __arm64_sys_read(const struct pt_regs* regs) {
- return __se_sys_read(regs->regs[0], regs->regs[1], regs->regs[2]);
- }
- static long __se_sys_read(
- __typeof(
- __builtin_choose_expr(
- (__builtin_types_compatible_p(typeof((unsigned int)0), typeof(0LL)) || __builtin_types_compatible_p(typeof((unsigned int)0), typeof(0ULL))),
- 0LL, 0L
- )
- ) fd,
- __typeof(
- __builtin_choose_expr(
- (__builtin_types_compatible_p(typeof((char*)0), typeof(0LL)) || __builtin_types_compatible_p(typeof((char*)0), typeof(0ULL))),
- 0LL, 0L
- )
- ) buf,
- __typeof(
- __builtin_choose_expr(
- (__builtin_types_compatible_p(typeof((size_t)0), typeof(0LL)) || __builtin_types_compatible_p(typeof((size_t)0), typeof(0ULL))),
- 0LL, 0L
- )
- ) count
- ) {
- long ret = __do_sys_read((unsigned int)fd, (char*)buf, (size_t)count);
- (void)(
- sizeof(struct {
- int:(-!!(!(__builtin_types_compatible_p(typeof((unsigned int)0), typeof(0LL)) || __builtin_types_compatible_p(typeof((unsigned int)0), typeof(0ULL))) && sizeof(unsigned int) > sizeof(long)));
- }),
- sizeof(struct {
- int:(-!!(!(__builtin_types_compatible_p(typeof((char*)0), typeof(0LL)) || __builtin_types_compatible_p(typeof((char*)0), typeof(0ULL))) && sizeof(char*) > sizeof(long)));
- }),
- sizeof(struct {
- int:(-!!(!(__builtin_types_compatible_p(typeof((size_t)0), typeof(0LL)) || __builtin_types_compatible_p(typeof((size_t)0), typeof(0ULL))) && sizeof(size_t) > sizeof(long)));
- })
- );
- do {} while (0);
- return ret;
- }
- static inline __attribute__((__always_inline__)) __attribute__((__gnu_inline__)) __attribute__((__unused__)) __attribute__((patchable_function_entry(0))) long __do_sys_read(unsigned int fd, char* buf, size_t count)
- {
- return ksys_read(fd, buf, count);
- }
非常实用的 tips:像此处执行 make fs/read_write.i 可以直接得到展开内容。
实验
通过上节内容,我们已经对 linux 系统调用的流程有了基本的认识。现在我们在系统中添加一个系统调用,以获取当前进程的 PID 和 UID。
首先我们在 include/uapi/asm-generic/unistd.h 中,在系统编号的最后,添加我们的系统调用。如代码清单 1 所示,这边将新的系统调用命名为 getpuid。添加之后,这个调用声明就添加到系统调用表中了。
- #define __NR_getpuid 295
- __SYSCALL(__NR_getpuid, sys_getpuid)
- #undef __NR_syscalls
- #define __NR_syscalls 296
有了系统调用的声明,我们将系统调用的实现放在 arch/arm64/kernel/sys.c 中。如代码清单 2 所示,因为需要两个参数,所以使用 SYSCALL_DEFINE2 宏来定义实现。
- SYSCALL_DEFINE2(getpuid, pid_t* __user, pid, uid_t* __user, uid)
- {
- pid_t tmp_pid;
- uid_t tmp_uid;
- if (pid == NULL && uid == NULL)
- return -EINVAL;
- printk("%s: pid=%p, uid=%p\n", __func__, pid, uid);
- if (pid != NULL)
- {
- tmp_pid = task_tgid_vnr(current);
- if (put_user(tmp_pid, pid))
- return -EFAULT;
- printk("%s: pid=%d\n", __func__, tmp_pid);
- }
- if (uid != NULL)
- {
- tmp_uid = from_kuid_munged(current_user_ns(), current_uid());
- if (put_user(tmp_uid, uid))
- return -EFAULT;
- printk("%s: uid=%d\n", __func__, tmp_uid);
- }
- return 0;
- }
以上就完成了系统调用的添加。我们需要重新编译 linux 源码。
编译并运行新系统之后,我们编写测试程序。如代码清单 3 所示,核心是使用 syscall 标准库函数。
- #include <stdio.h>
- #include <stdlib.h>
- #include <unistd.h>
- #include <errno.h>
- int main()
- {
- long pid, uid;
- int ret;
- ret = (int)syscall(295, &pid, &uid);
- if (ret != 0)
- {
- printf("call getpuid failed\n");
- return 1;
- }
- printf("call getpuid success, return pid = %d, uid = %d\n", pid, uid);
- while (1)
- sleep(1);
- return 0;
- }
我们可以使用 ps -ef 命令来核对 PID 和 UID 是否返回正确。
小插曲
书上在内核中的操作是直接解引用赋值传入的用户态变量地址。不知道为什么书作者运行无误,自己实验下来遇到以下内核奔溃。
- [ 91.545580] __do_sys_getpuid: pid=(____ptrval____), uid=(____ptrval____)
- [ 91.549733] Unable to handle kernel access to user memory outside uaccess routines at virtual address 0000ffffed6d2530
- [ 91.550291] Mem abort info:
- [ 91.550384] ESR = 0x9600004f
- [ 91.550541] Exception class = DABT (current EL), IL = 32 bits
- [ 91.550725] SET = 0, FnV = 0
- [ 91.550814] EA = 0, S1PTW = 0
- [ 91.550995] Data abort info:
- [ 91.551088] ISV = 0, ISS = 0x0000004f
- [ 91.562871] CM = 0, WnR = 1
- [ 91.564836] user pgtable: 4k pages, 48-bit VAs, pgdp = (____ptrval____)
- [ 91.566657] [0000ffffed6d2530] pgd=0000000064727003, pud=000000006440e003, pmd=0000000066a7c003, pte=00e80000609ecf53
- [ 91.604308] Internal error: Oops: 9600004f [#1] SMP
- [ 91.611445] Modules linked in:
- [ 91.612295] CPU: 3 PID: 567 Comm: test Kdump: loaded Not tainted 5.0.0+ #3
- [ 91.612961] Hardware name: linux,dummy-virt (DT)
- [ 91.613649] pstate: 60400005 (nZCv daif +PAN -UAO)
- [ 91.614665] pc : __se_sys_getpuid+0x98/0x138
- [ 91.615234] lr : __se_sys_getpuid+0x90/0x138
- [ 91.615778] sp : ffff8000246efc20
- [ 91.615955] x29: ffff8000246efc20 x28: ffff800024499c00
- [ 91.616225] x27: 0000000000000000 x26: 0000000000000000
- [ 91.616417] x25: 0000000056000000 x24: 0000000000000015
- [ 91.616606] x23: 0000000080001000 x22: 0000ffffaa42fec4
- [ 91.616794] x21: 00000000ffffffff x20: 000080001dffe000
- [ 91.616982] x19: 0000000000000000 x18: 0000000000000000
- [ 91.622325] x17: 0000000000000000 x16: 0000000000000000
- [ 91.622834] x15: 0000000000000000 x14: 0000000000000000
- [ 91.623588] x13: 0000000000000000 x12: 0000000000000000
- [ 91.624877] x11: 0000000000000000 x10: 0000000000000000
- [ 91.633289] x9 : ffff000010183f24 x8 : 5f6c61767274705f
- [ 91.633499] x7 : 5f5f5f283d646975 x6 : ffff0000121cfb5b
- [ 91.633713] x5 : ffff000010f30654 x4 : ffff80002fdd6f88
- [ 91.634097] x3 : 0000000000000000 x2 : 0000000000000000
- [ 91.634497] x1 : 0000000000000237 x0 : 0000ffffed6d2530
- [ 91.634813] Process test (pid: 567, stack limit = 0x(____ptrval____))
- [ 91.635419] Call trace:
- [ 91.635583] __se_sys_getpuid+0x98/0x138
- [ 91.642748] __arm64_sys_getpuid+0x34/0x3c
- [ 91.643152] __invoke_syscall+0x24/0x2c
- [ 91.643358] invoke_syscall+0xa4/0xd8
- [ 91.643592] el0_svc_common+0x100/0x1e4
- [ 91.643788] el0_svc_handler+0x414/0x440
- [ 91.644039] el0_svc+0x8/0xc
- [ 91.645545] Code: f94027e0 94035ff3 2a0003e1 f9401be0 (b9000001)
- [ 91.662141] SMP: stopping secondary CPUs
- [ 91.677656] Starting crashdump kernel...
- [ 91.678304] Bye!
更改使用 put_user 函数后,运行无误。