第四节 系统调用的三个层次
By 20135203齐岳
用户态、内核态和中断
内核态
在高的执行级别下,代码可以执行特权指令,访问任意的物理地址,这时的CPU就对应内核态
用户态
在低级别的指令状态下,代码 只能在级别允许的特定范围内活动。在日常操作下,执行系统调用的方式是通过库函数,库函数封装系统调用,为用户提供接口以便直接使用。
注:
1.在Linux下0级表示内核态,3级表示用户态。
2.内核态cs:eip的值是任意的,即可以访问所有的地址空间。用户态只能访问其中的一部分内存地址。
中断处理过程
系统调用是一种特殊的中断,中断处理是从用户态进入内核态的主要方式。中断发生后的第一件事就是保存现场。从用户态切换到内核态,中断指令会在堆栈上保存用户态的寄存器上下文,其中包括用户态栈顶地址、当时的状态字、cs:eip的值,以及内核态的栈顶地址、当时的状态字、中断处理程序入口。中断处理结束前的最后一件事就是恢复现场,退出中断程序,恢复保存寄存器的数据。
举例说明中断处理的完整过程
interrupt(ex:int 0X80)//发生系统调用
save cs:eip/ss:esp/eflags(current)to kernel stack
//保存cs:eip的值,保存当前堆栈段寄存器当前栈顶和标志位寄存器
load cs:eip(entry of a specific ISR)and ss:eip(point to kenerl stack)
//把当前的中断信号或系统调用相关中断服务例程入口加载到cs:eip中,把当前的堆栈段和esp加载到CPU
SAVE_ALL//保存现场
...//内核代码,完成中断服务,发生进程调度
RESTORE_ALL//恢复现场
iret - pop cs:eip/ss:esp/eflags from kernel stack//iret对应相反的中断指令
系统调用概述
系统调用的意义
系统调用——操作系统为用户态进程与硬件设备进行交互提供的一组接口
-
把用户从底层的硬件编程中解放出来
-
极大的提高了系统的安全性
-
使用户程序具有可移植性
API(应用程序编程接口)与系统调用的关系
课程中老师的讲解:
-
API是一个系统调用封装成的一个函数定义
-
系统调用通过软中断向内核发出一个明确的请求
-
Libc库定义的一些API引用了封装例程,目的是发布系统调用,让程序员写代码的时候可以通过函数调用而非汇编指令触发一个系统调用
-
一般每个系统调用对应一个封装例程,库再用这些封装例程定义出给用户的API
自己的理解:
API是一个封装好的函数接口,其封装的内容可以是一些数学函数,也可以是系统调用,其目的都是提供一个接口个给程序员,以简化代码的内容。而当API中封装的是系统调用时,程序员可以直接通过接口实现系统调用。API和系统调用之间并没有严格的对应关系,只是简化了系统调用的实现过程。
关于返回值
-
大部分封装例程返回一个整数,其值的含义依赖于相应的系统调用
-
-1在多数情况下表示内核不能满足进程的请求
-
Libc中定义的errno变量包含特定的出错码
应用程序、封装例程、系统调用处理程序及系统调用服务例程之间的关系:
xyz()函数代表系统调用封装成的编程接口,其中封装了一个系统调用,这个系统调用触发一个名为0X80的 中断,中断对应着内核中system_ call代码的入口,保存现场,然后执行API接口xyz()函数对应的中断服务程序sys_ xyz,如果需要进程调度,此时可以进行,执行结束之后恢复现场,返回到用户态xyz()函数的下一条指令继续执行。
系统调用的三个层次依次是:xyz函数(API)、system_ call(中断向量)和 sys_ xyz(中断服务程序)
关于传参:
内核实现了很多不同的系统调用,进程必须指明需要哪个系统调用,这需要传递一个名为系统调用号的参数,system_call是linux中所有系统调用的入口点,每个系统调用至少有一个参数,使用eax寄存器传递系统调用号。
寄存器传递参数具有如下限制:
- 每个参数的长度不能超过寄存器的长度,即32位
- 在系统调用号( eax)之外,参数的个数不能超过6个( ebx,ecx, edx, esi, edi, ebp)
- 超过6个的情况下,使用某一个寄存器作为指针,进入内核态之后可以访问所有的地址空间,通过某一片区域传递参数。
使用库函数API获取系统当前时间
利用API调用time函数,体现API的接口功能:
输出结果
C代码中嵌入汇编代码的写法
__asm__(
汇编语句模板:
输入部分:
输出部分:
破坏描述部分:);
include <stdio.h>
int main()
{
/*实现的功能:val1+val2=val3*/
unsigned int val1 = 1;
unsigned int val2 = 2;
unsigned int val3 = 0;
printf("val1:%d,val2:%d,val3:%d\n",val1,val2,val3);
asm volatile(
"movl $0,%%eax\n\t" /*两个%表示转义字符,这一句的目的是把%eax清零*/
"addl %1,%%eax\n\t" /*%1指的是参数的标记,m编号为0,c为1,d为2 这一句把val1赋给eax*/
"addl %2,%%eax\n\t" /*%eax=val1+val2*/
"movl %%eax,%0\n\t" /*把val1和val2的值存储在eax里面*/
: "=m" (val3) /* =表示把val3 的值写到内存变量里面*/
: "c" (val1),"d" (val2) /*用%ecx存储val1,用%edx存储val2*/
);
printf("val1:%d+val2:%d=val3:%d\n",val1,val2,val3);
return 0;
}
限定符对照表:
用汇编方式触发系统调用获取系统当前时间
"mov $0,%%ebx\n\t"/*将ebx清零*/
"mov $0xd,%%eax\n\t"/*将0xd放入eax中,传递系统调用号,13是time的系统调用号*/
"int $0x80\n\t"
"mov %%eax,%0\n\t"/*系统调用和普通函数一样返回值在eax中存储*/
:"=m"(tt)
实验—使用库函数API和C代码中嵌入汇编代码两种方式使用同一个系统调用
本实验中使用20号系统调用getpid来获取当前进程的pid
使用库函数API获取当前进程的pid
代码:
结果:
用汇编方式触发系统调用获取当前进程的pid
代码:
结果:
代码分析:
"movl $0x14,%%eax\n\t"/*将getpid系统调用号20放入寄存器eax中*/
"int $0x80\n\t"/*系统发生中断,执行getpid进程*/
"movl %%eax,%0\n\t"/*返回值保存在eax中,并将eax的值放在0号变量tt中输出*/
:"=m"(tt)
参考资料
【原创作品转载请注明出处】 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000