作者:罗宇哲,中国科学院软件研究所智能软件研究中心
上一期中我们介绍了ARM Linux内核中的系统调用和定义系统调用的流程,这一期我们将介绍系统调用的执行过程。
在第二十九期和第三十期中,我们介绍过ARM Linux内核中的异常向量表和异常处理的一般流程。系统调用属于同步异常的范畴,因此遵循异常处理的一般流程。ARM Linux 内核的异常向量表在/kernel-4.19/arch/arm64/kernel/entry.S文件中:
该表从上到下共有四组,系统调用则属于第三组和第四组:
两组中的sync项即为同步异常(包括系统调用)的入口。以64位应用程序的系统调用为例,通过sync入口程序跳转到el0_sync处(这是因为kernel_ventry宏会在跳转的目标label前加入el字符串和输入的异常级别),el0_sync入口代码在entry.S文件中:
/*
* EL0 mode handlers.
*/
.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_SVE // SVE access
b.eq el0_sve_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
cmp x24, #ESR_ELx_EC_PC_ALIGN // pc alignment exception
b.eq el0_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
我们在第三十期介绍过,发生异常的原因会被保存在ESR寄存器中,该寄存器中的值被移位后与各种异常情况的标号值相比较,如果该值等于#ESR_ELx_EC_SVC64说明该同步异常是由SVC指令触发的(系统调用也由该指令触发),程序跳转到el0_svc处。el0_svc处的代码在entry.S文件中:
它跳转到了el0_svc_handler处,其代码在openeuler/kernel/blob/kernel-4.19/arch/arm64/kernel/syscall.c中:
asmlinkage表示该C函数可以在汇编语言中被调用。该函数调用了el0_svc_common()函数,其代码在syscall.c文件中:
static void el0_svc_common(struct pt_regs *regs, int scno, int sc_nr,
const syscall_fn_t syscall_table[])
{
unsigned long flags = current_thread_info()->flags;
regs->orig_x0 = regs->regs[0];//x0中保存了栈指针sp
regs->syscallno = scno;//保存系统调用号到pt_regs结构体
cortex_a76_erratum_1463225_svc_handler();
local_daif_restore(DAIF_PROCCTX);
user_exit();
if (has_syscall_work(flags)) {//检查_TIF_SYSCALL_WORK位,如果为1
/* set default errno for user-issued syscall(-1) */
if (scno == NO_SYSCALL)
regs->regs[0] = -ENOSYS;
scno =
syscall_trace_enter(regs);//用于跟踪系统调用的情况,将系统调用进入时的信息写入审计上下文中,与安全审计功能有关
if (scno == NO_SYSCALL)
goto trace_exit;
}
invoke_syscall(regs, scno, sc_nr, syscall_table);//执行系统调用
……
trace_exit:
syscall_trace_exit(regs);//用于跟踪系统调用的情况,将系统调用退出时的信息写入审计上下文中,与安全审计功能有关
}
从输入参数看,该函数的输入有保存系统调用发生时的寄存器状态的pt_regs结构体、系统调用号(被保存在寄存器x8中)、系统调用总数__NR_syscalls(294)和系统调用表。该函数调用invoke_syscall()函数执行系统调用,invoke_syscall()函数代码在syscall.c文件中:
static void invoke_syscall(struct pt_regs *regs, unsigned int scno,
unsigned int sc_nr,
const syscall_fn_t syscall_table[])
{
long ret;
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);//处理未识别系统调用
}
regs->regs[0] = ret;//x0寄存器保存系统调用返回值
}
如果输入的系统调用号小于系统调用的总数,那么从系统调用表中找到对应的系统调用处理函数,并调用__invoke_syscall()函数处理系统调用。如果系统调用号(从0开始)大于或等于系统调用总数,那么为非法情形,调用do_ni_syscall()函数。array_index_nospec()宏用于确认scno不会超过sc_nr。__invoke_syscall()函数调用系统调用处理函数,其代码在syscall.c文件中:
do_ni_syscall()处理那些未识别的系统调用,其代码在syscall.c文件中:
static long do_ni_syscall(struct pt_regs *regs, int scno)
{
#ifdef CONFIG_COMPAT
long ret;
if (is_compat_task())
{//在openeuler/kernel/blob/kernel-4.19/include/linux/compat.h文件中该条件被定义为0
ret = compat_arm_syscall(regs, scno);//处理所有未识别的系统调用
if (ret != -ENOSYS)
return ret;
}
#endif
return sys_ni_syscall();//返回错误值
}
do_ni_syscall()调用sys_ni_syscall()函数,该函数直接返回错误值,其代码在openeuler/kernel/blob/kernel-4.19/kernel/sys_ni.c文件中:
错误值ENOSYS的含义为未定义函数。
本期我们介绍了ARM Linux内核中的系统调用的执行流程,下一期我们将尝试向ARM
Linux内核中增加一个系统调用。
参考文献