注意:请使用谷歌浏览器阅读(IE浏览器排版混乱)
【摘要】
本文将为您介绍linux内核是如何实现系统调用的。缺页异常、中断和系统调用同属arm异常处理,笔者计划分三篇文档分别介绍一下,其实在汇编阶段三种处理流程有很多相通之处,不过为了阅读方便,即使相同的部分也会重新在各自文档中介绍一遍。
【正文一】linux内核系统调用之汇编阶段
1 为了介绍方便介绍,先列出两个知识点。
1.1 linux系统为实现异常处理引入了栈帧的概念:
#define ARM_cpsr uregs[16]
#define ARM_pc uregs[15]
#define ARM_lr uregs[14]
#define ARM_sp uregs[13]
#define ARM_ip uregs[12]
#define ARM_fp uregs[11]
#define ARM_r10 uregs[10]
#define ARM_r9 uregs[9]
#define ARM_r8 uregs[8]
#define ARM_r7 uregs[7]
#define ARM_r6 uregs[6]
#define ARM_r5 uregs[5]
#define ARM_r4 uregs[4]
#define ARM_r3 uregs[3]
#define ARM_r2 uregs[2]
#define ARM_r1 uregs[1]
#define ARM_r0 uregs[0]
#define ARM_ORIG_r0 uregs[17]
1.2 Arm的几种模式介绍:
处理器模式 缩写 对应的M[4:0]编码 Privilegelevel
User usr 10000 PL0
FIQ fiq 10001 PL1
IRQ irq 10010 PL1
Supervisor svc 10011 PL1
Monitor mon 10110 PL1
Abort abt 10111 PL1
Hyp hyp 11010 PL2
Undefined und 11011 PL1
System sys 11111 PL1
1.3 ARM 异常处理总入口(entry-armv.s):
/*注释:
1)Arm架构异常处理向量表起始地址__vectors_start。
2)Arm架构定义7种异常包括中断、系统调用、缺页异常等,发生异常时处理器会跳转到相应入口。
3)异常向量表起始位置有cp15协处理器的控制寄存器c1的bit13
决定:v=0时,异常向量起始于0xffff0000;v=1时起始于0x0.
4)举例:当IRQ发生时跳转到0x18这个虚拟地址上(内核态虚拟地址)
head.s中设置了cp15寄存器器(proc-v7.s->__v7_setup()函数设置的)
*/
__vectors_start:
W(b) vector_rst
W(b) vector_und
/*
系统调用入口点:
__vectors_start + 0x1000=__stubs_start
此时pc指向系统调用异常 的处理入口:vector_swi
用户态通过swi指令产生软中断。
因为系统调用异常代码编译到其他文件,其入口地址与异常向量相隔
较远,使用b指令无法跳转过去(b指令只能相对当前pc跳转+/-32M范围)
*/
W(ldr) pc, __vectors_start + 0x1000
/* 取指令异常 */
W(b) vector_pabt
/* 数据异常--缺页异常 */
W(b) vector_dabt
W(b) vector_addrexcptn
/* 中断异常 */
W(b) vector_irq
W(b) vector_fiq
1.4 系统调用处理:vctor_swi
/*
vector_swi系统调用的处理.swi软中断处理过程:
*/
/* 2的5次方对齐 */
.align 5
/* 系统调用处理入口 */
ENTRY(vector_swi)
/*
1 注意swi指令让cpu进入svc模式,此时sp的值是arm在svc模式下的sp的值,而不是user模式下的sp.
2 对于用户进程来说此时的sp也是内核态下的sp
那么该sp是在何时指定的呢?
1) 在该用户态进程创建过程中sys_clone->do_fork->copy_thread()函数
实现了thread->cpu_context.sp = (unsigned long)childregs;
而这个childregs是在上一步sys_clone->do_fork->dup_task_struct中申请的
2) 当当前进程真正切换到本用户进程时 这个thread->cpu_context.sp
在__switch_to中被加载到arm在svc模式下的sp寄存器
sp的具体指向参见task_pt_regs中定义:(栈顶-8byte)就是sp的值
栈顶即是:current->stack+THREAD_SIZE;用户每次陷入内核,内核态的栈都指向此;
举例:
[linuxrc]进程:
1 current->stack=0x8053a000(8k字节对齐)
2 当linuxrc进入内核态时sp=0x8053a000+0x2000-8=0x8053BFF8;注意上下文切换时sp是0x8053BF80,返回用户态时restore_user_regs中sp加72;
3 也就是说进程在内核态下的栈是从current->stack+THREAD_SIZE-8-sizeof(pt_regs)
开始向下增长的,并且其中current->stack+THREAD_SIZE-8-sizeof(pt_regs)字节
是一个栈帧的长度
普通c函数中使用的栈一般是从current->stack+THREAD_SIZE-8-sizeof(pt_regs)
开始向下增长
sp=sp-72
*/
sub sp, sp, #S_FRAME_SIZE
/*
将r0-r12保存到栈上
1 其中sp没有加!,所以sp保存不变;
2 sp = r0;[sp+4]=r1 ...;
*/
stmia sp, {r0 - r12} @ Calling r0 - r12
#ifdef gSysDebugInfoSYSCALL
//sp=sp-4;[sp]=lr
#if 0
/* 打印r0的值 */
stmfd sp!,{lr}
bl do_show_r0
ldmfd sp!,{lr}
#endif
mov r0,sp
stmfd sp!,{lr}
bl do_show_sp
ldmfd sp!,{lr}
ldr r0,[sp]
#endif
/*
r8对应栈帧中的pc
*/
ARM( add r8, sp, #S_PC )
/*
r8 = rt_regs->pc 注意,r8中不是保存的pc指针,而是保存pc指针在栈帧中地址。
1 stmdb保存第一个值之前向下增加。r8保存的是PC:[r8-4]=lr;
2 r8-4栈地址:保存的的是lr(r14) ;r8-8保存sp(r13),即将sp、lr保存到栈上
*/
ARM( stmdb r8, {sp, lr}^ ) @ Calling sp, lr
/*r8中保存的是栈底地址*/
THUMB( mov r8, sp )
/* 未执行
.macro store_user_sp_lr, rd, rtemp, offset = 0
mrs \rtemp, cpsr
eor=^
eor \rtemp, \rtemp, #(SVC_MODE ^ SYSTEM_MODE)
msr cpsr_c, \rtemp @ switch to the SYS mode
//r0:中保存栈地址;r1中保存lr地址。
str sp, [\rd, #\offset] @ save sp_usr
str lr, [\rd, #\offset + 4] @ save lr_usr
eor \rtemp, \rtemp, #(SVC_MODE ^ SYSTEM_MODE)
msr cpsr_c, \rtemp @ switch back to the SVC mode
.endm
*/
THUMB( store_user_sp_lr r8, r10, S_SP ) @ calling sp, lr
/*
arm处理器除了在用户模式和系统模式,其余模式下
都存在一个私有的SPSR保存状态寄存器,用来保存切换
到该模式之前arm核的执行状态。
此时,系统调用swi后,arm处于supervisor模式,spsr中保存
arm核在系统调用前的状态user,系统调用完成后,
arm会切换到用户模式,此时的cpsr寄存器就是spsr中保存的
状态。
r8中保存arm状态切换之前cpu的状态
*/
mrs r8, spsr @ called from non-FIQ mode, so ok.
/*
lr保存到栈上和之前不同在于保存到了pt_regs->pc,
而之前保存到pt_regs->lr
*/
str lr, [sp, #S_PC] @ Save calling PC
/*保存系统调用前的arm状态到栈上
此时进程还在用户态空间*/
str r8, [sp, #S_PSR] @ Save CPSR
str r0, [sp, #S_OLD_R0] @ Save OLD_R0
zero_fp
#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
/*
.macro get_thread_info, rd
mov \rd, sp, lsr #13
mov \rd, \rd, lsl #13
.endm
tsk = sp <<13 = thread_union->stack&0x2000
tsk = thread_info
union thread_union {
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
*/
get_thread_info tsk
/*
* Get the system call number.
*/
#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
USER( ldreq r10, [lr, #-4] ) @ get SWI instruction
#else
USER( 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
/* 取出系统调用号保存到scno 中
用户将系统调用号保存到r7寄存器
*/
addne scno, r7, #__NR_SYSCALL_BASE @ put OS number in
USER( ldreq scno, [lr, #-4] )
#else
/* Legacy ABI only. */
USER( ldr scno, [lr, #-4] ) @ get SWI instruction
#endif
/*
将sys_call_table的地址放入tbl
*/
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:
/* r10= thread_info->flags */
ldr r10, [tsk, #TI_FLAGS] @ check for syscall tracing
/*r4/r5入栈 sp=sp-4;sp=r5*/
stmdb sp!, {r4, r5} @ push fifth and sixth args
/* r10 _TIF_SYSCALL_WORK为是否为0*/
tst r10, #_TIF_SYSCALL_WORK @ are we tracing syscalls?
bne __sys_trace
cmp scno, #NR_syscalls @ check upper syscall limit
/*系统调用完成,返回到ret_fast_syscal
l会检查系统是否需要调度 */
adr lr, BSYM(ret_fast_syscall) @ return address
/*
tbl中保存sys_call_table 基地址。
scno中保存系统调用号。
此处真正跳转到系统调用中执行。
执行完后返回到ret_fast_syscall。
*/
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
#if defined(CONFIG_OABI_COMPAT) || !defined(CONFIG_AEABI)
/*
* We failed to handle a fault trying to access the page
* containing the swi instruction, but we're not really in a
* position to return -EFAULT. Instead, return back to the
* instruction and re-enter the user fault handling path trying
* to page it in. This will likely result in sending SEGV to the
* current task.
*/
9001:
sub lr, lr, #4
str lr, [sp, #S_PC]
b ret_fast_syscall
#endif
ENDPROC(vector_swi)
1.5 以线程创建为例,介绍一下系统调用的使用过程。
以下是标准c库中实现sys_clone系统调用的核心代码。
/* int clone(int (*fn)(void *arg), void *child_stack, int flags, void *arg,
pid_t *ptid, struct user_desc *tls, pid_t *ctid);
*/
/*
__clone (fct, STACK_VARIABLES_ARGS, clone_flags,
pd, &pd->tid, TLS_VALUE, &pd->tid);
1 用户态系统调用之前:r0=start_thread;r1=child_stack;r2=clone_flags
r3=struct pthread *pd ;r4=tid;r5=tls_value;r6=pd->tid
2 用户态系统调用后
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int, tls_val,
int __user *, child_tidptr)
用户态系统调用后,内核中:r0=clone_flags,r1=newsp;r2=parent_tidptr;r3=tls_val;
r4=child_tidptr
*/
.text
ENTRY(__clone)
@ sanity check args
cmp r0, #0 /* r0与0比较 r0=start_thread */
ite ne
cmpne r1, #0 /*stackaddr !=0*/
moveq r0, #-EINVAL //if (r0!=0){if(r1==0) r0=-EINVAL}
beq PLTJMP(syscall_error)
@ insert the args onto the new stack
/*
将r0=start_thread,r3=pd保存到子进程的栈上。
*/
str r3, [r1, #-4]! //r1-4=r3=pd;r1=r1-4
str r0, [r1, #-4]! //r1-4=r0=start_thread;r1=r1-4;此时child_sp=r0;child_sp+4=r3
@ do the system call
@ get flags
mov r0, r2 //r0=clone_flags
#ifdef RESET_PID
mov ip, r2 //ip=flags
#endif
@ new sp is already in r1 //r1=newsp=child_stack
/*
入栈:sp=sp-8;[sp]=r4=&pd->tid;[sp+4]=r7;
1 此时sp地址上保存的是pd->tid的地址
*/
push {r4, r7}
cfi_adjust_cfa_offset (8)
cfi_rel_offset (r4, 0)
cfi_rel_offset (r7, 4)
/*r2=*(sp+8)=parent_tidptr; 入参中对应r4*/
ldr r2, [sp, #8] ldr r3, [sp, #12]//r3=sp+12=tls_val 入参中对应r5
ldr r4, [sp, #16]//r4=sp+16=child_tidptr 入参中对应r6
ldr r7, =SYS_ify(clone) //r7=syscall id
/*
系统调用之前:
r0=clone_flags
r1=child_sp:child_sp=start_thread;child_sp+4=pd
r5=tls_value
r7=syscallId
*/
swi 0x0 //软中断嵌入内核
cfi_endproc
cmp r0, #0 //r0=0 fork成功了,当前在子进程中
beq 1f //跳转到start_thread,pthread_create中注册的回调函数。
pop {r4, r7}
blt PLTJMP(C_SYMBOL_NAME(__syscall_error))
RETINSTR(, lr) //bx lr
cfi_startproc
PSEUDO_END (__clone)
1:
.fnstart
.cantunwind
@ pick the function arg and call address off the stack and execute
//child_sp+4=r3=pd
ldr r0, [sp, #4]
//执行start_thread ip=sp;sp=sp+8
ldr ip, [sp], #8
BLX (ip)
@ and we are done, passing the return value through r0
b PLTJMP(HIDDEN_JUMPTARGET(_exit))
.fnend
1.6 系统调用的返回处理,sysc_clone为例:
第一:系统调用时,内核保存用户进程的栈。vector_swi保存用户态进程的上下文信息到栈上:可以参考arm异常处理中对系统调用的介绍。
第二:系统调用返回时,恢复用户进程的栈。
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 )
1.7 fast_work_pending->do_work_pending 跳转到c函数,do_work_pending中发生调度。
fast_work_pending:
str r0, [sp, #S_R0+S_OFF]! @ returned r0
work_pending:
mov r0, sp @ 'regs'
mov r2, why @ 'syscall'
/* 跳转到c语言函数,wake_up中唤醒的进程,在此被调度执行 */
bl do_work_pending
cmp r0, #0
beq no_work_pending
movlt scno, #(__NR_restart_syscall - __NR_SYSCALL_BASE)
ldmia sp, {r0 - r6} @ have to reload r0 - r6
b local_restart @ ... and off we go
[aarch64]arm 64bit系统调用
1 arm32相比的指令差异
64bit下aarch64无push指令,宏定义如下:
*.macro push, xreg1, xreg2 //压栈两个寄存器
*stp \xreg1, \xreg2, [sp, #-16]! //注意!!!push指令也改变sp的值!!!
*.endm
其中:
stp \xreg1, \xreg2, [sp, #-16]!
!表示先修改寄存器,再使用:sp=sp-16; [sp]=reg1; [sp+8]=reg2.
ldp x19, x20, [sp], #0x10 表示使用完sp再加0x10:x19=[sp]; x20=[sp+8]; sp=sp+0x10;
在AArch64运行态下,在所有的异常级下,都可以访问31个64位通用寄存器,它们的命名是X0~X30.
这31个64位寄存器也可以通过W0~W30来访问低32位.
读Wn时,不会影响到高32位的值。写Wn时,会将高32位全部清0.
2 系统调用流程
实现代码位置:arch/arm64/kernel/entry.s
代码流程:
1) ENTRY(vectors)
ventry el0_sync
END(vectors)
其中aarch64架构下:el0模式主要由用户程序使用;el1模式主要linux kernel使用;
可参考: ARMv8 架构与指令集.学习笔记
2) el0_sync:
kernel_entry 0 //保存用户栈
b.eqel0_svc //真正实现系统调用
kernel_entry可参考:ARMv8 Linux内核异常处理过程分析
[总结]
本文介绍了linux内核态系统调用过程,并举了一个简单实例。这一部分可以参照以下两篇博文:
vsyscall和vDSO是用于加速某些系统调用的两种机制。
ARM64 汇编——寄存器和指令 https://www.jianshu.com/p/2f4a5f74ac7a
认识ARM64汇编:https://www.jianshu.com/p/99067af33f14
ARM64 基本的汇编语法:https://winddoing.github.io/post/5543.html