《linux系统调用的来龙去脉》分为上下两篇,本文为下篇。 1.LINUX系统调用实现linux系统调用分为3个部分:调用请求 ,响应请求 ,功能实现。
2.调用请求linux系统调用的第一部分是调用请求,调用请求作为系统调用提供给应用程序的接口,在linux系统调用的3部分中,应用程序只有使用调用请求的权限(其它两部分应用程序无法接触到),调用请求需要完成以下两个基本功能: 2.1接口函数linux系统调用有四种调用请求方式: 以上这4种方法,每种方法的底层都会用到触发软中断指令。 1、使用 glibc 库函数 glibc是GNU 发布的开源的标准 C 库,glibc 为程序员提供丰富的 API(Application Programming Interface),除了字符串处理、数学运算等用户态服务之外,重要的是封装了操作系统提供的系统服务,即系统调用的封装(本质上还是执行的系统调用)。 系统调用和glibc提供的API的对应关系如下:
#include<stdio.h>int main(){ int rc=chmod('./weiwei',0666); if(rc==-1) perror('chmod fail\n'); else printf('chmod succeed\n'); return 0;} chmod函数用于改变文件weiwei的属性,编译程序得到chmod.o执行文件,运行chmod.o后观察weiwei文件的属性,结果如下:
任何事物都有两面性,如果 glibc 没有封装某个内核提供的系统调用时,我们就没办法通过使用glibc的方法来调用该系统调用。假设我们自己通过编译内核增加了一个系统调用,这时 glibc 不可能有我们自己新增系统调用的封装 API。 在这种情况下,可以利用 glibc 提供的syscall 库函数直接调用。syscall是一个通过特定子功能号和特定参数调用汇编语言接口的库函数。该函数定义在 unistd.h 头文件中,函数原型如下: long int syscall (long int sysno, ...) sysno :为系统调用号,每个系统调用都有唯一的系统调用号来标识。 例如应用程序通过 syscall函数来改变文件的属性,示例代码如下: #include<stdio.h>#include<unistd.h>//syscall接口函数声明在头文件unistd.h中#include<sys/syscall.h>//SYS_chmod在头文件syscall.h中int main(){ int rc=syscall(SYS_chmod,'./weiwei',0777); if(rc==-1) perror('SYS_chmod chmod fail\n'); else printf('SYS_chmod chmod succeed\n'); return 0;} syscall(SYS_chmod,“./weiwei”,0777)函数用于改变文件weiwei的属性,编译得到syschmod.o执行文件,运行syschmod.o后观察weiwei文件的属性,结果如下:
_syscall0(),_syscall1(),_syscall2(),_syscall3(),_syscall4(),_syscall5(),_syscall6()。 宏名称字符串“syscall0”中的0表示无参数,“syscall1”中的1表示带1个参数,“syscall2”中的2表示带2个参数。如果系统调用带有1个参数,那么就应该使用宏_syscall1()。 以_syscall0()为例,_syscall0()为不带参数的系统调用宏函数,它以嵌入汇编的形式调用软中断指令int 0x80。 #define _syscall0(type,name) \type name(void) \ { \ long __res; \ __asm__ volatile ('int $0x80' \ // 调用系统中断0x80。 : '=a' (__res) \ // 返回值eax(__res)。 : '0' (__NR_##name)); \ // 输入为系统中断调用号__NR_name。 if (__res >= 0) \ // 如果返回值>=0,则直接返回该值。 return (type) __res; \ errno = -__res; \ // 否则置出错号,并返回-1。 return -1; \ } 假设我们要新增一个名为test系统调用,我们可以使用_syscall系统调用宏实现,假设test系统调用参数个数为0个,我们通过_syscall0(int, test)申请了一个新的系统调用。 static inline int test(void){long __res;__asm__ volatile ('int $0x80' : '=a' (__res) : '0' (__NR_test));if (__res >= 0) return (int) __res;errno = -__res;return -1;} _syscall0(int, test)实际上是声明了一个名为test的函数,声明函数后,应用程序就可以使用test这个系统调用了(当然还有内核相关代码需要实现),test系统调用示例代码如下: #include<stdio.h>#include<unistd.h>//syscall接口函数声明在头文件unistd.h中#include<sys/syscall.h>//SYS_chmod在头文件syscall.h中_syscall0(int, test);int main(){ int rc=test(); if(rc==-1) perror('test fail\n'); else printf('test succeed\n'); return 0;} 4、通过软中断指令陷入 用户态程序通过软中断指令int 0x80 来陷入内核态,通过寄存器传递参数,eax 传递的是系统调用号,ebx、ecx、edx、esi和edi 来依次传递参数,当系统调用返回值存放在 eax 中。 以调用chmod修改文件属性为例,系统调用通过内联汇编实现,示例代码如下: #include <stdio.h>#include <errno.h>#include <sys/syscall.h>//SYS_chmod在头文件syscall.h中#include <sys/types.h>int main(){ long rc; unsigned short mode = 0777; char *file_name = './weiwei'; /*内联汇编 软中断指令 */ asm( 'int $0x80' : '=a' (rc) : '0' (SYS_chmod), 'b' ((long)file_name), 'c' ((long)mode) ); if (rc == -1) perror('SYS_chmod chmod fail\n'); else printf('SYS_chmod chmod succeed\n'); return 0;} 2.2触发软中断以上这4种方法,每种方法的底层都会用到触发软中断指令。系统调用的参数 由各通用寄存器传递,然后执行软中断指令(INT 0x80) ,处理器响应中断并开始执行中断服务程序,此时处理器模式变为特权模式。我们以glibc-2.23中的X86体系为例,下面分别讲述4中方法如何调用软中断指令。 1、glibc 库函数 glibc-2.23\sysdeps\unix\sysv\linux\generic中有chmod函数的定义,代码如下: #include <errno.h>#include <stddef.h>#include <fcntl.h>#include <sys/stat.h>#include <sys/types.h>/* Change the protections of FILE to MODE. */int__chmod (const char *file, mode_t mode){ return INLINE_SYSCALL (fchmodat, 3, AT_FDCWD, file, mode);}weak_alias (__chmod, chmod) INLINE_SYSCALL 宏在glibc-2.23\glibc-2.23\sysdeps\i386\sysdep.h中定义,代码如下: # define INLINE_SYSCALL(name, nr, args...) \ ({ \ unsigned int resultvar = INTERNAL_SYSCALL (name, , nr, args); \ __glibc_unlikely (INTERNAL_SYSCALL_ERROR_P (resultvar, )) \ ? __syscall_error (-INTERNAL_SYSCALL_ERRNO (resultvar, )) \ : (int) resultvar; }) INTERNAL_SYSCALL 宏在glibc-2.23\glibc-2.23\sysdeps\i386\sysdep.h中定义,代码如下: #define INTERNAL_SYSCALL(name, err, nr, args...) \ ({ \ register unsigned int resultvar; \ INTERNAL_SYSCALL_MAIN_##nr (name, err, args); \ (int) resultvar; }) INTERNAL_SYSCALL_MAIN_##nr宏的几种形式在glibc-2.23\glibc-2.23\sysdeps\i386\sysdep.h中定义如下: #define INTERNAL_SYSCALL_MAIN_0(name, err, args...) \ INTERNAL_SYSCALL_MAIN_INLINE(name, err, 0, args)#define INTERNAL_SYSCALL_MAIN_1(name, err, args...) \ INTERNAL_SYSCALL_MAIN_INLINE(name, err, 1, args)#define INTERNAL_SYSCALL_MAIN_2(name, err, args...) \ INTERNAL_SYSCALL_MAIN_INLINE(name, err, 2, args)#define INTERNAL_SYSCALL_MAIN_3(name, err, args...) \ INTERNAL_SYSCALL_MAIN_INLINE(name, err, 3, args)#define INTERNAL_SYSCALL_MAIN_4(name, err, args...) \ INTERNAL_SYSCALL_MAIN_INLINE(name, err, 4, args)#define INTERNAL_SYSCALL_MAIN_5(name, err, args...) \ INTERNAL_SYSCALL_MAIN_INLINE(name, err, 5, args)
# define INTERNAL_SYSCALL_MAIN_INLINE(name, err, nr, args...) \ LOADREGS_##nr(args) \ asm volatile ( \ 'int $0x80' \ : '=a' (resultvar) \ : 'a' (__NR_##name) ASMARGS_##nr(args) : 'memory', 'cc') chmod库函数的定义最终通过int $0x80 触发软中断。 2、 syscall函数 glibc-2.23\glibc-2.23\sysdeps\unix\sysv\linux\hppa\syscall.c中的syscall函数定义如下: long intsyscall (long int __sysno, ...){ /* FIXME: Keep this matching INLINE_SYSCALL for hppa */ va_list args; long int arg0, arg1, arg2, arg3, arg4, arg5; long int __sys_res; /* Load varargs */ va_start (args, __sysno); arg0 = va_arg (args, long int); arg1 = va_arg (args, long int); arg2 = va_arg (args, long int); arg3 = va_arg (args, long int); arg4 = va_arg (args, long int); arg5 = va_arg (args, long int); va_end (args); { LOAD_ARGS_6 (arg0, arg1, arg2, arg3, arg4, arg5) register unsigned long int __res asm('r28'); PIC_REG_DEF LOAD_REGS_6 asm volatile (SAVE_ASM_PIC ' ble 0x100(%%sr2, %%r0) \n' ' copy %1, %%r20 \n' LOAD_ASM_PIC : '=r' (__res) : 'r' (__sysno) PIC_REG_USE ASM_ARGS_6 : 'memory', CALL_CLOB_REGS CLOB_ARGS_6); __sys_res = __res; } if ((unsigned long int) __sys_res >= (unsigned long int) -4095) { __set_errno (-__sys_res); __sys_res = -1; } return __sys_res;} 在syscall 函数中使用嵌入汇编的形式调用软中断指令。 3、_syscall系统调用宏 4、通过软中断指令陷入 3.响应请求软中断是通过一条具体指令SWI,当CPU执行到SWI指令时会触发中断,进入中断程序。 以X86 体系架构为例,软件中断指令( int 0x80)用于产生软中断,同时处理器从用户模式变换到特权模式。
\linux-2.6.28.6\arch\x86\include\asm\irq_vectors.h中SYSCALL_VECTOR宏如下: # define SYSCALL_VECTOR 0x80 linux-2.6.28.6\arch\x86\kernel\traps.c 中断配置如下: void __init trap_init(void){ .....省略代码..... set_intr_gate(0, ÷_error); set_intr_gate_ist(1, &debug, DEBUG_STACK); set_intr_gate_ist(2, &nmi, NMI_STACK); /* int3 can be called from all */ set_system_intr_gate_ist(3, &int3, DEBUG_STACK); /* int4 can be called from all */ set_system_intr_gate(4, &overflow); set_intr_gate(5, &bounds); set_intr_gate(6, &invalid_op); set_intr_gate(7, &device_not_available); .....省略代码..... set_system_trap_gate(SYSCALL_VECTOR, &system_call); .....省略代码.....} 0x80 中断向量服务入口为system_call,system_call函数定义在 ENTRY(system_call) RING0_INT_FRAME # can't unwind into user space anyway pushl %eax # save orig_eax CFI_ADJUST_CFA_OFFSET 4 SAVE_ALL GET_THREAD_INFO(%ebp) # system call tracing in operation / emulation /* Note, _TIF_SECCOMP is bit number 8, and so it needs testw and not testb */ testw $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp) jnz syscall_trace_entry cmpl $(nr_syscalls), %eax jae syscall_badsyssyscall_call: call *sys_call_table(,%eax,4) movl %eax,PT_EAX(%esp) # store the return value .....省略代码.....ENDPROC(system_call) 我们将以上简化的代码根据syscall_call标号分为两部分:syscall_call准备,syscall_call调用 syscall_call准备 cmpl $(nr_syscalls), %eax 指令语句则是用来判断从用户态传入的系统调用号是否大于系统中所实现的最大的系统调用编号,判断传入的系统调用编号是否合法,如果不合法程序就跳转到 syscall_badsys 处执行相应的出错处理程序。 syscall_call调用 call *sys_call_table(,%eax,4) call *sys_call_table(,%eax,4) 根据 eax 中传入的系统调用号来调用对应的系统调用服务程序, sys_call_table跳转表 sys_call_table定义如下: /* 代码文件路径:/linux-2.6.28.6/arch/x86/kernel/syscall_32.c */#undef __SYSCALL#define __SYSCALL(nr, sym) [nr] = sym,#undef _ASM_X86_UNISTD_64_Htypedef void (*sys_call_ptr_t)(void);extern void sys_ni_syscall(void);const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = { /* *Smells like a like a compiler bug -- it doesn't work *when the & below is removed. */ [0 ... __NR_syscall_max] = &sys_ni_syscall,#include <asm/unistd_64.h>}; sys_call_table 是函数指针类型的的数组,数组长度就是系统中所包含的全部系统调用的数量__NR_syscall_max + 1, asm/unistd_64.h文件内容如下: #ifndef _ASM_X86_UNISTD_64_H#define _ASM_X86_UNISTD_64_H#ifndef __SYSCALL#define __SYSCALL(a, b)#endif/* at least 8 syscall per cacheline */#define __NR_read 0__SYSCALL(__NR_read, sys_read)#define __NR_write 1__SYSCALL(__NR_write, sys_write)#define __NR_open 2__SYSCALL(__NR_open, sys_open)#define __NR_close 3__SYSCALL(__NR_close, sys_close)#define __NR_stat 4__SYSCALL(__NR_stat, sys_newstat)#define __NR_fstat 5__SYSCALL(__NR_fstat, sys_newfstat)#define __NR_lstat 6__SYSCALL(__NR_lstat, sys_newlstat)#define __NR_poll 7__SYSCALL(__NR_poll, sys_poll) .....省略代码..... sys_call_table扩展开后为: __visible const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = { [0 ] = sys_read, [1] = sys_write [2] = sys_open, [3] = sys_close, .....省略代码.....}; sys_call_table[0] 就是执行sys_read函数,使用跳转表后程序的执行状态如下图: 4.功能实现系统调用号和系统调用服务程序是一一对应关系,一个系统调用对应一个系统调用服务程序。90号系统调用对应的系统调用服务程序就是 sys_chmod,如下所示: #define __NR_chmod 90__SYSCALL(__NR_chmod, sys_chmod) sys_chmod函数定义如下: linux-2.6.28.6\linux-2.6.28.6\fs\open.c SYSCALL_DEFINE2(chmod, const char __user *, filename, mode_t, mode){ return sys_fchmodat(AT_FDCWD, filename, mode);} 为什么不是 sys_chmod() 作为函数名呢? Linux 系统中的系统调用服务函数都是使用 SYSCALL_DEFINEx (0,1,2,3,4,5,6) 宏来实现的,这样使得用户态和内核态的两个函数“看起来一样”。 SYSCALL_DEFINEx 的定义如下所示, /* 代码文件路径:/linux-3.18.6/include/linux/syscalls.h */#define SYSCALL_METADATA(sname, nb, ...)#define SYSCALL_DEFINE0(sname) \ SYSCALL_METADATA(_##sname, 0); \ asmlinkage long sys_##sname(void)#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__) #define SYSCALL_DEFINEx(x, sname, ...) \ SYSCALL_METADATA(sname, x, __VA_ARGS__) \ __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)#define __PROTECT(...) asmlinkage_protect(__VA_ARGS__)#define __SYSCALL_DEFINEx(x, name, ...) \ asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__)) \ __attribute__((alias(__stringify(SyS##name)))); \ 所以根据上面的定义,宏 SYSCALL_DEFINE2(chmod, const char __user *, filename, mode_t, mode)展开后得到的结果就是: asmlinkage long sys_chmod(const char __user *filename, mode_t mode){ return sys_fchmodat(AT_FDCWD, filename, mode);} 5.自定义一个系统调用通常情况下已有的系统调用可以满足我们绝大多少需求,但是在极少数情况下,我们需要额外增加一个自定义的系统调用。接下来我们来学习一下如何增加一个自定义系统调用。例如我们自定义一个名为weiweicall的系统调用(该系统调用的参数为0)。 一旦系统调用的名字确定下来了,那么在系统调用中的几个相关名字也就确定下来了。 5.1接口函数 #include <linux/unistd.h>_syscall0(int,weiweicall) /* 注意这里没有分号*/int main(){ weiweicall();} 定义了weiweicall用户系统调用函数,接下来我们定义系统调用号。 5.2添加系统调用号 我们要做的就是在文件asm/unistd_64.h文中添加自定义的系统调用号__NR_weiweicall,代码如下: linux-2.6.28.6\arch\x86\include\asm\unistd_64.h#define __NR_eventfd2 290__SYSCALL(__NR_eventfd2, sys_eventfd2)#define __NR_epoll_create1 291__SYSCALL(__NR_epoll_create1, sys_epoll_create1)#define __NR_dup3 292__SYSCALL(__NR_dup3, sys_dup3)#define __NR_pipe2 293__SYSCALL(__NR_pipe2, sys_pipe2)#define __NR_inotify_init1 294__SYSCALL(__NR_inotify_init1, sys_inotify_init1)//增加自定义系统调用#define __NR_weiweicall 295__SYSCALL(__NR_weiweicall, sys_weiweicall) 添加系统调用号之后,系统才能根据这个号作为索引,去找syscall_table中的相应表项。 5.3sys_mysyscall 的实现 我们自定义的程序添加在内核代码中,如kernel/sys.c 里面,我们没有在kernel 目录下另外 //直接定义sys_xxx函数asmlinkage int sys_weiweicall(void){ printk( 'weiwei system call!'); return 1;} //使用SYSCALL_DEFINE定义函数SYSCALL_DEFINE0(weiweicall){ printk( 'weiwei system call!'); return 1;} 编译内核后,应用程序就可以使用weiweicall系统调用。 #include <linux/unistd.h>_syscall0(int,weiweicall) /* 注意这里没有分号*/int main(){ weiweicall();} 6.系统调用分类系统调⽤按照功能逻辑⼤致可分为“进程控制”、“⽂件系统控制”、“系统控制”、“存管管理”、“⽹络管理”、“socket控制”、“⽤户管理”、“进程间通信”。 进程控制类系统调用
文件操作类系统调用
文件系统操作类系统调用
系统控制类系统调用
内存管理类系统调用
网络管理类系统调用
用户管理类系统调用
进程间通信类系统调用
总结:系统调用使用接口函数产生软中断,处理器进入中断状态执行相应的功能程序 |
|
来自: imnobody2001 > 《Linux pgm》