【README】
本文内容总结自 《操作系统-哈工大李治军老师》,内容非常棒,墙裂推荐;
【1】操作系统接口
0)用户使用计算机3种方式:
- 命令行; 命令行执行 hello world
- 图形界面;如计算机磁盘浏览器,c,d盘;
- 应用程序;如word;
1)命令行
系统启动完成后, 会执行shell主体程序, 并打印出 /home/username/ 字符串; 并scanf 等待用户输入;用户输入后,执行函数 fork() exec() 申请cpu执行 output.c 代码;
具体的:
- 调用Fork() exec() ,实现对cpu使用;
- 调用scanf() ,实现对键盘的使用;
- 调用printf(),实现对显示器的使用;
【总结】命令行就是一段程序,程序调用了一些函数,来对计算机硬件进行使用;
- 无论命令行,还是图形界面,都是普通C程序;关键是调用了重要函数,这才可以操作计算机硬件;
【1.1】操作系统接口
1)操作系统接口:
- 指的就是 重要函数,这些函数是操作系统对外提供的接口;即,接口表现为 函数调用,又由操作系统提供,所以称为系统调用 system call;
- 所以操作系统接口就是系统调用;
2)有哪些具体的操作系统接口? 或系统调用呢?
2.1)操作系统接口标准: POSIX ;为操作系统接口定义了统一的标准;可以让相同的应用程序可以在不同的操作系统上运行,即调用系统接口;
POSIX(可移植操作系统接口)是一个公认的工业标准。规范中的操作系统是POSIX兼容的。 |
2.2)到哪里去查询系统调用, posix 查询;
X 通常都指 Unix, 这是惯例;
Posix 常用系统调用:
8.1 posix --The most common POSIX system calls.
【小结】什么是操作系统接口?被称为系统调用的函数;
- 具体来说,如 fork,open,read,write 等函数(如上图);
- 系统调用函数可以理解操作系统开放出来的操作操作系统的api;
【2】系统调用实现
【2.1】为啥不直接访问内核数据
不能直接访问内核内存,需要通过系统调用访问内核数据;
- 原因在于 内核有很多关键数据是不能被外泄的,如root用户名密码,而且访问可能带来被恶意修改的问题;内核被修改,可能导致操作系统不可用,带来严重后果;(不能随意调用内核数据,不能随意jmp的原因)
【2.2】内核态与用户态
0)引入硬件设计,把内存分为内核段与用户段;计算机对内存的使用都是一段一段的使用;
- 用户段的程序不能直接访问内核段的数据,只能通过系统调用接口来完成;
- 用户段,内核段都需要段寄存器来做,两个段寄存器 CS DS,代码段寄存器,数据段寄存器;
1)如何区分 内核态与用户态
- 0 表示内核态, 3 表示用户态;
2)可以看下 DPL >= CPL
- DPL- destination privilege level:用于描述目标内存段的特权级;存储在 GDT表中; 特权级数字越大,权限越低;(DPL存储在GDT中)
- CPL-current privilege level: 当前特权级; (CPL存储在cs段寄存器中)
3) DPL CPL 有什么作用
- 只有当当前特权级 CPL 小于等于 目标函数的DPL时,才允许访问;否则访问权限不足;(DPL CPL 数值越小,级别越高)
4) 用户态请求内核态
- 用户态的DPL是3,在执行用户态时,会请求cs段寄存器,会把DPL赋值给cs中的CPL;
- 而内核态的DPL是0;
- 当用户态访问内核态时, 因CPL=3,而内核态DPL=0,权限不足,所以访问段寄存器就会报错了;
【例】用户态访问内核态权限不足
序号 | 代码 | 内存区域 | 描述 |
1 | void main() { whoami(); } | 用户态 | 特权级CPL=3,CPL保存在cs段寄存器中; |
2 | void whoami() { printf(100,8); } | 内核态 | 特权级CPL=0; 所以代码1 无法访问代码2 ; CPL保存在cs段寄存器中; |
【补充】GDT
- 一个GDT表项就描述一段内存;GDT表示整个操作系统,所以gdt表项,无论数据段还是代码段,对应gdt表项的DPL 全等于0;
5)那用户态如何访问内核态的代码呢?
- 通过中断;计算机提供的唯一进入内核的方法,通过中断才能进入内核;
6)系统调用核心:
- 用户程序中包含一段包含 int指令的代码;
- 操作系统写中断处理,获取想调用程序的编号;
- 操作系统根据编号执行代码;
【2.3】 系统调用具体实现
操作系统通过 int 0x80 这条中断指令,操作系统才能进去;
Printf() 调用 printf库函数, printf库函数调用 write库函数,write库函数调用 write系统调用;write库函数被编译成宏,需要用宏 _syscall3 来展开成一段包含 int 0x80中断的 汇编代码,因为 int 中断时进入内核的唯一方式;
【小结】
- 所以系统调用细节从宏 _syscall3 开始说起;
【2.4】linux系统调用实现细节
1)syscall3 宏
通过实参我们知道;
- type赋值为int;
- name赋值为write;
- atype赋值为int;
- a赋值为fd;
把type,name,atype,a 替换为右边的值, 就会得到
int write(int a, int b, int c)
{long __res;__asm_volative // 这个是内嵌汇编指令;。。。。。。
}
其中
- “=a”(__res) :把 %eax寄存器数据 赋值给 __res 变量,作为返回值;
- “”(__NR_##name) 把 __NR_write赋值给 %eax 寄存器;
- “b”(long(a)) 把fd赋值给 %ebx寄存器;
- “c((long)b)” 把 b 赋值给%ecx 寄存器;
- 其中, __NR_write 是系统调用号,用来标识是哪种系统调用, 如write 为 __NR_write , open为 __NR_open 等;
【补充】
- syscall3 表示有3个参数;syscall2 表示有2个参数;
【总结】系统调用步骤
- 把一个系统调用号 赋值给eax;
- 调用 int 0x80 ,进入内核;(进入内核的唯一方法依靠int指令)
【2.5】int 0x80 (中断指令)
1)int 中断指令, 要查询 IDT 表;通过IDT 找到中断要转到哪个地方去执行(转到哪个中断服务程序执行);
2) IDT 中断描述符表结构
处理函数入口点偏移 | P | DPL | 01110 | |
段选择符 | 处理函数入口点偏移 |
补充:
- 中断是计算机设计中的里程碑的创新;
通过 set_system_gate(0x80, &system_call) 直到 0x80中断跑去 system_call 执行,即初始化 中断描述符表 IDT表;
当中断发生时,查找 IDT,找到中断处理函数入口地址,并跳转到中断处理函数执行;
3)看代码
_set_gate(&idt[n], 15, 3, addr) // idt 是中断向量表基址;
3赋值给 dpl;又main函数入口的调用程序的cpl等于3,所以main函数可以访问内核态;
- 换句话说,int80的在IDT的初始化故意把dpl设置为3了;
&system_call 赋值给 addr;0x0008 赋值给 段选择符;
所以 cs:ip = 8 : &system_call ;跳转到 cs:ip 去执行 system_call 这个函数;
补充:
- cs=0x0008;又 cs的最后两位表示CPL;所以 CPL = 00;
CS段寄存器结构 | |||
0000 | 0000 | 0000 | 1000 |
main函数 发起系统调用,如 write;此时 CPL=3,是用户态;
write系统调用通过中断 int 0x80,从 IDT 中断描述符表 查找80中断的服务程序入口地址;
入口地址通过 cs:ip 来表示,其中cs=0x0008 ,由cs的最后两位表示CPL=00=0所以进入内核态调用 system_call 执行,特权级变成0了;
【总结】
- 在初始化IDT的时候,80号中断的DPL设置为3, 故意让 CPL=3 的用户代码能够进来,一旦进来后,又由于 cs设置为0x0008 其CPL=00=0,所以进入内核态的system_call 执行;
【2.6】system_call 功能
1) 代码1:
movl 0x10, %edx mov %dx, %ds mov %dx, %es
把ds es 都设置为0x10 ;
补充: 0x08是内核的代码段; 0x10是内核的数据段; |
2) 代码2:
Call_sys_call_table (, %eax, 4) ;
根据 eax% 跳转到 sys_call_table 去执行; 其中 eax是 __NR_write,系统调用号;
即跳转到 _sys_call_table 为基址的,以 4*%eax 为偏移量的地址去执行;
为啥是4? 每个函数的指针为4个字节,32位;
也可以 把 sys_call_table 理解为函数表;
【2.7】函数表 sys_call_table (存储函数指针的表)
以 __NR_write=4为例,则下标为4的元素就是 sys_write ;
【小结】写文件调用过程
- Step1)用户调用 printf; CPL=3;
- Step2)printf 展成 int 0x80 ;而在 初始化 IDT时,把 80号中断服务处理程序设置为
- sys_call;补充:80号中断的 IDT表项的DPL初始化为3 ;
- Step3)调用 system_call 中断处理程序时, cs:ip=0x0008:&system_call,其中cs的DPL=0,所以 CPL=0,进入内核态;
- (补充 cs=0x0008表示代码段,ds=0x10表示数据段)
- Step4)system_call 根据传入的 系统调用号 __NR_write=4 会查询 sys_call_table 函数表项 得到 sys_write;
- Step5)调用 sys_write 函数;
【问题】
main函数所在内存的DPL=3;whoami所在内存的DPL=0,所以main函数无法直接调用 whoami函数;
解决方法:硬件提供了唯一渠道,即中断,让用户态访问内存态的程序;
- 步骤1)设置 eax=72,72等于 whoami函数的系统调用号;int 0x80 发生中断;
- 步骤2)又 80号中断在IDT初始化时的中断服务程序是 system_call;
- 步骤3)system_call 根据 eax=72 查询 sys_call_table 系统调用函数表的72号元素值 sys_whoami;
- 步骤4)执行 sys_whoami() 函数;