20135323符运锦----第四周:扒开系统调用的“三层皮”

扒开系统调用的“三层皮”

一、 用户态、内核态和中断处理过程

1.用户态、内核态区别

①在高级别的状态下,代码可以执行特权指令,访问任意的物理地址,这种CPU执行级别就对应着内核态。

②在相应的低级别执行状态下,代码的掌控范围会受到限制。

③为什么会有这种级别划分?答:没有访问权限划分容易使得系统混乱,程序员代码写得不健壮,防止非法访问。

④Intel x86 CPU有四种不同的执行级别0——3,Linux只是用了其中的0和3来表示内核态和用户态

-如何区分?CPU每条指令都是通过cs:eip这两个寄存器读取的(代码段选择寄存器:偏移量寄存器)。一般在Linux中,0xc0000000以上的地址(指的是逻辑地址)空间只能在内核态下访问(即全部的4G内存都可以访问)

2.中断处理

①中断时从用户态转换为内核态的主要方式。

②中断发生的两种情况:1.可能是硬件中断,中断服务进程;2.用户态程序调用了系统调用(其中系统调用是一种特殊的中断)。

③当从用户态切换到内核态时,必须要保存用户态的寄存器上下文,中断指令会在寄存器上保存一些寄存器的值放入内核堆栈,比如:用户态栈顶地址(ss:esp),标志寄存器(eflags),cs:eip(为了返回的时候popl弹出保存的返回地址)。同时,将相关联的中端服务历程的入口加载到cs:eip,把当前的堆栈段esp也加载到CPU里面。

④举例:

interrupt(ex:int 0X80)//系统调用
save cs:eip/ss:esp/eflags(current)to kernel stack,then load cs:eip(entry of a specific ISR)and ss:eip(point to kenerl stack)
//保存当前堆栈段寄存器和当前栈顶和标志位寄存器到内核堆栈中,然后加载当前系统调用相关中断服务例程入口到cs:eip中,把当前的堆栈段和栈顶加载到CPU
SAVE_ALL//进入内核态
...//内核代码,完成中断服务,发生进程调度
RESTORE_ALL//恢复现场,只有在进程调度执行完后才会被执行
iret - pop cs:eip/ss:esp/eflags from kernel stack

⑤中断发生之后第一件事就是保存现场;同样,中断处理结束前的最后一件事情就是恢复现场。也就是说,SAVE ALL之后就是内核态了;restore all之后再返回用户态。

⑥iret指令与中断信号(包括int指令)发生时的CPU所做的动作恰好相反。

二、系统调用概述和系统调用的三层皮

1.系统调用概述

系统调用的意义:

操作系统为用户态进程与硬件设备进行交互提供了一组接口——系统调用。

①把用户从底层的硬件编程中解放出来

②极大的提高了系统的安全

③使用户程序具有可移植性

- 解释:系统调用减少了系统与硬件之间的耦合,所以极大提高了系统可移植性

操作系统提供的API和系统调用的关系

①应用编程接口(API)和系统调用是不同的。使用API是为了让用户从底层硬件编程中解放出来。

②API只是一个被封装好的函数定义

③系统调用通过软中断向内核发出一个明确的请求

④Libc库定义的一些API引用了封装例程(wrapper routine),唯一目的就是发布系统调用。libc库定义的API使得程序员不用去以汇编代码进行系统调用而是直接以函数调用的形式。

⑤一般每个系统调用对应一个封装例程,库再用这些封装例程定义出给用户的API

⑥API与系统调用不是单一的一对一的关系,也存在多对多的关系。但也有特例,例如一些数学函数没有用到系统调用

⑦大部分封装例程返回一个整数,其值的含义依赖于相应的系统调用

⑧返回值-1在多数情况下表示内核不能满足进程的请求,Libc中定义的errno变量包含特定的出错码

2.系统调用的三层皮


①系统调用的三层皮:xyz,system_call,sys_xyz。也就是:API,中断向量,服务程序。

②详细过程:首先,xyz()函数是系统调用对应的API,这个应用程序编程接口里面封装了一个系统调用,这个系统调用会触发一个int0x80的中断,产生向量为128的编译异常,0x80这个中断向量对应着system_call这个内核代码的起点,这个内核代码里面会有SAVE_ALL,然后执行到sys_xyz()中断服务程序,进入程序里面处理,在中断服务程序执行完之后会ret_from_sys_call,在return的过程中可能会发生进程调度,如果没有进程调度,就会iret,回到用户态接着执行。

3.系统调用的参数传递方法

①内核实现了很多不同的系统调用, 进程必须指明需要哪个系统调用,这需要传递一个名为系统调用号的参数

②system_call是linux中所有系统调用的入口点,每个系统调用至少有一个参数,即由eax传递的系统调用号。具体过程如下:

1.一个应用程序调用fork()封装例程,那么在执行int $0x80之前就把eax寄存器的值置为2(即__NR_fork)。
2.这个寄存器的设置是libc库中的封装例程进行的,因此用户一般不关心系统调用号
3.进入sys_call之后,立即将eax的值压入内核堆栈

③补充:

1.系统调用号将xyz和sys_xyz关联起来,使用eax寄存器来传递;
2.为什么把eax寄存器的值设置为2?
答:这句话的意思是举例说明eax的作用,即:fork()通过系统调用创建与原进程几乎一样的进程。在进行系统调用之前就把_NR_fork(也就是2)传给eax
3.参数超过6个怎么办?
答:把某一个寄存器作为一个指针,指针会指向一块内存,访问内核态可以访问所有的内核空间。

三、使用库函数API和C代码中嵌入汇编代码触发系统调用

1.使用库函数API获取当前系统时间(这算是一个比较简单的系统调用)

代码:

time.c
#include <stdio.h>
#include <time.h>
int main()
{
	time_t tt;//int型数值
	struct tm *t;//t指针指向一个结构体
	tt = time(NULL);//调用系统函数time,返回一个指针类型的变量
	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.c -o time -m32

之后,再输入

./time

结果:

打印出的就是系统时间下的 年:月:日:时:分:秒

2.C代码中嵌入汇编代码的复习


3.用汇编方式触发系统调用获取系统当前时间

代码:

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"//传递系统调用号13(13的16进制即为d)
		"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;
}

①系统调用返回值使用eax存储,与普通函数一样。

②编译:

gcc time-asm.c -o time-asm -m32

运行:

./time-asm

结果与上一个代码一致

②本段代码让我们更清楚地知道用户态对内核态做了什么

四、实验

1.实验要求

①选择一个系统调用(13号系统调用time除外),系统调用列表参见http://codelab.shiyanlou.com/xref/linux-3.18.6/arch/x86/syscalls/syscall_32.tbl

②参考视频中的方式使用库函数API和C代码中嵌入汇编代码两种方式使用同一个系统调用

2.库函数API进行20号系统调用

代码如下:

实验结果:

3.C代码中嵌入汇编代码进行20号系统调用

代码如下:

#include <unistd.h>
int main( void )
{
int d = getpid();
asm volatile(
“mov $0,%%ebx\n\t”
“mov $0x14,%%eax\n\t”//调用20号系统调用,即为16进制的14
“int $0x80\n\t”
“mov %%eax,%0\n\t”
:”=m”(d)
};
printf( "Process id: %d\n",d);
return 0;
}


实验结果:

4.实验改进

针对自己实验中出现警告,自己在自己电脑的虚拟机上重新做了一下,对代码进行优化后,做出来更正确的结果,截图如下:



5.实验补充

①何为getpid?答:getpid函数用来取得目前进程的进程ID,许多程序利用取到的此值来建立临时文件,以避免临时文件相同带来的问题。

五、系统调用的工作机制的总结

在start_kernel中的trap_init将(系统调用的)中断向量和汇编代码的入口(system_call)绑定,一旦执行int指令(如int0x80),cpu就会自动跳转到绑定的汇编代码入口处执行中断服务程序,此时cpu进入内核态,在服务处理结束返回用户态之前,可能会发生进程调度,调度完成后,cpu才会返回用户态。

六、参考资料

http://codelab.shiyanlou.com/xref/linux-3.18.6/arch/x86/syscalls/syscall_32.tbl

http://www.cnblogs.com/bastard/archive/2012/08/31/2664896.html

《Linux内核设计与实现》

【符运锦 原创作品转载请注明出处 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

posted @ 2016-03-14 22:09  20135323符运锦  阅读(300)  评论(1编辑  收藏  举报