本文学习基于armv7和armv8体系的linux系统调用机制,linux内核版本为3.10.79。通过分析系统调用机制和源代码来展示系统调用过程。
linux内核中设置了一组用于实现系统功能的子程序,称为系统调用。系统调用和普通库函数调用很相似,仅仅是系统调用由操作系统核心提供,执行于核心态。而普通的函数调用由函数库或用户自己提供。执行于用户态。
通常系统调用过程如下:
用户程序------>C库(即API):软中断 ----->system_call------->系统调用服务函数------->内核程序。比如, 用户程序打开一个文件------>C库open:中断----->system_call------->sys_open------>内核程序------>返回到用户程序。
注:图片来自网络
应用编程接口(API)与系统调用的不同在于,前者只是一个函数定义,说明了如何获得一个给定的服务,而后者是通过软件中断向内核发出的一个明确的请求。POSIX标准针对API,而不针对系统调用。Unix系统给程序员提供了很多API库函数。libc的标准c库所定义的一些API引用了封装例程(wrapper routine)(其唯一目的就是发布系统调用)。通常情况下,每个系统调用对应一个封装例程,而封装例程定义了应用程序使用的API。反之则不然,一个API没必要对应一个特定的系统调用。从编程者的观点看,API和系统调用之间的差别是没有关系的:唯一相关的事情就是函数名、参数类型及返回代码的含义。然而,从内核设计者的观点看,这种差别确实有关系,因为系统调用属于内核,而用户态的库函数不属于内核。
用户空间的程序无法直接执行内核代码。它们不能直接调用内核空间中的函数,因为内核驻留在受保护的地址空间上。如果进程可以直接在内核的地址空间上读写的话,系统安全就会失去控制。所以,应用程序应该以某种方式通知系统,告诉内核自己需要执行一个系统调用,希望系统切换到内核态,这样内核就可以代表应用程序来执行该系统调用了。
系统调用在用户空间进程和硬件设备之间加入了一个中间层。该层主要作用有三个:
(1) 它为用户空间提供了一种统一的硬件的抽象接口。
比方当须要读些文件的时候,应用程序就能够不去管磁盘类型和介质,甚至不用去管文件所在的文件系统究竟是哪种类型。
(2)系统调用保证了系统的稳定和安全。作为硬件设备和应用程序之间的中间人,内核能够基于权限和其它一些规则对须要进行的访问进行裁决。
举例来说,这样能够避免应用程序不对地使用硬件设备,窃取其它进程的资源,或做出其它什么危害系统的事情。
(3) 每一个进程都执行在虚拟系统中,而在用户空间和系统的其余部分提供这样一层公共接口,也是出于这样的考虑。假设应用程序能够任意访问硬件而内核又对此一无所知的话,就没法实现多任务和虚拟内存,当然也不可能实现良好的稳定性和安全性。在Linux中。系统调用是用户空间访问内核的惟一手段。除异常和中断外,它们是内核惟一的合法入口。
应用编程接口(API)内部实现向内核切换的机制。首先,API为系统调用设置参数,其中一个参数是系统调用编号,其它参数为API接口参数。参数设置完成后,程序执行“系统调用”指令。这个指令会导致一个异常使处理器切换到内核态并跳转到一个新的地址,并开始执行那里的异常处理程序。此时的异常处理程序实际上就是系统调用处理程序,它与硬件体系结构紧密相关,异常处理程序会根据系统调用编号跳转到系统调用函数地址,执行系统调用函数。
系统调用号来标识系统调用函数,区分不同调用函数,系统调用号是唯一的,独一无二的。用户态向内核态切换方式都是一样的,所以必须通过这个系统调用号作为参数,使内核能够识别用户态想调用哪个系统调用函数。
文件/arch/arm/kernel/calls.S中定义了系统调用表,文件\arch\arm\include\uapi\asm\unistd.h中定义了系统调用号,编号从0开始,arm 32位系统中一个表项占用4个字节,地址为基地址+编号*4(编号左移2位),另外定义了编号的上限,当编号不在范围内时,调用,直接返回错误号。
系统调用号:
\arch\arm\include\uapi\asm\unistd.h
#if defined(__thumb__) || defined(__ARM_EABI__)
#define __NR_SYSCALL_BASE 0
#else
#define __NR_SYSCALL_BASE __NR_OABI_SYSCALL_BASE
#endif
/*
* This file contains the system call numbers.
*/
#define __NR_restart_syscall (__NR_SYSCALL_BASE+ 0)
#define __NR_exit (__NR_SYSCALL_BASE+ 1)
#define __NR_fork (__NR_SYSCALL_BASE+ 2)
#define __NR_read (__NR_SYSCALL_BASE+ 3)
#define __NR_write (__NR_SYSCALL_BASE+ 4)
#define __NR_open (__NR_SYSCALL_BASE+ 5)
#define __NR_close (__NR_SYSCALL_BASE+ 6)
/arch/arm/kernel/calls.S
/* 0 */ CALL(sys_restart_syscall)
CALL(sys_exit)
CALL(sys_fork_wrapper)
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_wrapper)
CALL(sys_chdir)
CALL(OBSOLETE(sys_time)) /* used by libc4 */
CALL(sys_mknod)
文件:arch\arm64\kernel\syc.c, arch\arm64\include\asm\ unistd.h,
arch\arm64\include\upai\asm\ unistd.h, include\asm-generic\ unistd.h,
include\upai\\asm-generic\ unistd.h
syc.c中:定义了sys_call_table,包含了几个头文件unistd.h,最终\upai\\asm-generic\ unistd.h添加了其它系统调用。
#define __SYSCALL(nr, sym) [nr] = sym,
/*
* The sys_call_table array must be 4K aligned to be accessible from
* kernel/entry.S.
*/
注意这里数组初始化用法,第一次见。
void *sys_call_table[__NR_syscalls] __aligned(4096) = {
[0 ... __NR_syscalls - 1] = sys_ni_syscall,
#include <asm/unistd.h>
};
include\upai\\asm-generic\ unistd.h中:
/* fs/read_write.c */
#define __NR3264_lseek 62
__SC_3264(__NR3264_lseek, sys_llseek, sys_lseek)
#define __NR_read 63
__SYSCALL(__NR_read, sys_read) //相当于[nr] = sym, [__NR_read] = sys_read
#define __NR_write 64
__SYSCALL(__NR_write, sys_write)
#define __NR_readv 65
__SC_COMP(__NR_readv, sys_readv, compat_sys_readv)
#define __NR_writev 66
__SC_COMP(__NR_writev, sys_writev, compat_sys_writev)
#define __NR_pread64 67
__SC_COMP(__NR_pread64, sys_pread64, compat_sys_pread64)
#define __NR_pwrite64 68
__SC_COMP(__NR_pwrite64, sys_pwrite64, compat_sys_pwrite64)
#define __NR_preadv 69
__SC_COMP(__NR_preadv, sys_preadv, compat_sys_preadv)
#define __NR_pwritev 70
__SC_COMP(__NR_pwritev, sys_pwritev, compat_sys_pwritev)
API接口中有参数,如ssize_t write (int fd, const void * buf, size_t count); 参数是通过寄存器传递到内核的。
armv7:通过寄存器r1-r5传递函数参数, r0存放函数返回值,r7存放系统调用号
armv8 aarch64:通过寄存器x1-x6传递函数参数,x0存放函数返回值,x8存放系统调用号。
文件:glibc中\sysdeps\unix\sysv\linux\arm\sysdep.h, \sysdeps\unix\ sysdep.h。
后面系统调用举例章节会详细描述。
用户态通过执行中断指令切换到内核态,
armv7:swi 0
armv8 aarch64:svc 0
文件:glibc中\sysdeps\unix\sysv\linux\arm\sysdep.h, \sysdeps\unix\ sysdep.h
后面系统调用举例章节会详细描述。
文件:linux/arch/arm/kernel/ entry-common.S
分析主要代码,vector_swi为swi中断处理函数,swi指令后,cpu会自动跳转到中断向量进行处理。关于中断,后续中断学习中再解析。
- 中断向量入口函数
ENTRY(vector_swi)
//保存栈信息和状态寄存器
sub sp, sp, #S_FRAME_SIZE
stmia sp, {r0 - r12} @ Calling r0 - r12
ARM( add r8, sp, #S_PC )
ARM( stmdb r8, {sp, lr}^ ) @ Calling sp, lr//这里^表示被备份的sp lr 是usr_mode的寄存器不是目前所在svc_mode的寄存器
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
//把系统调用表指针存在tbl中
adr tbl, sys_call_table @ load syscall table pointer
#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 //把r7中的中断号存储到scno变量中
USER( ldreq scno, [lr, #-4] )
local_restart:
ldr r10, [tsk, #TI_FLAGS] @ check for syscall tracing
stmdb sp!, {r4, r5} @ push fifth and sixth args //把参数所在寄存器r4,r5入栈,因为ATPCS,参数传递只能使用r0-r3,所以r4,r5通过栈传递
cmp scno, #NR_syscalls @ check upper syscall limit //检查是否超过了最大调用号
adr lr, BSYM(ret_fast_syscall) @ return address //设置返回地址到lr
ldrcc pc, [tbl, scno, lsl #2] @ call sys_* routine // scno小于NR_syscalls执行此指令,跳转到系统调用函数地址,tbl+scno*4,系统调用表首地址+调用号*4,每个指针占4字节
// scno大于等于NR_syscalls执行如下指令
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)
只解析64位运行模式。
文件arch\arm64\kernel\ entry.S:
中断向量表:
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 // FIQ EL1h
ventry el1_error_invalid // Error EL1h
ventry el0_sync // Synchronous 64-bit EL0 //svc为同步异常,中断向量处理,来自el0
ventry el0_irq // IRQ 64-bit EL0
ventry el0_fiq // FIQ 64-bit EL0
ventry el0_error_invalid // Error 64-bit EL0
…
END(vectors)
//同步异常处理函数,svc指令,data abort, instruct abort等都是同步异常,这里需要判断哪种异常,关于中断,详细分析看中断学习笔记。
el0_sync:
kernel_entry 0 //保存寄存器,状态
msr daifclr, #1 //enable fiq
mrs x25, esr_el1 // read the syndrome register
lsr x24, x25, #ESR_EL1_EC_SHIFT // exception class
cmp x24, #ESR_EL1_EC_SVC64 // SVC in 64-bit state
b.eq el0_svc
/*
* SVC handler.
*/
.align 6
el0_svc:
adrp stbl, sys_call_table // load syscall table pointer 系统调用表
uxtw scno, w8 // syscall number in w8 系统调用号X8寄存器
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
disable_step x16
isb
enable_dbg
enable_irq
get_thread_info tsk
ldr x16, [tsk, #TI_FLAGS] // check for syscall hooks
tst x16, #_TIF_SYSCALL_WORK
b.ne __sys_trace
adr lr, ret_fast_syscall // return address 返回地址
cmp scno, sc_nr // check upper syscall limit
b.hs ni_sys
ldr x16, [stbl, scno, lsl #3] // address in the syscall table 计算系统调用函数地址,基地址+系统调用号*8, 64位是8字节存放指针长度
br x16 // call sys_* routine //跳转到系统调用
ni_sys:
mov x0, sp
b do_ni_syscall //系统调用号错误处理函数
ENDPROC(el0_svc)
Armv7系统函数调用表在calls.S中,以sys_开头,具体定义方式在SYSCALL_DEFINE进行定义,举例如下,通过SYSCALL_DEFINE3定义,这里不在介绍具体定义方式。Armv8参考章节5.1。
函数执行完返回执行结果。
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
struct fd f = fdget(fd);
ssize_t ret = -EBADF;
if (f.file) {
loff_t pos = file_pos_read(f.file);
ret = vfs_read(f.file, buf, count, &pos);
file_pos_write(f.file, pos);
fdput(f);
}
return ret;
}
系统调用函数返回结果一般成功是0,失败返回一个负值,并且把错误码放到全局变量errno中。返回值放在r0寄存器中。
文件:linux/arch/arm/kernel/ entry-common.S
返回时执行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 )
.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
文件arch\arm64\kernel\ entry.S:系统调用返回时执行ret_fast_syscall。
ret_fast_syscall:
disable_irq // disable interrupts
ldr x1, [tsk, #TI_FLAGS]
and x2, x1, #_TIF_WORK_MASK
cbnz x2, fast_work_pending
tbz x1, #TIF_SINGLESTEP, fast_exit
disable_dbg
enable_step x2
fast_exit:
kernel_exit 0, ret = 1
.macro kernel_exit, el, ret = 0
ldp x21, x22, [sp, #S_PC] // load ELR, SPSR // ELR为返回用户空间,执行的地址
.if \el == 0
ldr x23, [sp, #S_SP] // load return stack pointer
#ifdef CONFIG_ARM64_ERRATUM_845719
tbz x22, #4, 1f
#ifdef CONFIG_PID_IN_CONTEXTIDR
mrs x29, contextidr_el1
msr contextidr_el1, x29
1:
#else
msr contextidr_el1, xzr
1:
#endif
#endif
.endif
.if \ret
ldr x1, [sp, #S_X1] // preserve x0 (syscall return)
add sp, sp, S_X2
.else
pop x0, x1
.endif
pop x2, x3 // load the rest of the registers
pop x4, x5
pop x6, x7
pop x8, x9
msr elr_el1, x21 // set up the return data
msr spsr_el1, x22
.if \el == 0
msr sp_el0, x23
.endif
pop x10, x11
pop x12, x13
pop x14, x15
pop x16, x17
pop x18, x19
pop x20, x21
pop x22, x23
pop x24, x25
pop x26, x27
pop x28, x29
ldr lr, [sp], #S_FRAME_SIZE - S_LR // load LR and restore SP //LR为在用户空间函数跳转时保存的地址。而ELR是异常发生切换时,保存的地址。
eret // return to kernel
.endm
以write为例,分析调用过程。
用户程序调用write();
ssize_t write (int fd, const void * buf, size_t count);
文件:glibc中\sysdeps\unix\sysv\linux\arm\sysdep.h, \sysdeps\unix\ sysdep.h
\sysdeps\unix\sysv\linux\arm\sysdep.h,write.c
- Write->__write(__libc_write)-> SYSCALL_CANCEL
ssize_t
__libc_write (int fd, const void *buf, size_t nbytes)
{
return SYSCALL_CANCEL (write, fd, buf, nbytes); //3个参数转化为4个,添加了函数名write
}
libc_hidden_def (__libc_write)
weak_alias (__libc_write, __write)
- SYSCALL_CANCEL-> INLINE_SYSCALL_CALL->__INLINE_SYSCALL4-> INLINE_SYSCALL
INLINE_SYSCALL_CALL宏定义会根据参数个数转换为_INLINE_SYSCALL4
#define SYSCALL_CANCEL(...) \
({ \
long int sc_ret; \
if (SINGLE_THREAD_P) \
sc_ret = INLINE_SYSCALL_CALL (__VA_ARGS__); \
else \
{ \
int sc_cancel_oldtype = LIBC_CANCEL_ASYNC (); \
sc_ret = INLINE_SYSCALL_CALL (__VA_ARGS__); \
LIBC_CANCEL_RESET (sc_cancel_oldtype); \
} \
sc_ret; \
})
#define __INLINE_SYSCALL4(name, a1, a2, a3, a4) \
INLINE_SYSCALL (name, 4, a1, a2, a3, a4)
#define __INLINE_SYSCALL_NARGS_X(a,b,c,d,e,f,g,h,n,...) n
#define __INLINE_SYSCALL_NARGS(...) \
__INLINE_SYSCALL_NARGS_X (__VA_ARGS__,7,6,5,4,3,2,1,0,)
#define __INLINE_SYSCALL_DISP(b,...) \
__SYSCALL_CONCAT (b,__INLINE_SYSCALL_NARGS(__VA_ARGS__))(__VA_ARGS__) //转换,如果有4个参数,那么__VA_ARGS__占用a,b,c 那n的位置正好是3,所以__INLINE_SYSCALL_NARGS_X(a,b,c,d,e,f,g,h,n,...) n 计算后为(write, fd, buf, nbytes,7,6,5,4, 3,2,1,0,...) 4
#define INLINE_SYSCALL_CALL(...) \
__INLINE_SYSCALL_DISP (__INLINE_SYSCALL, __VA_ARGS__)
- INLINE_SYSCALL-> INTERNAL_SYSCALL-> INTERNAL_SYSCALL_RAW
#define INLINE_SYSCALL(name, nr, args...) \
({ unsigned int _sys_result = INTERNAL_SYSCALL (name, , nr, args); \
#define INTERNAL_SYSCALL(name, err, nr, args...) \
INTERNAL_SYSCALL_RAW(SYS_ify(name), err, nr, args) // SYS_ify计算系统调用号
- # define INTERNAL_SYSCALL_RAW(name, err, nr, args...) \
({ \
register int _a1 asm ("r0"), _nr asm ("r7"); \//定义变量_a1,_nr存放在寄存器r0和r7中,
LOAD_ARGS_##nr (args) \ //加载参数args寄存器中,nr为个数
_nr = name; \ //存放系统号
asm volatile ("swi 0x0 @ syscall " #name \//swi指令,切换到内核
: "=r" (_a1) \ //输出,返回值放在r0
: "r" (_nr) ASM_ARGS_##nr \ //变量参数
: "memory"); \ //告诉编译器,这段内嵌式汇编代码,所有变量通过内存获取或存储,不使用缓存
_a1; })
接口和调用过程一样,只是最后中断指令和使用寄存器不同,armv8如下,
# define INTERNAL_SYSCALL_RAW(name, err, nr, args...) \
({ long _sys_result; \
{ \
LOAD_ARGS_##nr (args) \//参数存储在x1-x5
register long _x8 asm ("x8") = (name); \//x8存储系统调用号
asm volatile ("svc 0 // syscall " # name \//中断指令svc,切换到内核
: "=r" (_x0) : "r"(_x8) ASM_ARGS_##nr : "memory"); \
_sys_result = _x0; \
} \
_sys_result; })
内核空间执行,前面已经分析了,
中断后,cpu跳转到中断向量处理函数vector_swi,之后查找系统调用表,跳转到asmlinkage long sys_write(unsigned int fd, const char __user *buf,
size_t count);
执行完,返回用户空间。
Armv8类似,前面章节已分析。
系统调用设计原则保持函数通用性,简洁性,可移植性,健壮性。
放在相关功能的.c文件中,如文件相关的在fs/read_write.c等
SYSCALL_DEFINE3(write_example, unsigned int, fd, const char __user *, buf,
size_t, count)
{
……
…….
return ret;
}
SYSCALL_DEFINE 使用与参数个数有关,n个参数就使用SYSCALL_DEFINEn,
SYSCALL_DEFINEn(xxx)中第一个参数为函数名,后面是函数参数内容,这里参数类型和变量用逗号隔开。
在相关.h文件添加声明,通用函数一般在kernel\linux-3.10.y\include\linux\syscalls.h中添加声明,与体系相关的对应体系目录下syscalls.h文件中添加声明
asmlinkage long sys_ write_example (unsigned int fd, const char __user *buf,
size_t count);
在体系相关头文件unistd.h添加系统调用号
如armv7:\arch\arm\include\uapi\asm\unistd.h 最后添加系统调用号
#define __NR_finit_module (__NR_SYSCALL_BASE+379)
#define __NR_seccomp (__NR_SYSCALL_BASE+383)
#define __NR_ write_example (__NR_SYSCALL_BASE+384)
在体系相关文件中添加系统调用表
如armv7:/arch/arm/kernel/calls.S
/* 380 */ CALL(sys_ni_syscall) /* reserved sys_sched_setattr */
CALL(sys_ni_syscall) /* reserved sys_sched_getattr */
CALL(sys_ni_syscall) /* reserved sys_renameat2 */
CALL(sys_seccomp)
CALL(sys_ write_example)
至此系统调用添加完成;
一种方法是添加到glibc库中,作为一个库函数,添加方式参考章节6.另一种方式使用库函数syscall函数直接调用,如ret = syscall(系统调用号,参数….)