什么是系统调用
上层应用是怎么使用操作系统的呢?操作系统提供系统调用来供上层应用来和操作系统交互,就相当于OS为上层应用暴露了一堆接口,应用使用这些接口来完成相应的功能。举个例子,当你使用printf("hello, world!");
时,printf
是怎么把 hello, world! 输出到控制台上的呢?这里就是printf
通过系统调用接口write
,将其输出到了控制台上。那为什么不是printf
直接将内容输出,还要通过系统调用write
来做呢?展示控制台的这个屏幕,称其为显存,说白了就是一块硬件,那么你在屏幕上显示hello, world!
,其实就是在往显存这个硬件中写入数据,而往硬件中写入数据是进程控制CPU去做的,也就是内核部分做的事情,所以write
这个动作就属于系统调用了,需要使用内核态去完成write
这个动作。那么就知道了为什么printf
不能直接在屏幕输出内容,在屏幕输出内容相当于在硬件写入数据,需要内核态去做这件事,而printf
属于用户态,是没有办法直接访问内核数据的,所以需要借助提供出来的write
系统调用接口来完成。
操作系统如何进行权限控制
上面提到了用户态和内核态,那么怎么区别正在执行的程序是属于内核态还是用户态呢,程序也就是一条条指令,也就是怎么区别当前的指令属于用户态还是内核态,就是通过该指令的地址判断的:CS:IP
。
CS和IP是8086CPU中两个关键的寄存器,它们指示了CPU当前要读取指令的地址。CS : 代码段寄存器;IP : 指令指针寄存器。在8086机中,任意时刻,CPU将CS:IP指向的内容当作指令来执行。
通过该指令的地址CS:IP
,CS
的低两位表示当前处于内核态还是用户态,0表示内核态,1表示用户态。当当前指令的CPL <= 要访问的数据段DPL时,才能够进行访问。
CPL:当前内存段的特权级。DPL:目标内存段的特权级。
printf(“hello, world!”)源码解析
前面我们提到了,printf
函数通过系统调用write
函数,将内容输出到显存上。其中还涉及到了用户态到内核态的转变,但是用户态的内存 CPL = 3,内核态的内存段 DPL = 0,本来是不能访问的,这里是怎么做到用户态调用内核态代码的呢?我们一点点来分析。
这里我们以linux.011举例,printf
函数最终会调用到内核态的sys_write
(_syscall3宏展开)
1 | int printf (const char *format, ...) { |
1 |
这里使用了一段内联汇编代码+宏,将宏展开后其实就是标准的write函数,这里不做代入,来看下这里最关键的int 0x80
,也是本文要描述的重点对象。
int 0x80
int 0x80
怎么解释呢,int
是一个中断指令,0x80代表使用idt
表中位与0x80位置的中断处理函数来进行中断处理。那么这里又牵扯到了idt
表,他又是个什么东西呢。
idt表: 在发生中断时,会选择一个中断处理函数去执行,idt表就是一个存放了所有中断处理函数地址的数组,当执行
int 0x80
的时候,就是从idt
表中取出0x80
对应存放的中断处理函数地址,并执行该中断处理函数。除了idt表外,还有gdt、ldt,详细可查阅资料了解。
也就是说,在执行int 0x80
的时候,就直接跳到了内核态,我们来看下int 0x80
到底做了些什么。
idt[0x80] 初始化过程
1 | void sched_init(void) { |
1 |
1 |
1 | typedef struct desc_struct { |
sched_init
调用了_set_gate(&idt[0x80], 15, 3, &system_call)
,这里的system_call
就是系统调用,我们将这里的四个参数一一对应下
- gate_addr -> idt[0x80],gate_addr就是idt表中下标为0x80的那个元素,根据idt的数据结构能看出,一个idt有64位。
- type: 15,这个跟类型相关的不是关注重点,可以忽略。
- dpl: 3,前面我们提到cpl为当前指令特权级,dpl为目标段特权级,可想而知,这里可能将目标段指令dpl置为了3
- addr -> 指中断处理函数地址,即
system_call
的地址
根据参数,我们大概可以推测出来一些东西,我们要将中断处理函数的地址、type、dpl全部塞到大小为64bit的idt[0x80]中,接下来就是解释下这段汇编做了什么就能理解怎么塞的了。
我用伪代码的形式重新描述下
1 | // init |
赋值完成,接下来我们看下idt详细的数据结构。
1 | struct gate_struct { |
1 | struct idt_bits { |
那么我们再来想下,这里的系统调用是怎么实现用户态代码CPL = 3,调用到DPL = 0的内核中断处理函数的
- 用户态代码访问idt[0x80]
- 用户态代码CPL与idt[0x80]的DPL都为3,可以访问
- 使用idt[0x80]查询
system_call
的地址,此时会使用到段选择符 - 这里的段选择符为
0x0008
,段选择符就是用来访问system_call
代码段地址的CS
,展开为CS=0000000000001000
,前面讲到CS的末两位即为CPL
,也就是说中断处理函数的代码段地址CPL = 0 - 所以总结来看,就是idt[0x80]帮助我们完成了用户态代码调用内核态的过程。