接口表现为函数调用,又由系统提供,所以称为系统调用(System Call)。
IEEE对其制定了一个标准族——POSIX(Portable Operating System Interface of Unix)。
系统调用的实现
特权级
在系统调用中不能随意的调用数据,不能随意的jmp。否则会影响系统安全。
通过(也仅能通过)硬件电路对其进行控制,将内存分割为内核段与用户段来区别内核态与用户态。
内核态可以访问任何数据,用户态不能访问内核数据。同时对指令跳转也进行隔离。
CPL(Current Privilege Level)是当前进程的权限级别,是当前正在执行的代码所在的段的特权级,存在于CS寄存器的低两位:0是内核态、3是用户态。
DPL(Descriptor Privilege Level)存储在段描述符中,规定访问该段的权限级别,每个段的DPL固定。
当进程访问一个段时,由硬件进行特权级检查,只有当前指令的特权级大于等于目标指令的特权级时才被允许。
系统初始化时(head.s)针对内核态的代码建立GDT表项,对应的DPL为0。初始化完成后,转为用户态运行,需要将CS的CPL置为3。
硬件提供了主动进入内核的方法——中断
中断是用户程序调用内核代码的唯一方法,
int 0x80
指令将CS中的CPL改成0,进入内核。系统调用的核心:
- 用户程序中包含一段含int指令的代码(库函数)
- 操作系统写中断处理,获取想调用程序的编号
- 操作系统根据编号执行相应代码
Linux中的系统调用的实现细节
库函数write最终展开成包含int指令的代码
//linux/lib/write.c #include <unistd.h> _syscall3(int, write, int, fd, const char *buf, off_t, count)
//linux/include/unistd.h //syscall3表示有3个参数 #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; \ }
显然,__NR_write是系统调用号,放在eax中;
同时,eax也存放返回值,ebx、ecx、edx存放3个参数。
int 0x80 中断的处理
在IDT表取出中断处理函数然后跳到那里执行,处理完成后回到中断时代码
初始化时在sched_init函数中设置了中断处理门:
void sched_init(void) { //system_call是中断处理函数的地址 //使用set_system_gate设置0x80的中断处理 set_system_gate(0x80,&system_call); }
//linux/include/asm/system.h //将dpl置为3,addr传入中断处理函数 #define set_system_gate(n,addr) \ _set_gate(&idt[n],15,3,addr) #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))
进入80号中断时,将
段:偏移
设置 为新的PC(CS=8,IP=system_call)。而且DPL是3,用户程序可以访问。CS中的最后两位是CPL,CS=8时CPL=0,所以通过80中断执行中断处理函数时特权级已经是0。
//linux/kernel/system_call.s nr_system_calls=72 .globl _system_call _system_call: cmpl $nr_system_calls-1,%eax //eax中存放的是系统调用号 ja bad_sys_call push %ds push %es push %fs pushl %edx //调用的参数 pushl %ecx pushl %ebx movl $0x10,%edx mov %dx,%ds //内核数据 mov %dx,%es movl $0x17,%edx mov %dx,%fs //fs可以找到用户数据 call _sys_call_table(,%eax,4) //a(,%eax,4)=a+4*eax eax是__NR_write pushl %eax //返回值压栈,留着ret_from_sys_call时用 ... //其他代码 ret_from_sys_call: popl %eax, 其他pop, iret
将ds、es赋为0x10(16),也就是内核的数据段。此时既是内核的数据段,也是内核的代码段。
_sys_call_table+4*%eax
就是相应的系统调用处理函数入口。4表示每个系统调用(函数指针)占4个字节(32位)。eax=4,
call _sys_call_table(,%eax,4)
就是call sys_wirte
。//include/linux/sys.h fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read, sys_write, ... }; //sys_wirte对应下标为4,__NR_write=4
//include/linux/sched.h typedef int (fn_ptr*)();