- 重要:本系列文章内容摘自
<Linux内核深度解析>
基于ARM64架构的Linux4.x内核一书,作者余华兵。系列文章主要用于记录Linux内核的大部分机制及参数的总结说明
1 系统调用
系统调用是内核给用户程序提供的编程接口。用户程序调用系统调用,通常使用glibc库针对单个系统调用封装的函数。如果glibc库没有针对某个系统调用封装函数,用户程序可以使用通用的封装函数syscall():
#define _GNU_SOURCE
#include <unistd.h>
#include <sys/syscall.h>
long syscall(long number, ...);
参数number是系统调用号,后面是传递给系统调用的参数。
返回值0表示成功,返回值−1表示错误,错误号存储在变量errno中。
例如,应用程序使用系统调用fork()创建子进程,有两种调用方法:
(1)ret = fork();
(2)ret = syscall(SYS_fork);
ARM64处理器提供的系统调用指令是svc,调用约定如下:
(1)64位应用程序使用寄存器x8传递系统调用号,32位应用程序使用寄存器x7传递系统调用号。
(2)使用寄存器x0~x6最多可以传递7个参数。
(3)当系统调用执行完的时候,使用寄存器x0存放返回值。
1.1 定义系统调用
Linux内核使用宏SYSCALL_DEFINE定义系统调用,以创建子进程的系统调用fork为例:
kernel/fork.c
SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
return _do_fork(SIGCHLD, 0, 0, NULL, NULL, 0);
#else
return -EINVAL;
#endif
}
把宏“SYSCALL_DEFINE0(fork)”展开以后是:
asmlinkage long sys_fork(void)
“SYSCALL_DEFINE”后面的数字表示系统调用的参数个数,“SYSCALL_DEFINE0”表示系统调用没有参数,“SYSCALL_DEFINE6”表示系统调用有6个参数,如果参数超过6个,使用宏“SYSCALL_DEFINEx”。头文件“include/linux/syscalls.h”定义了这些宏。
“asmlinkage”表示这个C语言函数可以被汇编代码调用。如果使用C++编译器,“asmlinkage”被定义为extern “C”;如果使用C编译器,“asmlinkage”是空的宏。
系统调用的函数名称以“sys_”开头。
需要在系统调用表中保存系统调用号和处理函数的映射关系,ARM64架构定义的系统调用表sys_call_table如下:
arch/arm64/kernel/sys.c
#undef __SYSCALL
#define __SYSCALL(nr, sym) [nr] = sym,
void * const sys_call_table[__NR_syscalls] __aligned(4096) = {
[0 ... __NR_syscalls - 1] = sys_ni_syscall,
#include <asm/unistd.h>
};
对于ARM64架构,头文件“asm/unistd.h”是“arch/arm64/include/asm/unistd.h”:
arch/arm64/include/asm/unistd.h
#include <uapi/asm/unistd.h>arch/arm64/include/uapi/asm/unistd.h
#include <asm-generic/unistd.h>include/asm-generic/unistd.h
#include <uapi/asm-generic/unistd.h>include/uapi/asm-generic/unistd.h
#define __NR_io_setup 0
__SC_COMP(__NR_io_setup, sys_io_setup, compat_sys_io_setup)
…
#define __NR_fork 1079
#ifdef CONFIG_MMU
__SYSCALL(__NR_fork, sys_fork)
#else
__SYSCALL(__NR_fork, sys_ni_syscall)
#endif
#undef __NR_syscalls
#define __NR_syscalls (__NR_fork+1)
1.2 执行系统调用
ARM64处理器把系统调用划分到同步异常,在异常级别1的异常向量表中,系统调用的入口有两个:
(1)如果64位应用程序执行系统调用指令svc,系统调用的入口是el0_sync。
(2)如果32位应用程序执行系统调用指令svc,系统调用的入口是el0_sync_compat。
el0_sync的代码如下:
arch/arm64/kernel.c
.align 6
el0_sync:
kernel_entry 0
mrs x25, esr_el1
lsr x24, x25, #ESR_ELx_EC_SHIFT
cmp x24, #ESR_ELx_EC_SVC64
b.eq el0_svc
…
1)把当前进程的寄存器值保存在内核栈中。
2)读取异常症状寄存器esr_el1。
3)解析出异常症状寄存器的异常类别字段。
4)如果异常类别是系统调用,跳转到el0_svc。
el0_svc负责执行系统调用,其代码如下:
arch/arm64/kernel.c
sc_nr .req x25
scno .req x26
stbl .req x27
tsk .req x28
.align 6
el0_svc:
adrp stbl, sys_call_table
uxtw scno, w8
mov sc_nr, #__NR_syscalls
el0_svc_naked:
stp x0, scno, [sp, #S_ORIG_X0]
enable_dbg_and_irq
ct_user_exit 1
ldr x16, [tsk, #TSK_TI_FLAGS]
tst x16, #_TIF_SYSCALL_WORK
b.ne __sys_trace
cmp scno, sc_nr
b.hs ni_sys
ldr x16, [stbl, scno, lsl #3]
blr x16
b ret_fast_syscall
ni_sys:
mov x0, sp
bl do_ni_syscall
b ret_fast_syscall
ENDPROC(el0_svc)
1)把寄存器x27设置为系统调用表sys_call_table的起始地址。
2)把寄存器x26设置为系统调用号。64位进程使用寄存器x8传递系统调用号,w8是寄存器x8的32位形式。
3)把寄存器x25设置为系统调用的数量,也就是(最大的系统调用号+1)。
4)把寄存器x0和x8的值保存到内核栈中,x0存放系统调用的第一个参数,x8存放系统调用号。
5)开启调试异常和中断。
6)如果使用ptrace跟踪系统调用,跳转到__sys_trace处理。
7)如果进程传递的系统调用号等于或大于系统调用的数量,即大于最大的系统调用号,那么是非法值,跳转到ni_sys处理错误。
8)计算出系统调用号对应的表项地址(sys_call_table + 系统调用号 * 8),然后取出处理函数的地址。
9)调用系统调用号对应的处理函数。
10)从系统调用返回用户空间。
ret_fast_syscall从系统调用返回用户空间,其代码如下:
arch/arm64/kernel.c
ret_fast_syscall:
disable_irq
str x0, [sp, #S_X0]
ldr x1, [tsk, #TSK_TI_FLAGS]
and x2, x1, #_TIF_SYSCALL_WORK
cbnz x2, ret_fast_syscall_trace
and x2, x1, #_TIF_WORK_MASK
cbnz x2, work_pending
enable_step_tsk x1, x2
kernel_exit 0
ret_fast_syscall_trace:
enable_irq
b __sys_trace_return_skipped
work_pending:
mov x0, sp
bl do_notify_resume
#ifdef CONFIG_TRACE_IRQFLAGS
bl trace_hardirqs_on
#endif
ldr x1, [tsk, #TSK_TI_FLAGS]
b finish_ret_to_user
ret_to_user:
…
finish_ret_to_user:
enable_step_tsk x1, x2
kernel_exit 0
ENDPROC(ret_to_user)
1)禁止中断。
2)寄存器x0已经存放了处理函数的返回值,把保存在内核栈中的寄存器x0的值更新为返回值。
3)如果使用ptrace跟踪系统调用,跳转到ret_fast_syscall_trace处理。
4)如果进程的thread_info.flags设置了需要重新调度(_TIF_NEED_RESCHED)或者有信号需要处理(_TIF_SIGPENDING)等标志位,跳转到work_pending处理。
5)如果使用系统调用ptrace设置了软件单步执行,那么开启单步执行。
6)使用保存在内核栈中的寄存器值恢复寄存器,从内核模式返回用户模式。
work_pending调用函数do_notify_resume,函数do_notify_resume的代码如下:
arch/arm64/kernel/signal.c
asmlinkage void do_notify_resume(struct pt_regs *regs,
unsigned int thread_flags)
{
…
do {
if (thread_flags & _TIF_NEED_RESCHED) {
schedule();
} else {
local_irq_enable();
if (thread_flags & _TIF_UPROBE)
uprobe_notify_resume(regs);
if (thread_flags & _TIF_SIGPENDING)
do_signal(regs);
if (thread_flags & _TIF_NOTIFY_RESUME) {
clear_thread_flag(TIF_NOTIFY_RESUME);
tracehook_notify_resume(regs);
}
if (thread_flags & _TIF_FOREIGN_FPSTATE)
fpsimd_restore_current_state();
}
local_irq_disable();
thread_flags = READ_ONCE(current_thread_info()->flags);
} while (thread_flags & _TIF_WORK_MASK);
}
1)如果当前进程的thread_info.flags设置了标志位_TIF_NEED_RESCHED,那么调度进程。
2)如果设置了标志位_TIF_UPROBE,调用函数uprobe_notify_resume()处理。uprobes(user-space probes,用户空间探测器)可以在进程的任何指令地址插入探测器,收集调试和性能信息,发现性能问题。需要内核支持,编译内核时开启配置宏CONFIG_UPROBE_EVENTS。
3)如果设置了标志位_TIF_SIGPENDING,调用函数do_signal()处理信号。
4)如果设置了标志位_TIF_NOTIFY_RESUME,那么调用函数tracehook_notify_resume(),执行返回用户模式之前的回调函数。
5)如果设置了标志位_TIF_FOREIGN_FPSTATE,那么恢复浮点寄存器。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)