这里我们不再赘述系统调用的基本原理以及系统调用产生时的函数调用以及系统调用表,直接看arm中的软中断产生及返回时如何保存寄存器上下文、如何陷入内核再返回、如何传参,模式切换做了哪些处理。
这里mark一下: EABI和OABI系统调用实现的方式有所不同。
不同于x86的int 80中断,arm中使用软中断指令swi实现系统调用,直接看swi的处理向量
vector_swi:
/*=============================================================================
* SWI handler
*-----------------------------------------------------------------------------
*/
.align 5
ENTRY(vector_swi)
sub sp, sp, #S_FRAME_SIZE
/*保存r0~r10到寄存器,因为r0~r10时usr/sys模式和svc共用的寄存器*/
stmia sp, {r0 - r12} @ Calling r0 - r12
/*
保存sp和lr,既r13和r14, 该寄存器是sys模式和svc各自私有的,所以要使用sys模式的值
* arm指令是用{sp, lr}^表示,thumb指令是直接切回sys模式的
*/
ARM( add r8, sp, #S_PC )
ARM( stmdb r8, {sp, lr}^ ) @ Calling sp, lr
THUMB( mov r8, sp )
THUMB( store_user_sp_lr r8, r10, S_SP ) @ calling sp, lr
mrs r8, spsr @ called from non-FIQ mode, so ok.
str lr, [sp, #S_PC] @ Save calling PC
str r8, [sp, #S_PSR] @ Save CPSR
str r0, [sp, #S_OLD_R0] @ Save OLD_R0
zero_fp
/*获取系统调用号,OABI和EABI不一样*/
#if defined(CONFIG_OABI_COMPAT)
/*
* If we have CONFIG_OABI_COMPAT then we need to look at the swi
* value to determine if it is an EABI or an old ABI call.
*/
#ifdef CONFIG_ARM_THUMB
tst r8, #PSR_T_BIT
movne r10, #0 @ no thumb OABI emulation
ldreq r10, [lr, #-4] @ get SWI instruction
#else
ldr r10, [lr, #-4] @ get SWI instruction
#endif
#ifdef CONFIG_CPU_ENDIAN_BE8
rev r10, r10 @ little endian instruction
#endif
#elif defined(CONFIG_AEABI)
/*
* Pure EABI user space always put syscall number into scno (r7).
*/
#elif defined(CONFIG_ARM_THUMB)
/* Legacy ABI only, possibly thumb mode. */
tst r8, #PSR_T_BIT @ this is SPSR from save_user_regs
addne scno, r7, #__NR_SYSCALL_BASE @ put OS number in
ldreq scno, [lr, #-4]
#else
/* Legacy ABI only. */
ldr scno, [lr, #-4] @ get SWI instruction
#endif
#ifdef CONFIG_ALIGNMENT_TRAP
ldr ip, __cr_alignment
ldr ip, [ip]
mcr p15, 0, ip, c1, c0 @ update control register
#endif
enable_irq
ct_user_exit
get_thread_info tsk
/*加载中断向量表*/
adr tbl, sys_call_table @ load syscall table pointer
#if defined(CONFIG_OABI_COMPAT)
/*
* If the swi argument is zero, this is an EABI call and we do nothing.
*
* If this is an old ABI call, get the syscall number into scno and
* get the old ABI syscall table address.
*/
bics r10, r10, #0xff000000
eorne scno, r10, #__NR_OABI_SYSCALL_BASE
ldrne tbl, =sys_oabi_call_table
#elif !defined(CONFIG_AEABI)
bic scno, scno, #0xff000000 @ mask off SWI op-code
eor scno, scno, #__NR_SYSCALL_BASE @ check OS number
#endif
local_restart:
ldr r10, [tsk, #TI_FLAGS] @ check for syscall tracing
/*将第5个参数和第6个参数保存到栈顶,将系统调用和函数调用适配*/
stmdb sp!, {r4, r5} @ push fifth and sixth args
/*如果支持系统调用回溯,调用__sys_trace*/
tst r10, #_TIF_SYSCALL_WORK @ are we tracing syscalls?
bne __sys_trace
cmp scno, #NR_syscalls @ check upper syscall limit
/*设置系统调用返回函数为ret_fast_syscall*/
adr lr, BSYM(ret_fast_syscall) @ return address
ldrcc pc, [tbl, scno, lsl #2] @ call sys_* routine
add r1, sp, #S_OFF
2: mov why, #0 @ no longer a real syscall
cmp scno, #(__ARM_NR_BASE - __NR_SYSCALL_BASE)
eor r0, scno, #__NR_SYSCALL_BASE @ put OS number back
bcs arm_syscall
b sys_ni_syscall @ not private func
ENDPROC(vector_swi)
步骤简述:
保存用户态寄存器到内核栈底,包括:
r0~r12: 用户态(sys/usr模式)和内核态(svc)共用
SP、LR: 用户态和内核独自的寄存器,读取用户态(sys模式)的值,arm指令是用{sp, lr}^读取,而thumb指令是直接切回sys模式读取的;
只用记录用户态返回地址LR即可,PC值各模式共用,执行指令的时候PC值已做修改,没有保存的意义。
读取系统调用号,加载系统调用表,OABI和EABI不同;
如果有系统调用trace,则调用__sys_trace函数
将系统调用的第5个参数和第6个参数压栈,因为系统调用使用r0r5传参,而ARM函数调用使用r0r3传参,这里做适配。
设置函数返回地址为ret_fast_syscall
执行系统调用具体处理函数
上述执行系统调用具体处理函数之前设置返回地址为ret_fast_syscall,也是使用该函数返回用户态的。
ret_fast_syscall:
UNWIND(.fnstart )
UNWIND(.cantunwind )
disable_irq @ disable interrupts
ldr r1, [tsk, #TI_FLAGS]
tst r1, #_TIF_WORK_MASK
bne fast_work_pending
asm_trace_hardirqs_on
/* perform architecture specific actions before user return */
arch_ret_to_user r1, lr
ct_user_enter
/*加载用户态的上下文,并返回用户态*/
restore_user_regs fast = 1, offset = S_OFF
UNWIND(.fnend )
ret_fast_syscall中主要是加载用户态的上下文,并返回用户态。
UNWIND是追溯函数调用链的段。返回用户态不再用UNWIND段。
restore_user_regs函数:
.macro restore_user_regs, fast = 0, offset = 0
ldr r1, [sp, #\offset + S_PSR] @ get calling cpsr
ldr lr, [sp, #\offset + S_PC]! @ get pc
msr spsr_cxsf, r1 @ save in spsr_svc
#if defined(CONFIG_CPU_V6)
strex r1, r2, [sp] @ clear the exclusive monitor
#elif defined(CONFIG_CPU_32v6K)
clrex @ clear the exclusive monitor
#endif
.if \fast
ldmdb sp, {r1 - lr}^ @ get calling r1 - lr
.else
ldmdb sp, {r0 - lr}^ @ get calling r0 - lr
.endif
mov r0, r0 @ ARMv5T and earlier require a nop
@ after ldm {}^
add sp, sp, #S_FRAME_SIZE - S_PC
/*加载系统调用返回地址继续执行*/
movs pc, lr @ return & move spsr_svc into cpsr
.endm
sys_call_table的定义entry-common.S中
ENTRY(sys_call_table)
#include "calls.S"
#undef ABI
#undef OBSOLETE
calls.S中定义系统调用
/* 0 */ CALL(sys_restart_syscall)
CALL(sys_exit)
CALL(sys_fork)
CALL(sys_read)
CALL(sys_write)
/* 5 */ CALL(sys_open)
CALL(sys_close)
CALL(sys_ni_syscall) /* was sys_waitpid */
CALL(sys_creat)
CALL(sys_link)
/* 10 */ CALL(sys_unlink)
CALL(sys_execve)
CALL(sys_chdir)
CALL(OBSOLETE(sys_time)) /* used by libc4 */
CALL(sys_mknod)
我们看看系统调用中是如何读取参数的即可知道是如何传参的。并且研究系统调用在定义时如何知名参数的。
A.当系统调用所需参数的个数不超过5个的时候,执行"int$0x80"指令时,需在eax中存放系统调用的功能号,传递给系统调用的参数则按照参数顺序依次存放到寄存器ebx,ecx,edx,esi,edi中,当系统调用完成之后,返回值存放在eax中;
B.当系统调用的参数超过5个的时候,执行"int$0x80"指令,需在eax中存放系统调用的功能号,所不同的只是全部的参数应该依次存放在一块连续的内存区域里,同时在寄存器ebx中保存指向该内存区域的指针(即:该连续内存块的首地址),返回值仍然保存在寄存器eax中;由于只是需要一块连续的内存区域来保存系统调用所需要的参数,因此,完全可以像普通的函数调用一样使用栈来传递系统调用所需要的参数;但是要注意一点:Linux采用的是C语言的调用模式,这就意味着所有参数必须以相反的顺序进栈,即:最后一个参数最先进栈,而第一个参数最后进栈;如果采用栈来传递系统调用所需要的参数,在执行"int$0x80"指令时,还应将栈指针的当前值(栈顶地址)复制到寄存器ebx中;
系统调用采用r0~r5传参,最多传6个参数。
因为系统调用传参和函数调用传参规则不一样,在SWI向量vector_swi中有做适配,将r4、r5压栈,ARM中函数调用使用r0~r3传参,多余的参数采用栈传递,之后就走函数调用过程。
ldr r10, [tsk, #TI_FLAGS] @ check for syscall tracing
/*将第5个参数和第6个参数保存到栈顶,将系统调用和函数调用适配*/
stmdb sp!, {r4, r5} @ push fifth and sixth args
/*如果支持系统调用回溯,调用__sys_trace*/
tst r10, #_TIF_SYSCALL_WORK @ are we tracing syscalls?
bne __sys_trace
cmp scno, #NR_syscalls @ check upper syscall limit
/*设置系统调用返回函数为ret_fast_syscall*/
adr lr, BSYM(ret_fast_syscall) @ return address
ldrcc pc, [tbl, scno, lsl #2] @ call sys_* routine
上面我们已经知道系统调用的CPU上下文保存在栈底,那么其他场景下CPU上下文保存在什么地方呢?