一、原理总结
本周老师讲的内容主要包括三个方面,用户态、内核态和中断,系统调用概述,以及使用库函数API获取系统当前时间。系统调用是操作系统为用户态进程与硬件设备进行交互提供的一组接口,也是一种特殊的中断,可使用户态切换到内核态。当用户态进程调用一个系统调用时,CPU切换到内核态并开始执行一个内核函数。
1.用户态、内核态和中断
内核态:一般现代CPU有几种指令执行级别。在高执行级别下,代码可以执行特权指令,访问任意的物理地址,这种CPU执行级别对应着内核态
用户态:在相应的低级别执行状态下,代码的掌控范围有限,只能在对应级别允许的范围内活动。如intel x86 CPU有四种不同的执行级别0-3,Linux只使用0级表示内核态,3级表示用户态。权限级别的划分使系统更稳定
区分用户态与内核态主要通过代码段选择寄存器cs和偏移量寄存器eip,cs寄存器的最低两位表明了当前代码特权级,CPU每条指令的读取都是通过cs:eip这两个寄存器一般在Linux中,(逻辑)地址空间是显著标志:0xc0000000以上的地址空间只能在内核态下访问,0x00000000-0xbfffffff的地址空间在两种状态下都能访问
中断处理是从用户态进入内核态的主要方式。系统调用只是一种特殊的中断。从用户态切换到内核态时:必须保存用户态的寄存器上下文,同时将内核态的寄存器相应的值放入当前CPU。中断/int指令会在堆栈上保存一些寄存器的值:如用户态栈顶地址、当前的状态字、当时cs:eip的值(当前中断程序的入口)
保护现场:进入中断程序,保存需要用到的寄存器的数据(中断发生后的第一件事)
#define SAVE_ALL //将其他寄存器的值push到内核堆栈中
恢复现场:退出中断程序,恢复保存寄存器的数据(中断处理结束前最后一件事)
#RESTORE_ALL //将用户态保存的寄存器pop到当前CPU中
iret指令与中断信号(包括int指令)发生时的CPU的动作相反
2.系统调用概述
系统调用是操作系统为用户态进程与硬件设备进行交互提供的一组接口。把用户从底层的硬件编程中解放出来,极大的提高了系统的安全性,使用户程序具有可移植性。应用程序接口(API)与系统调用不同,API只是一个函数定义,系统调用通过软件中断trap向内核发出一个明确的请求,Libc库定义的一些API引用了封装例程,库再用这些封装例程定义出给用户的API。不是每个API都对应一个特定的系统调用,一个单独的API可能调用几个系统调用,不同的API可能调用了同一个系统调用
系统调用的三层皮:xyz(API)、system_ call(中断向量)、sys_xyz(中断向量对应的中断服务程序)。当用户态进程调用一个系统调用时,CPU切换到内核态并开始执行一个内核函数,在Linux中是通过执行int $0x80来执行系统调用的, 这条汇编指令产生向量为128的编程异常。
3.使用库函数API和C代码中嵌入汇编代码触发同一个系统调用
使用库函数API获取系统当前时间的C代码time.c
#include <stdio.h> #include <time.h> int main() { time_t tt;//int型数值 struct tm *t; //便于输出值可读 tt = time(NULL); t = localtime(&tt);//将tt转换成之前声明的t类型,便于可读 printf("time:%d:%d:%d:%d:%d:%d:\n",t->tm_year+1960,t->tm_mon,t->tm_mda,t->tm_hour,t->tm_min,t->tm_sec); return 0; } 编译: gcc time.c -o time -m32 运行: ./time 结果:输出系统时间的年:月:日:时:分:秒
使用C代码中嵌入汇编代码触发系统调用获取系统当前时间,嵌入汇编代码time_asm.c
#include <stdio.h> #include <time.h> int main() { time_t tt;//int型数值 struct tm *t; asm volatile( "mov $0,%%ebx\n\t"//系统调用传递第一个参数使用ebx,这里是null "mov $0xd,%%eax\n\t"//使用%eax传递系统调用号13,用16进制表示为0xd "int $0x80\n\t" //执行系统调用 "mov %%eax,%0\n\t"//通过eax这个寄存器返回系统调用值,和普通函数一样 :"=m"(tt) ); t = localtime(&tt); printf("time:%d:%d:%d:%d:%d:%d:\n",t->tm_year+1960,t->tm_mon,t->tm_mda,t->tm_hour,t->tm_min,t->tm_sec); return 0; } 编译: gcc time-asm.c -o time-asm -m32 运行: ./time-asm 结果:输出系统时间的年:月:日:时:分:秒
该嵌入汇编代码的执行结果和C代码一样。
二、实验内容
使用库函数API和C代码中嵌入汇编代码两种方式使用同一个系统调用,在这里选出系统调用号为64的系统调用sys_getppid(),该系统调用用于返回当前进程的父进程进程号数。
下面是直接使用库函数API使用系统调用。getppid.c代码中,函数getppid()是glibc对系统调用sys_getppid的封装,用于获取当前进程的父进程的进程号。sys_getppid系统调用号为64,在用户态时候,如果用户调用了getppid(),系统会产生一中断,进入到了内核态执行sys_getppid。getppid()的功能是返回当前进程的父进程的ID,它本身是不能完成的,必须请求操作系统服务即sys_getppid,让操作系统把当前进程的ID告诉给getppid()。
#include <stdio.h> #include<unistd.h> int main() { pid_t pid; pid=getppid(); printf("The number of parent process is: %d\n",pid); return 0; }
下面再使用C语言内嵌汇编代码的方式实现同一个系统调用。getpid_asm.c代码
#include <stdio.h> #include<unistd.h> int main() { pid_t pid; asm volatile( "mov $0,%%ebx\n\t" /*ebx用来传递参数,getppid(void)的参数是void所以设置为零*/ "mov $0x40,%%eax\n\t" /*eax用来传递系统调用号,getppid的系统调用号是64,所以是0x40*/ "int $0x80\n\t" /* 软中断汇编指令,系统进入内核态 */ "mov %%eax,%0\n\t" /*eax保留返回值,把返回值放到输出参数中,即pid变量中*/ :"=m" (pid) /*输出参数是pid*/ ); printf("The number of parent process is: %d\n",pid); return 0; }
由实验截图可知,两种实现方式得到的父进程进程号数一致
下面分析嵌入式汇编代码及参数传递:
mov $0,%%ebx\n\t 系统调用传递第一个参数使用ebx,这里是null
mov $0xd,%%eax\n\t 使用%eax传递系统调用号13,用16进制表示为0xd
int $0x80\n\t 执行系统调用
mov %%eax,%0\n\t 通过eax这个寄存器返回系统调用值,和普通函数一样
三、总结
本实验的关键是系统调用。在Linux中,每个系统调用被赋予一个系统调用号。这样,通过这个独一无二的号就可以关联系统调用。当用户空间的进程执行一个系统调用的时候,这个系统调用号就被用来指明到底是要执行哪个系统调用。进程不会提及系统调用的名称。 系统调用号相当关键,一旦分配就不能再有任何变更,否则编译好的应用程序就会崩溃。Linux有一个“未实现”系统调用sys_ni_syscall(),它除了返回一ENOSYS外不做任何其他工作,这个错误号就是专门针对无效的系统调用而设的。因为所有的系统调用陷入内核的方式都一样,所以仅仅是陷入内核空间是不够的。因此必须把系统调用号一并传给内核。在x86上,系统调用号是通过eax寄存器传递给内核的。在陷人内核之前,用户空间就把相应系统调用所对应的号放入eax中了。这样系统调用处理程序一旦运行,就可以从eax中得到数据。其他体系结构上的实现也都类似。
除了系统调用号以外,大部分系统调用都还需要一些外部的参数输人。所以,在发生异常的时候,应该把这些参数从用户空间传给内核。最简单的办法就是像传递系统调用号一样把这些参数也存放在寄存器里。
刘帅
原创作品转载请注明出处
《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000