日常生活中有很多接口:电源插座;汽车油门…
那什么是接口?
操作系统接口并不是直接暴露给用户使用的,用户是通过应用软件间接调用到操作系统接口的。
图形化界面本质是内核接收到硬件输入后,会将消息放入队列中,应用程序可以不断去查询队列,从而去消费相关消息,对不同的消息,应用程序可以采取不同的处理方式。
类似生产者消费者模型
上面讲到了,程序的执行就是普通C语言代码加上部分重要函数完成的,那么这部分重要的函数其实底层就会调用到操作系统内核去给我们提供的系统函数,因此我们调用这些系统函数也被称为系统调用。
例如: printf函数,调用后会向屏幕输出字符,其实该函数就可以看做一个接口,因为用户无需关心为什么该函数调用后,就可以像屏幕输出字符串。 而该函数底层实际还需要调用操作系统提供的接口,完成向屏幕的输出,但是这一切,用户无需关心,这就是接口的魅力。
操作系统接口是操作系统内核区中提供的相关函数,这些函数封装了常用的复杂操作,利用向屏幕输出内存,创建进程,创建目录等。
应用程序需要调用操作系统接口,完成相关操作,为了确保应用程序编写完成后,可以再不同的操作系统上运行,就需要确保各个操作系统内部提供的操作系统接口是相同的。
因此就有了IEEE制定的统一操作系统的相关接口。
例如上面讲到的printf函数,底层就是通过调用操作系统提供的write接口来完成对屏幕的输出操作。
问题+直观想法
假设我们要写一个程序,该程序可以打印出登录当前操作系统的用户,即下面的whoami(),登录操作系统的用户名和密码都是存放操作系统内核区中100地址处,大家思考一下,我们的程序能直接访问该地址,然后输出该地址保持的用户名吗?
用户区程序无法直接访问内核区
既然用户区程序没法直接访问内核区代码,那么该怎么进行间接访问呢?
操作系统底层是如何实现的,可以让用户区无法访问内核区代码
操作系统底层还是需要靠硬件实现来确保用户区无法访问内核区。
计算机对内存的使用都是一段一段的使用,处于用户段的程序不能跳过用户段使用。
而对段的区分,实际靠的是段寄存器完成的.
DPL用来描述要访问的目标内存段的特权级
当操作系统启动的时候,通过head.s初始化gdt表,该表中会记录每个地址段相关信息,包括访问权限。
不清楚gdt表和操作系统启动流程的,建议先复习一下上一篇文章: 操作系统启动篇–01
内核段的访问权限在初始时被设置为了0,而用户段的访问权限被设置为了3,数字越小,优先级越高。
CPL表示当前所处代码段的优先级,用当前cs低两位表示
如果当前应用程序处于用户态,即此时CPL=3,而要访问内核态的某个段地址,对应段地址的DPL=0,因此DPL>=CPL的检查不通过,无法访问
而如果是内核态要访问用户态,此时CPL=0,DPL=3,DPL>=CPL成立,可以访问
再思考一个问题,通过特权级限制了用户态对内核态的访问之后,那么又如何打开一扇门,让用户态可以调用操作系统相关接口呢?
硬件提供了中断功能,来让程序可以调用操作系统相关的接口,当调用int指令进行中断的时候,会将CS中的CPL设置为0,然后去调用内核区中的系统接口。
应用程序调用C库函数提供的printf函数,库函数中实现的printf函数调用了write库函数,在这期间会对参数进行相关格式转换,让其适配write库函数需要的参数。
write库函数,会通过一段包含int 0x80的中断代码去调用操作系统提供的wirte接口。
c库函数中的_syscall3函数主要功能如下:
大家思考一下: 如果IDT表中对应0x80中断程序表项的DPL=0,而我们当前用户区程序的CPL=3,显然是无法访问的,如果可以调用存在两种情况:
上面通过查询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,因此特权级发生了改变,非常巧妙。
小结:
如果还不清楚,也可以看看下面这篇文章的分析:
系统调用:用户级函数如何通过INT 80中断进入操作系统内核
下面我们进入system_call代码,也即真正的中断处理程序linux/kernel/system_call.s在程序中,跳到一个表里面的一个函数地址
eax存放系统调用号
_sys_call_table是一个函数地址表,其中write对应的处理函数的地址就放在表的%eax索引处
乘4是因为函数表中每个函数地址占据4个字节
sys_write才是真正的处理函数,可以对内核中的数据进行读取,也就是所谓的系统调用
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://cjdhy.blog.csdn.net/article/details/125581062
内容来源于网络,如有侵权,请联系作者删除!