前言
用户进程有用户态和内核态两种执行状态,用户进程可以通过系统调用陷入内核态,陷入内核态就意味着可以访问内核的资源。
那么如何陷入内核态呢?一般通过同步异常操作来实现。ARM64专门定义了svc指令,用于进入同步异常,也就是说,一旦执行了svc指令,cpu立即跳转到同步异常入口地址处,从这个地址进入内核态。
从glibc说起
要了解系统调用的完整机制,我们可以从glibc入手,因为glibc有很多的系统调用实例。
我们随便找一个库函数,比如tcsetattr,该函数用于设置终端的属性,它会调用__ioctl函数,__ioctl函数在ARM64中的定义如下:
ENTRY(__ioctl)
mov x8, #__NR_ioctl
sxtw x0, w0
svc #0x0
cmn x0, #4095
b.cs .Lsyscall_error
ret
PSEUDO_END (__ioctl)
- 将系统调用号存放在x8寄存器中
- 执行svc指令,陷入异常,并且从el0切换到el1。
异常入口
我们首先来看下ARM64的异常向量表:
ENTRY(vectors)
ventry el1_sync_invalid // Synchronous EL1t
ventry el1_irq_invalid // IRQ EL1t
ventry el1_fiq_invalid // FIQ EL1t
ventry el1_error_invalid // Error EL1t
ventry el1_sync // Synchronous EL1h
ventry el1_irq // IRQ EL1h
ventry el1_fiq_invalid // FIQ EL1h
ventry el1_error_invalid // Error EL1h
ventry el0_sync // Synchronous 64-bit EL0
ventry el0_irq // IRQ 64-bit EL0
ventry el0_fiq_invalid // FIQ 64-bit EL0
ventry el0_error_invalid // Error 64-bit EL0
#ifdef CONFIG_COMPAT
ventry el0_sync_compat // Synchronous 32-bit EL0
ventry el0_irq_compat // IRQ 32-bit EL0
ventry el0_fiq_invalid_compat // FIQ 32-bit EL0
ventry el0_error_invalid_compat // Error 32-bit EL0
#else
ventry el0_sync_invalid // Synchronous 32-bit EL0
ventry el0_irq_invalid // IRQ 32-bit EL0
ventry el0_fiq_invalid // FIQ 32-bit EL0
ventry el0_error_invalid // Error 32-bit EL0
#endif
END(vectors)
arm64有4份异常向量表,那么需要用哪一份呢?由于svc导致CPU从el0切换到el1,所以要使用第三份异常向量表,即:
ventry el0_sync // Synchronous 64-bit EL0
ventry el0_irq // IRQ 64-bit EL0
ventry el0_fiq_invalid // FIQ 64-bit EL0
ventry el0_error_invalid // Error 64-bit EL0
而同步异常的入口是el0_sync。
同步异常到系统调用
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
读esr寄存器,获取异常产生的原因,由于系统调用是通过 svc指令产生的异常,所以调用el0_svc函数。
内核中系统调用的定义
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, #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这张表中,系统调用号是这张表的索引,而以以系统调用号为索引的地址存放该系统调用的函数地址。下一章节详细介绍。
- x8 前面说过了,存放系统调用号。
- 该函数要做的事情非常简单,就是以x8为索引,找到sys_call_table存放的与系统调用号对应的函数。
系统调用定义
对于ARM64来说,sys_call_table存放了所有的系统调用,索引就是系统调用号。
include/uapi/asm-generic/unistd.h中定义通用了系统调用。我们来看一个ioctl的例子:
#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>
};
#define __SC_COMP(_nr, _sys, _comp) __SYSCALL(_nr, _comp)
#define __SC_COMP_3264(_nr, _32, _64, _comp) __SYSCALL(_nr, _comp)
......
#define __NR_ioctl 29
__SC_COMP(__NR_ioctl, sys_ioctl, compat_sys_ioctl)
很简单,就是以__NR_ioctl 为索引,将compat_sys_ioctl存放在sys_call_table这张系统调用表中。
版权声明:本文为liuhangtiant原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。