Linux+ARM64 系统调用

前言


用户进程有用户态和内核态两种执行状态,用户进程可以通过系统调用陷入内核态,陷入内核态就意味着可以访问内核的资源。

那么如何陷入内核态呢?一般通过同步异常操作来实现。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版权协议,转载请附上原文出处链接和本声明。