3.操作系统接口与系统调用

【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() 函数;

 

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值