linux系统调用_arm实现深入剖析(上下文保存、参数传递)

前言

这里我们不再赘述系统调用的基本原理以及系统调用产生时的函数调用以及系统调用表,直接看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)

linux系统调用_arm实现深入剖析(上下文保存、参数传递)_第1张图片

步骤简述:

  1. 保存用户态寄存器到内核栈底,包括:

    r0~r12: 用户态(sys/usr模式)和内核态(svc)共用

    SP、LR: 用户态和内核独自的寄存器,读取用户态(sys模式)的值,arm指令是用{sp, lr}^读取,而thumb指令是直接切回sys模式读取的;

    只用记录用户态返回地址LR即可,PC值各模式共用,执行指令的时候PC值已做修改,没有保存的意义。

  2. 读取系统调用号,加载系统调用表,OABI和EABI不同;

  3. 如果有系统调用trace,则调用__sys_trace函数

  4. 将系统调用的第5个参数和第6个参数压栈,因为系统调用使用r0r5传参,而ARM函数调用使用r0r3传参,这里做适配。

  5. 设置函数返回地址为ret_fast_syscall

  6. 执行系统调用具体处理函数

返回用户态

上述执行系统调用具体处理函数之前设置返回地址为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)

我们看看系统调用中是如何读取参数的即可知道是如何传参的。并且研究系统调用在定义时如何知名参数的。

x86中系统调用传参

A.当系统调用所需参数的个数不超过5个的时候,执行"int$0x80"指令时,需在eax中存放系统调用的功能号,传递给系统调用的参数则按照参数顺序依次存放到寄存器ebx,ecx,edx,esi,edi中,当系统调用完成之后,返回值存放在eax中;

B.当系统调用的参数超过5个的时候,执行"int$0x80"指令,需在eax中存放系统调用的功能号,所不同的只是全部的参数应该依次存放在一块连续的内存区域里,同时在寄存器ebx中保存指向该内存区域的指针(即:该连续内存块的首地址),返回值仍然保存在寄存器eax中;由于只是需要一块连续的内存区域来保存系统调用所需要的参数,因此,完全可以像普通的函数调用一样使用栈来传递系统调用所需要的参数;但是要注意一点:Linux采用的是C语言的调用模式,这就意味着所有参数必须以相反的顺序进栈,即:最后一个参数最先进栈,而第一个参数最后进栈;如果采用栈来传递系统调用所需要的参数,在执行"int$0x80"指令时,还应将栈指针的当前值(栈顶地址)复制到寄存器ebx中;

ARM中系统调用传参

系统调用采用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上下文保存在什么地方呢?

中断: per_cpu变量_irq_reg

进程切换:thread_info的cpu_context

系统调用: 内核栈底

你可能感兴趣的:(小张学inux内核,linux,系统调用,软中断)