您的位置:首页 > 运维架构 > Linux

基于arm的linux系统调用分析

2017-09-25 15:44 232 查看

1系统调用的作用

linux内核中设置了一组用于实现各种系统功能的子程序,称为系统调用,最简单的如read,write,open等等。用户可以通过系统调用命令在自己的应用程序上调用他们,从某种角度来看,系统调用和普通函数调用非常相似。区别在于系统调用由操作系统核心提供,运行在内核态,而普通函数调用由函数库或用户自己提供,运行于用户态。实际上,很多已经被我们习以为常的C语言标准函数,在linux平台上的实现都是靠系统调用来完成的,所以如果想对系统底层原理作深入的了解,就得掌握各种系统调用时初步的要求。

2 系统调用是如何工作的

我们知道一般进程是不能访问内核的,系统调用时用户空间访问内核空间唯一合法的途径。早期linux采用OABI方式传递系统调用的number。现在linux采用新的EABI(Embedded)方式传递系统调用的number。我们主要了解EABI的方式,关于OABI的方式这里就不说了。现在的android编译器arm-linux-androideabi-gcc使用的就是EABI方式。这种新的系统调用方式流程如下:

1.将syscall number 存储到r7寄存器中。

2. 执行swi指令跳转到软中断处执行,并从r7中获取syscall number。

注意这里提到的软中断是 supervisor call exception(svc),由于原来名字是software interrupt(swi)一直沿用软中断的叫法。

如果应用程序中有read函数,系统调用里也有read函数,如何从应用程序中找到内核空间中的read函数呢,需要使用swi指令,内核中有很多系统调用函数,read,write,open等等,内核如何知道调用的是哪个函数呢,这里需要用到一个寄存器r7,这个寄存器我们之后还会谈到。

在内核源码中,arch\arm\kernel中有一个文件entry-common.S汇编代码,这段代码是应用程序访问内核的接口,下面给出部分代码。

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
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
/*
* Get the system call number.
*/

#if defined(CONFIG_OABI_COMPAT)
/*这里采用的是OABI的方式
* 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
A710( and ip, r10, #0x0f000000        @ check for SWI     )
A710( teq ip, #0x0f000000                     )
A710( bne .Larm710bug                     )
#endif
#ifdef CONFIG_CPU_ENDIAN_BE8
rev r10, r10            @ little endian instruction
#endif

#elif defined(CONFIG_AEABI)
/*这里采用EABI的方式
* Pure EABI user space always put syscall number into scno (r7).
*/
A710( ldr ip, [lr, #-4]           @ get SWI instruction   )
A710( and ip, ip, #0x0f000000     @ check for SWI     )
A710( teq ip, #0x0f000000                     )
A710( bne .Larm710bug                     )

#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
A710( and ip, scno, #0x0f000000       @ check for SWI     )
A710( teq ip, #0x0f000000                     )
A710( bne .Larm710bug                     )

#endif


在用EABI的方式中,我们可以看出应用系统空间把系统调用编号放在r7寄存器中,如下图



内核需要拿到这个编号,将r7寄存器的值取出来放到scno中。

程序中有句ldrcc pc, [tbl, scno, lsl #2],我们暂且不看其他代码部分,

这句表明内核通过scno取出r7寄存器中的syscall number,

1 该指令将scno逻辑左移2位加到tbl上,把结果位置处所存储的值赋值给PC。 PC = *(tbl+(scno<<2))。

2 tbl 是系统调用table的入口地址,可以把tbl看作char数组头。char tbl[] = {…….};

3 因为scno只是第几个系统调用的意思,不是系统调用的实际地址或者相对地址。

4 而实际每一个系统调用的地址(也就是形象理解的函数名),是 .long x 32位,需要四个字节存储。

5 scno逻辑左移2位,意思为在tbl上偏移scno<<2个字节的位置上,存储着所需的系统调用函数的入口地址。tbl[scno<<2] 存储着所需的系统调用函数的入口地址。

在entry-common.S中调用了sys_call_table,其实就是calls.S的汇编代码,保存着各个系统调用的编号, 内核代码通过这个编号作为偏移来找到相应的系统调用。calls.S部分代码如下,系统调用从0开始,第一个系统调用是sys_reatart_syscall,第二个是sys_exit,当然这些代码是专门由linux内核编写者定义的,如果我们想自己添加自己的系统调用函数,就在这些代码的末尾逐个添加,格式也一般都是相同的(sys_函数名)。例如当你编写一个函数名为mem的时候,在更新calls.S的时候,只要在代码中顺序添加CALL(sys_mem)即可。

/* 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)


在unistd.h文件中,系统调用程序编写基本上靠这个头文件,在每次添加一个系统调用函数的时候,都要更新一次unistd.h和calls.S文件。其中系统调用编号也和call.S中的编号一一对应。代码部分如下,同样也是按照代码中顺序添加我们想要加入的函数名,一般来说格式都为—_NR函数名。

这里在最后需要手动添加#define __NR_mem。

/*
* 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)


到此,linux的系统调用大概过程就是这样子,具体的笔者并没有去深入,不过现在先理解这么多就可以了,以后随着学习的深入再继续努力。

参考资料:

ARM Linux系统调用详细分析

http://blog.csdn.net/liduxun/article/details/48119849#
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  arm linux 面试