视频内容知识学习
一、用户态、内核态和中断
1.内核态:处于高的执行级别下,代码可以执行特权指令,访问任意的物理地址,这时的CPU就对应内核态
2.用户态:处于低的执行级别下,代码只能在级别允许的特定范围内活动。在日常操作下,执行系统调用的方式是通过库函数,库函数封装系统调用,为用户提供接口以便直接使用。
3.Intel x86 CPU有四种不同的执行级别0-3,Linux只使用了其中的0 3级分别表示内核态和用户态。cs寄存器的最低两位表明了当前代码的特权级,00或者11。
4.内核态cs:eip的值是任意的,即可以访问所有的地址空间。用户态只能访问其中的一部分内存地址(0x00000000-0xbbbbbbbf),0xc0000000以上的地址(逻辑地址而不是物理地址)只能在内核态下访问。
5.中断处理是从用户态进入内核态的主要方式,系统调用是一种特殊的中断。从用户态切换到内核态时,中断/int指令会在堆栈上保存用户态的寄存器上下文,其中包括用户态栈顶地址、当时的状态字、当时的cs:eip的值,还有内核态的栈顶地址、内核态的状态字、中断处理程序的入口。中断发生后的第一件事就是保存现场,保存一系列的寄存器的值;中断处理结束前的最后一件事就是恢复现场,退出中断程序,恢复保存寄存器的数据。特别说明: 保护现场:就是进入中断程序,保存需要用到的寄存器的数据;恢复现场:就是退出中断程序,恢复保存寄存器的数据。
二、系统调用
1.系统调用的意义:操作系统为用户态进程与硬件设备进行交互提供了一组接口——系统调用。把用户从底层的硬件编程中解放出来,极大的提高了系统的安全性,使用户程序具有可移植性
2.操作系统提供的API和系统调用的关系。应用编程接口和系统调用是不同的,API只是一个函数定义,系统调用通过软中断向内核发出一个明确的请求
3.Libc库定义的一些API引用了封装例程(wrapper routine,唯一目的就是发布系统调用)。 一般每个系统调用对应一个封装例程,库再用这些封装例程定义出给用户的API
4.不是每个API都对应一个特定的系统调用。API可能直接提供用户态的服务,如:一些数学函数,一个单独的API可能调用了几个系统调用,不同的API可能调用了同一个系统调用
5.返回值。大部分封装例程返回一个整数,其值的含义依赖于相应的系统调用, -1在多数情况下表示内核不能满足进程的请求,Libc中定义的errno变量包含特定的出错码
6.系统调用的三层皮:xyz、system_call (中断向量)和sys_xyz(中断服务程序)
7.当用户态进程调用一个系统调用时,CPU切换到内核态并开始执行一个内核函数。在Linux中是通过执行int$0x80来执行系统调用的,这条汇编指令产生向量为128的编程异常,Intel Pentium II 中引入了sysenter指令(快速系统调用),2.6已经支持(本课程不考虑这个)
8.传参:内核实现了很多不同的系统调用,进程必须指明需要哪个系统调用,这需要传递一个名为系统调用号的参数,使用eax寄存器
9.系统调用也需要输入输出参数,例如:实际的值,用户态进程地址空间的变量的地址,甚至是包含指向用户态函数的指针的数据结构的地址
10.system_call是linux中所有系统调用的入口点,每个系统调用至少有一个参数,即由eax传递的系统调用号。一个应用程序调用fork()封装例程,那么在执行into$0x80之前就把eax寄存器的值置为2(即_NR_fork)。这个寄存器的设置是libc库中的封装例程进行的,因此用户一般不关心系统调用号,进入sys_call之后,立即将eax的值压入内核堆栈
11.寄存器传递参数具有如下限制: 1)每个参数的长度不能超过寄存器的长度,即32位 2)在系统调用号(eax)之外,参数的个数不能超过6个(ebx, ecx,edx,esi,edi,ebp) 3)超过6个怎么办?超过6个的话就把某一个寄存器作为一个指针,指向某一块内存。
三、使用库函数API来获取系统当前时间
声明了一个time_t tt
的变量,struct tm *t
tm为了输出时变成可读的,因为tt是int型的数值,通过gcc time.c -o time -m32
运行./time
就可获得当前的运行时间
四、C代码中嵌入汇编代码的写法
把0赋给eax,add%1指下面输出和输入的部分,“=m”(val3)输出为0,“c”(val1)为1,“d”(val2)为2,“c”指ecx寄存器存储val1的值,把ecx的值赋给eax,%2就是指val2,把它放到edx这个寄存器里面,val1+val2放在eax里面,然后把val1+val2存储的值放到%0,%0就是val3,=m就是写到内存变量里面去,这就实现了val1+val2=val3的功能。
实验
C程序库函数实现系统调用
代码如下:
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
int main () {
int ret = 0;
ret = mkdir ("./testdir",0777);
printf ("ret is: %d.
",ret);
return 0;
}
mkdir函数有两个参数,第一个是待创建的目录名称;第二个是模式,这里设置为0777。
编译成功并执行,在当前目录下生成目录testdir,如图所示
C程序嵌入式汇编实现系统调用
代码如下:
#include <stdio.h>
int main () {
int ret =0;
char *dir = "./testdir";
int mode = 0777;
asm volatile(
"movl $39, %%eax
"
"int $0x80
"
"movl %%eax, %0
"
: “=m”(ret)
:"b"(dir),"c"(mode)
);
printf("ret is: %d.
",ret);
return 0;
}
编译成功并执行,如图所示
使用汇编实现系统调用的核心知识点有两个:一是如何执行系统调用,二是如果系统调用有参数,如何将参数传给系统调用。
对于使用汇编语言执行系统调用的方法比较简单,只需要将系统调用的编号传给eax寄存器之后,执行int $0x80指令就可以了,对应代码如下:
movl $39, %%eax
int $0x80
对于传参,则需要借助除eax之外的通用寄存器(如:ebx,ecx,edx,csi,cdi),在执行汇编之前将参数依次存入ebc,ecx,....这些通用寄存器即可,对应代码如下
:"b"(dir),"c"(mode)
这里的“b”代表%ebx寄存器,“c”代表%ecx寄存器。需要注意的是,参数顺序一定要和ebx,ecx,....通用寄存器的顺序一致,如果不一致的话程序就无法正确识别参数。如:写成下面这样的话
:"c"(dir),"b"(mode)
虽然编译OK,但在执行的时候会报如下错误
$./a.out
ret is:-14
程序总结
通过对系统调用的两种代码实现方法的分析,我们可以知道C语言的API只不过是对OS底层系统调用的一次封装而已,本质上是通过系统中断实现的。
实验问题
如图所示
在定义char *dir="./testdir1"
;为什么生成的目录是testdir?如图所示
第7、8章课程学习
1.中断就是由硬件来打断操作系统,中断使得硬件得以发出硬件通知给处理器。
2.中断处理程序与其他内核函数的真正的区别在于,中断处理程序是被内核调用来响应中断的,而它们运行于我们称之为中断上下文的特殊上下文。中断处理程序是上半部——接收到一个中断,它就立即开始执行,能够被允许稍后完成的工作会推迟到下半部。
3.内核提供的接口包括注册和注销中断处理程序、禁止中断、屏蔽中断线以及检查中断系统的状态。
4.用于延迟Linux内核工作的三种机制:软中断、tasklet和工作队列。