当用户态的进程调用一个系统调用时,CPU从用户态切换到内核态并开始执行一个内核函数。Linux通过由向量为128(0x80)的编程异常实现CPU由用户态到内核态的转换。
因为内核实现了许多不同的系统调用,为了区别他们,进程必须传递一个系统调用号的参数来识别所需的系统调用。EAX寄存器是负责传递系统调用号的。
系统调用处理程序执行下列操作:
(1)在内核栈保存大多数寄存器的内容(这个操作对所有的系统调用都是通用的,并用汇编语言编写)。
(2)调用系统调用服务例程的相应的C函数处理系统调用。
(3)通过syscall_exit_work()函数从系统调用返回(这个函数用汇编语言编写)。
1.初始化系统调用
内核初始化期间调用trap_init()函数建立IDT表(中断描述符表)中128号向量对应的表项,语句如下:
set_system_gate(SYSCALL_VECTOR, &system_call);
其中SYSCALL_VECTOR是一个宏定义,其值为0x80,该调用把下列值装入这个门描述符的相应域。
(1)段选择子:因为系统调用处理程序属于内核代码,填写内核代码段__KERNEL_CS的段选择子。
(2)偏移量:指向system_call()系统调用处理程序。
(3)类型:置为15.表示这个异常是一个陷阱门,相应的处理程序不禁止可屏蔽中断。
(4)DPL(描述符权级):置为3。这就允许用户态进程调用这个异常处理函数。
2.system_call()函数
由于未学过汇编语言,可能理解的并不是很清晰。
system_call()函数实现了系统调用处理函数。它首先把系统调用号和这个异常处理程序可以用到的所有CPU寄存器保存到相应的栈中,当然,栈中还有CPU已自动保存的EFLAGS,CS,EPI,SS和ESP寄存器,也在DS和ES中装入内核数据段的段选择子。
然后对用户态进程传来的系统调用号进行有效性检查。如果这个号大于或等于NR_syscalls,系统调用处理程序终止。
如果系统调用号无效,跳转到syscall_babsys处执行,此时就把—ENOSYS值存放在栈中EAX寄存器(该寄存器即存放系统调用号也存放系统调用的返回值,前者为正数,后者为负数)所在的单元(从当前栈顶开始偏移为24的单元,即EXA寄存器所在的单元)。然后返回用户空间。当进程以这种方式恢复它在用户态的执行时,会在EXA中发现一个负数的返回码。
最后,根据EXA中所包含的系统调用号调用对应的服务例程,因为系统调用表种的每一表项占4个字节,因此首先要把EXA中的系统调用号乘以4再加上sys_call_table系统调用表的起始地址(相当于起始位置加偏移量从而找到所要找到的地址),然后从这个地址单元获取指向相应服务例程的指针,内核就找到了要调用的服务例程。
当服务例程执行结束时,system_call()从EAX获得他的返回值,并把这个返回值存放在栈中,让其位于用户态EXA寄存器曾存放的位置。然后执行syscall_exit代码段,终止系统调用处理程序的执行。
3.参数传递
与普通的函数相似,系统调用通常也需要输入/输出参数,这些参数可能是实际的值,也可能是函数的地址即用户态进程地址空间的变量。因为system_call()函数时LINUX中所有系统调用唯一的入口点,因此每隔系统调用至少由一个函数,即通过EXA寄存器传递来的系统调用号。例如,如果一个应用程序调用至少有一个参数,即通过EXA寄存器传递来的系统调用号。而EXA这个寄存器的设置是由libc种的封装例程进行的,所以程序员通常不用关心系统调用号。
普通函数的参数传递是通过把参数写入活动的程序栈(或用户态栈或内核态栈)。但是系统调用的参数通常是通过寄存器传递给系统调用处理程序的,然后在拷贝到内核态堆栈。(实际就是相应的服务例程中所需的参数)。
寄存器的使用时的系统调用处理程序的结构与其它的异常处理程序结构类似。
然而,为了用寄存器传递参数,必须满足以下两个条件。
(1)每隔参数的长度不得超过寄存器的长度,即32位。
(2)参数的个数不得超过6个(包括系统调用号),因为寄存器的数量是有限的。
第一条总能成立,因为根据POSIX的标准,不能存放在32位寄存器的长参数必须通过指定他们的地址来传递。
对于第二个条件,确实存在超过6个参数的系统调用。在这种情况下,用一个单独的寄存器指向进程地址空间中这些参数值所在的一个内存区间即可。当然,编程者并不用关心这个工作区。与任何C调用一样,当调用libc封装例程时,参数被自动的保存在栈中。封装例程将找到合适的方式把参数传递给内核。
system_call()使用SAVE_ALL宏把这些寄存器的值保存在内核堆栈态中。因此,当系统调用服务例程转换到内核堆栈态时,就会找到system_call()的返回地址,紧接着是存放在EAX中的参数(即系统调用的第一个参数),存放在其与寄存器的参数等。这种栈结构与普通函数调用的栈结构完全相同,因此,服务例程可以很容易的使用一般C语言构造的参数。
处理write()系统调用的sys_write()服务例程的声明如下:
int sys_write(unsigned int fd, const char *buf, unsigned int count);
C编辑器产生一个汇编语言函数,该函数可以在栈顶找到fd,buf和count参数,因为这些参数就位与返回地址的下面。
在少数情况下,系统调用不是用任何参数,但是相应的服务例程需要知道发出系统调用之前CPU寄存器中的内容。在这种情况下,一个类型位pt_regs的单独参数允许服务例程访问由SAVE_ALL宏保存在内核态堆栈中的值,例如系统调用fork()的服务例程sys_fork():
int sys_fork(struct pt_regs regs);
服务例程的返回值必须写到EXA寄存器中,这是在执行return n 指令时由C编译程序自动完成时。
对于getpid()函数的执行:
(1)程序调用libc库的封装函数getpid。该封装函数将系统调用号__NR_getpid压入EAX寄存器。
(2)在内核中首先执行system_call()函数,接着根据系统调用号在系统调用表中查到找到对应的系统调用服务例程sys_getpid。
(3)执行sys_getpid服务例程。
(4)执行完毕后,转入system_exit_work例程,从系统调用返回。