操作系统接口和调用--02

x33g5p2x  于2022-07-04 转载在 其他  
字(3.3k)|赞(0)|评价(0)|浏览(604)

什么是接口

日常生活中有很多接口:电源插座;汽车油门…

那什么是接口?

  • 连接两个东西、信号转换、屏蔽细节…

什么是操作系统接口?

  • 连接上层用户和操作系统软件

  • 什么是操作系统接口? 都是命令吗?…
  • 问题:操作系统直接面对用户吗?
  • 即用户是怎么用操作系统的?..

会学习从会问问题开始…

操作系统接口并不是直接暴露给用户使用的,用户是通过应用软件间接调用到操作系统接口的。

命令行是怎么回事?

  • 所谓命令行,不过是一个用C语言编写出来的程序,即shell就是一个c语言程序,可以把shell程序简化为上那段死循环程序,不断得等待用户输入命令,然后调用exec(cmd)去解析执行命令,然后继续等待用户输入
  • 在程序执行过程中会去调用相关函数,这些函数底层会去调用系统接口,完成对屏幕输出操作,cpu执行操作等。

图形按钮又是怎么回事?

图形化界面本质是内核接收到硬件输入后,会将消息放入队列中,应用程序可以不断去查询队列,从而去消费相关消息,对不同的消息,应用程序可以采取不同的处理方式。
类似生产者消费者模型

再回到那个问题 什么是操作系统接口?

上面讲到了,程序的执行就是普通C语言代码加上部分重要函数完成的,那么这部分重要的函数其实底层就会调用到操作系统内核去给我们提供的系统函数,因此我们调用这些系统函数也被称为系统调用。

例如: printf函数,调用后会向屏幕输出字符,其实该函数就可以看做一个接口,因为用户无需关心为什么该函数调用后,就可以像屏幕输出字符串。 而该函数底层实际还需要调用操作系统提供的接口,完成向屏幕的输出,但是这一切,用户无需关心,这就是接口的魅力。

用一个概念来回答问题:什么是操作系统接口?

操作系统接口是操作系统内核区中提供的相关函数,这些函数封装了常用的复杂操作,利用向屏幕输出内存,创建进程,创建目录等。

应用程序需要调用操作系统接口,完成相关操作,为了确保应用程序编写完成后,可以再不同的操作系统上运行,就需要确保各个操作系统内部提供的操作系统接口是相同的。

因此就有了IEEE制定的统一操作系统的相关接口。

例如上面讲到的printf函数,底层就是通过调用操作系统提供的write接口来完成对屏幕的输出操作。

系统调用的实现

系统调用的直观实现

问题+直观想法

假设我们要写一个程序,该程序可以打印出登录当前操作系统的用户,即下面的whoami(),登录操作系统的用户名和密码都是存放操作系统内核区中100地址处,大家思考一下,我们的程序能直接访问该地址,然后输出该地址保持的用户名吗?

  • 如果程序可以随意访问并修改内核区中的代码,显然会存在很大的安全隐患,比如你网上下载了一个软件,结果这个软件直接把你内核区代码乱搞一通,这不凉凉

用户区程序无法直接访问内核区

既然用户区程序没法直接访问内核区代码,那么该怎么进行间接访问呢?

操作系统底层是如何实现的,可以让用户区无法访问内核区代码

内核(用户)态,内核(用户)段

操作系统底层还是需要靠硬件实现来确保用户区无法访问内核区。

计算机对内存的使用都是一段一段的使用,处于用户段的程序不能跳过用户段使用。

而对段的区分,实际靠的是段寄存器完成的.

DPL用来描述要访问的目标内存段的特权级

当操作系统启动的时候,通过head.s初始化gdt表,该表中会记录每个地址段相关信息,包括访问权限。

不清楚gdt表和操作系统启动流程的,建议先复习一下上一篇文章: 操作系统启动篇–01

内核段的访问权限在初始时被设置为了0,而用户段的访问权限被设置为了3,数字越小,优先级越高。

CPL表示当前所处代码段的优先级,用当前cs低两位表示

CPL RPL与DPL 之间的区别和联系

如果当前应用程序处于用户态,即此时CPL=3,而要访问内核态的某个段地址,对应段地址的DPL=0,因此DPL>=CPL的检查不通过,无法访问

而如果是内核态要访问用户态,此时CPL=0,DPL=3,DPL>=CPL成立,可以访问

再思考一个问题,通过特权级限制了用户态对内核态的访问之后,那么又如何打开一扇门,让用户态可以调用操作系统相关接口呢?

硬件提供了“主动进入内核的方法”

硬件提供了中断功能,来让程序可以调用操作系统相关的接口,当调用int指令进行中断的时候,会将CS中的CPL设置为0,然后去调用内核区中的系统接口。

  • c语言关于文件操作实际是调用了相关的int中断,通过中断号,操作系统查表找到对应的中断程序地址,然后执行对应的中断程序,从而完成文件相关操作,这里实际调用的是操作系统提供的open接口

系统调用的实现

应用程序调用C库函数提供的printf函数,库函数中实现的printf函数调用了write库函数,在这期间会对参数进行相关格式转换,让其适配write库函数需要的参数。

write库函数,会通过一段包含int 0x80的中断代码去调用操作系统提供的wirte接口。

将关于write的故事完整的讲完…

c库函数中的_syscall3函数主要功能如下:

  • 触发0x80号中断
  • 将中断号放入eax中
  • 操作系统根据中断号加系统调用号,可以定位到具体中断程序的地址,然后调用执行
  • 实际上是通过中断号和系统调用号去IDT表中查询到对应中断程序的地址,然后调用执行的

大家思考一下: 如果IDT表中对应0x80中断程序表项的DPL=0,而我们当前用户区程序的CPL=3,显然是无法访问的,如果可以调用存在两种情况:

  • 访问LDT对应0x80中断表项过程中,会将当前CPL设置为0,
  • 0x80中断表项的DPL被故意设置为了3,好让用户程序可以去访问

int 0x80中断的处理

上面通过查询IDT表,找到了对应的0x80中断,并进行了执行,那么LDT表是怎么被初始化,里面每个表项长什么样子的呢?

初始化好了IDT表中int 0x80对应的表项,主要设置0x80中断程序在LDT表中对应表项的数据,该表项中设置当前中断程序的DPL=3,段选择符(CS–段选择子)为8,设置处理函数入口点偏移,即中断函数偏移地址。

当用户程序(CPL=3)调用int 0x80中断时,首先去查询LDT表,因为0x80中断程序对应的表项DPL已经被设置为了3,因此用户程序可以直接访问该表项。

要跳到对应的中断函数地址,就是改变cs:ip的值,这里cs的值就是段选择符,cs是段选择子,为8,cs后两位为00,表示当前CPL=0,

而ip为中断函数偏移地址,cs为8,不光改变当前CPL=0,还表示回去GDT表寻找下标为8的表项,得到其对应段地址和ip相加,得到最终中断处理程序的地址。

因此通过0x80中断跳到中断处理程序执行的时候,因为CPL被设置为0,因此特权级发生了改变,非常巧妙。

小结:

  • ldt表在初始化的时候,将LDT表中0x80中断表项的DPL设置为了3,方便用户程序访问,而当通过0x80中断表项跳到真正中断处理程序时,又将CPL设置为了0,让其可以访问内核区中的函数。
  • 当中断程序执行结束后,会将CPL重新设置为3,回到用户态

如果还不清楚,也可以看看下面这篇文章的分析:

系统调用:用户级函数如何通过INT 80中断进入操作系统内核

中断处理程序: system_call

下面我们进入system_call代码,也即真正的中断处理程序linux/kernel/system_call.s在程序中,跳到一个表里面的一个函数地址

eax存放系统调用号

_sys_call_table是一个函数地址表,其中write对应的处理函数的地址就放在表的%eax索引处
乘4是因为函数表中每个函数地址占据4个字节

sys_write才是真正的处理函数,可以对内核中的数据进行读取,也就是所谓的系统调用

  • 用户程序在用户态调用printf函数
  • printf函数展开为int 0x80,去LDT表中查询到对应的0x80表项(可以访问是因为LDT初始化0x80表项时,设置其DPL=3)
  • 定位到对应的0x80表项后,重新设置CS:IP,此时CPL被设置为了0,然后CS作为段选择子,会去GDT表查询出对应的表项,得到对应的段地址,然后和IP拼接,跳转到对应的中断处理程序处(可以访问是因为当前CPL=0)
  • 执行system_call内核函数,通过传入的系统调用号,去函数表中定位到对应的函数,然后执行函数

相关文章