1、参考资料
2、概要 操作系统作为软件应用层和底层硬件之间的部分,向下提供服务,向上提供接口。系统调用便是操作系统向上层应用提供的操作底层硬件的和核心服务的接口,也就是API(Application Programming Interface)。我们知道一般API实现的方法是提供函数接口,只需要调用函数就可以实现相应的功能,底层的原理是程序地址的跳转。因为操作系统和用户程序同时存在于内存中,我们当然是不希望操作系统的数据被随意的篡改和访问,有可能造成十分严重的后果,所以操作系统对内存做了区分:
数值越小,级别越高,低级别进程无权访问高级别的内存区域,由此隔离了系统程序和用户程序,提高了操作系统的安全性。
因为CS:IP表示当前指令,所以用CS最低两位(CPL)来表示当前程序属于用户态还是核心态,访问的数据段DS最低两位(DPL)表示目标代码属于用户态还是核心态,在地址跳转时检查DPL和CPL,只有在CPL≤DPL时才允许跳转。但是对于系统调用来说,就需要找到一种方法穿透用户态和内核态的屏障,在x86处理器中,当用户代码想要调用内核代码时,硬件通过终端指令int将CPL改为0,从而穿透屏障实现调用。
3、系统调用write原理
选取write(…)指令进行分析,最典型调用write(…)的命令就是c中的printf(…)函数。write(…)定义在lib/write.c
首先看_syscall3的定义:
1 2 3 4 5 6 7 8 9 10 #define __LIBRARY__ #include <unistd.h> _syscall3(int ,write,int ,fd,const char *,buf,off_t ,count)
_syscall3是一个宏,定义在include/unistd.h中,可以发现其中还有多个相似的_syscallx宏,x表示可传入的参数个数
1 2 3 4 5 6 7 8 9 10 11 12 #define _syscall3(type,name,atype,a,btype,b,ctype,c) \ type name(atype a,btype b,ctype c) \ { \ long __res; \__asm__ volatile ("int $0x80" \ : "=a" (__res) \ : "0" (__NR_##name),"b" ((long )(a)),"c" ((long )(b)),"d" ((long )(c))) ; \if (__res>=0 ) \ return (type) __res; \ errno=-__res; \ return -1 ; \}
展开后可以看到是将传入宏的参数替换成了一个函数定义,主要内容是将传入的参数存入各个寄存器,之后调用了int 80的中断。其中__NR_##name被传入了eax,因为所有的调用都通过80号中断,显然这是区分不同函数的参数。同时发现在unistd.h中还定义了一系列类似的宏
1 2 3 4 5 #ifdef __LIBRARY__ ... #define __NR_write 4 ... #endif
所以可以看出这是对系统调用的索引,而真正起作用的函数定义在fs/read_write.c中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 int sys_write (unsigned int fd,char * buf,int count) { struct file * file ; struct m_inode * inode ; if (fd>=NR_OPEN || count <0 || !(file=current->filp[fd])) return -EINVAL; if (!count) return 0 ; inode=file->f_inode; if (inode->i_pipe) return (file->f_mode&2 )?write_pipe(inode,buf,count):-EIO; if (S_ISCHR(inode->i_mode)) return rw_char(WRITE,inode->i_zone[0 ],buf,count,&file->f_pos); if (S_ISBLK(inode->i_mode)) return block_write(inode->i_zone[0 ],&file->f_pos,buf,count); if (S_ISREG(inode->i_mode)) return file_write(inode,file,buf,count); printk("(Write)inode->i_mode=%06o\n\r" ,inode->i_mode); return -EINVAL; }
可以看到sys_write(…)的函数原型与之前宏展开的一致,所以在调用write(…)后,实际是调用了sys_write(…),要弄清这个原理,就要去看int 80的实现。
在init/main.c中调用了sched_init(),sched_init()中有一句
1 set_system_gate(0x80 ,&system_call);
出现了80号中断,这是一句宏,在include/asm/system.h中,展开得
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #define _set_gate(gate_addr,type,dpl,addr) \ __asm__ ("movw %%dx,%%ax\n\t" \ "movw %0,%%dx\n\t" \ "movl %%eax,%1\n\t" \ "movl %%edx,%2" \ : \ : "i" ((short ) (0x8000 +(dpl<<13 )+(type<<8 ))), \ "o" (*((char *) (gate_addr))), \ "o" (*(4 +(char *) (gate_addr))), \ "d" ((char *) (addr)),"a" (0x00080000 )) ... #define set_system_gate(n,addr) \ _set_gate(&idt[n],15 ,3 ,addr) ...
idt是中断向量表基址,这里指定第80号中断,DPL被指定为3,同时在system_call为”处理函数入口点偏移“,之后进入kernel/system_call.s的system_call
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 ... nr_system_calls = 74 ... .globl system_call ... system_call: cmpl $nr_system_calls-1 ,%eax ja bad_sys_call push %ds push %es push %fs pushl %edx pushl %ecx # push %ebx,%ecx,%edx as parameters pushl %ebx # to the system call movl $0x10 ,%edx # set up ds,es to kernel space mov %dx,%ds mov %dx,%es movl $0x17 ,%edx # fs points to local data space mov %dx,%fs call sys_call_table(,%eax,4 ) pushl %eax movl current,%eax cmpl $0 ,state(%eax) # state jne reschedule cmpl $0 ,counter(%eax) # counter je reschedule
nr_system_calls表示总系统调用数,所以最先比较eax中的调用号是否小于总数,之后将各个参数推入堆栈,之后调用地址为sys_call_table + %eax × 4处的函数。sys_call_table在include/linux/sys.h中里面有包括sys_write在内的72个系统调用函数的函数地址表。
1 2 3 4 5 ... extern int sys_write () ; ... fn_ptr sys_call_table[] = { ... sys_write, ...};
而sys_write()就是索引为4的函数。由此系统调用的过程结束,总结为
4、添加调用printval 首先在 include/unistd.h中添加系统调用编号:
1 #define __NR_printval 72
在include/linux/sys.h中添加:
1 2 3 extern int sys_printval () ;fn_ptr sys_call_table[] = {..., sys_printval };
在kernel/system_call.s中修改nr_system_calls的值 :
在kernel/sys.c中实现系统调用函数:
1 2 3 4 5 int sys_printval () { printk("in sys_printval\n" ); return 0 ; }
至此系统调用添加完成,重新编译,并启动内核,接下来我们在用户态编写测试代码。
5、用户态测试代码 修改系统头文件 /usr/include/unistd.h ,增加:
1 #define __NR_printval 72
编写用户态测试代码main.c:
1 2 3 4 5 6 7 8 9 10 11 12 #define __LIBRARY__ #include <stdio.h> #include <unistd.h> _syscall0(int , printval); int main () { printf ("Hello World!\n" ); printval(); return 0 ; }
编译运行:
1 2 3 4 Hello World! in sys_printval
参考:Linux-0.11内核学习笔记【2】:添加系统调用 Linux-0.11源代码