操作系统(二) -- 操作系统的接口与实现
- 前言
- 操作系统的接口
- 什么是操作系统的接口
- POSIX标准
- 系统调用的实现
- 1,用户程序能不能直接调用系统内核
- 2,如果不能直接调用,为什么?如何实现的
- 3,用户程序如何才能调用系统内核
- 系统调用的核心:
- 具体实现:以printf为例
- 总结一下系统调用的实现:
- 参考资料
前言
前面说了操作系统启动时发生的事情,最后一个文件main.c中有这样一行代码:
if(!fork()){init();}
这行代码就是启动第一个进程,对于windows来说就是启动桌面,对于linux来说就是打开shell。这一篇文章说说操作系统的接口以及实现,即上层应用是如何穿过接口进入操作系统的。
操作系统的接口
什么是操作系统的接口
接口其实是一种抽象,比如插排,它将内部的电路全部封装起来,只提供两个插口,用电设备插上就能用;不用管插座内部是如何实现的。操作系统的接口也是如此,操作系统的接口其实就是一个个函数,知道它的功能然后直接调用就行,而不用管它内核里面是怎么实现的,因为这个函数是系统调用的,所以也称为系统调用。比如:write()、read()等等
POSIX标准
POSIX(Portable Operating System Interface of Unix),POSIX标准定义了操作系统应该为应用程序提供的接口标准,目的是为了增强程序的可移植性。
系统调用的实现
前面说的是操作系统的接口,说白了就是一个个函数,调用它们就可以使用相应的功能。那这些系统调用到底是如何实现的呢?下面就来解解密。解决三个问题:
- 1,用户程序能不能直接调用系统内核
- 2,如果不能直接调用,为什么?如何实现的
- 3,用户程序如何才能调用系统内核
1,用户程序能不能直接调用系统内核
不能
2,如果不能直接调用,为什么?如何实现的
如果能的话,那么你从网上下载一段程序就可能进入系统内核获取你的root密码,那么还有什么安全感呢?
但是操作系统和用户程序都是在内存里面,在内存里面是可以交换数据的呀?那为什么就不能直接使用jmp、mov或者函数调用直接进入操作系统内核呢?怎么实现的呢?
实现方法:利用硬件设计将内核程序与用户程序进行隔离,内核程序的所在的那段内存程称为核心态,用户程序所在的那段内存叫用户态。用户态的程序不能直接访问核心态的数据。
实现手段:利用CS的低两位CPL和DS的低两位DPL来实现隔离。首先在head.s里面建立gdt表的时候就将内核段DPL置为0,而CPL是当前指令的特权级,如果是在用户态,那么CPL就为3(如果是核心态就是0);在访问某个地址的时候,要看有没有权限访问,0的特权级是高于3,如果CPL的特权级小于等于DPL的特权级,那么就不能访问;注意:如果CPL=DPL是可以访问的;比如CPL=0(说明是内核态),DPL=3(说明是用户态),CPL的特权级大于DPL的特权级,所以能访问。也就是说内核态能访问内存的任意区域。这个隔离对于跳转指令(jmp、mov)同样有效。
3,用户程序如何才能调用系统内核
用户态不能直接访问内核态,那么有什么方法可以访问呢?方法肯定是有的,不然系统调用就实现不了了啊;用户态访问内核态只能通过一种途径,那就是中断,int指令将使CS中的CPL从3变为0,这样就可以访问了(即进入内核),这是用户程序发起的调用内核代码的唯一方式。并且这个中断号只能是0x80.
系统调用的核心:
1,用户程序中包含一段包含int指令的代码
2,操作系统中有中断函数表,从中可以获取中断服务函数入口地址
3,操作系统执行中断服务函数
具体实现:以printf为例
首先c代码里面的printf是这样的,printf(“%d”,a);在printf()内部其实是调用了系统函数write,而write函数的函数头其实是这样的:
ssize_t write(int fd, const void *buf, size_t count);
fd:要进行写操作的文件描述词。
buf:需要输出的缓冲区
count:最大输出字节计数
可以看到,printf()函数的形参和write()的形参是不一样的,因此如果printf(“%d”,a)能调用write函数的话,肯定要对printf的形参进行处理,使其符合write函数的格式,或者说换一种方式调用。在printf()函数里面调用write()如下所示:
# include <unisted.h>
_syscall3(int, write, int, fd, const char* buf, off_t, count)
可以看到其实利用的是_syscall3这个宏,这个宏的定义如下:
#define _syscall3(type,name,atype,a,btype,b,ctype,c)\
type name(atype a, btype b, ctype c) \
{ long __res;\
__asm__ volatile(“int 0x80”:”=a”(__res):””(__NR_##name),
”b”((long)(a)),”c”((long)(b)),“d”((long)(c)))); if(__res>=0) return
(type)__res; errno=-__res; return -1;}
_syscall3这个宏调用之后就是展开成上面的一段汇编代码,比如write调用:
_syscall3(int, write, int, fd, const char* buf, off_t, count)
就是将宏展开的代码中的
type=int,name=write,atype=int,a=fd,btype=const char * ,b=buf,ctype=off_t,c=count;
用这些来替换;因此
type name(atype a, btype b, ctype c)
就变成了
int write(int fd,const char * buf, off_t count)
这样,展开的汇编代码一样跟着变。这里需要注意的是int0x80这个中断;前面已经说过在head.s里面会重新建立idt表,之后中断就是表示根据中断号查那个表,然后获取中断服务函数的入口地址,int0x80这个中断就是进入操作系统内核,这是上层应用进入操作系统的唯一手段,int 0x80相当于是操作系统的一个门户,接着看_syscall3宏定义下面的代码:
long __res;\
__asm__ volatile(“int 0x80”:”=a”(__res):””(__NR_##name),
”b”((long)(a)),”c”((long)(b)),“d”((long)(c)))); if(__res>=0) return
(type)__res; errno=-__res; return -1;
这是一段内嵌汇编,冒号左边为输入,右边为输入,,上面代码最右边一个冒号右边是:”“表示与前面的a一样,即eax这个寄存器,所以”“(_NR##name)的意思就是将__NR_write赋值给eax这个寄存器,__NR_write称为系统调用号,后面有大用。
在linux/inlcude/unistd.h中
# define __NR_write 4
什么是系统调用号呢?所有的系统调用都是通过int 0x80这个中断来
调用的,那么如何区分是write调用还是read调用或者是其他调用呢?就是根据这个系统调用号来区分的,__NR_write表示write调用,会接着执行write对应的内核代码,__NR_read表示read调用,同理,其他的系统调用号也是如此。后面的
”b”((long)(a)),”c”((long)(b)),“d”((long)(c))
就是把形参的a、b、c依次赋值给ebx、ecx、edx三个寄存器;输入完成之后就通过int 0x80这个中断号进入操作系统,int 0x80这条指令执行完之后,eax中就会存放int 0x80的返回值,然后将这个返回值赋值给__res,__res就是int write()这个系统调用的返回值。write这个系统调用也就结束了。
总结一下_syscall3这个宏的用法:
调用这个宏可以理解为调用一个函数,宏的定义:
#define _syscall3(type,name,atype,a,btype,b,ctype,c)
type 表示函数返回值,name表示函数名,后面分别是三个形参的类型和行参名。
name不同,系统调用号不同,所以调用_syscall3之后执行的代码不同,在宏里面通过
int 0x80进入系统内核并将指条指令的结果存在eax寄存器中,然后返回到宏的调用处。
具体再扒一下:
前面说的int 0x80都是用“这条指令“来表示了,那么int 0x80到底
是什么呢?int 0x80是进入中断服务函数的一条指令。
int 指令首先要查idt表转去哪里执行。
void sched_init(void)
{ set_system_gate(0x80,&system_call); }
int 0x80对应的中断处理程序就是system_call,从这个init就知道这是一个初始化,0x80这个中断就是用后面这个system_call来处理,那么系统是怎么设置的呢?通过set_system_gate这个宏。
在linux/include/asm/system.h中
#define set_system_gate(n, addr) \
_set_gate(&idt[n],15,3,addr); //idt是中断向量表基址
set_system_gate这个宏又调用了_set_gate这个宏,
在linux/include/asm/system.h中
#define _set_gate(gate_addr, type, dpl, addr)\
__asm__(“movw %%dx,%%ax\n\t” “movw %0,%%dx\n\t”\
“movl %%eax,%1\n\t” “movl %%edx,%2”:\
:”i”((short)(0x8000+(dpl<<13)+type<<8))),“o”(*(( \
char*)(gate_addr))),”o”(*(4+(char*)(gate_addr))),\
“d”((char*)(addr),”a”(0x00080000))
这里我也看不懂,但是我知道_set_gate这个宏的作用就是建立一个类似这样的下图表,处理函数入口点偏移=system_call,DPL就是3,段选择符就是0x0008,即CS是8。
用户态的程序如果要进入内核,必须使用0x80号中断,那么就必须先要进入idt表。用户态的CPL=3,且idt表的DPL故意设置成3,因此能够跳到idt表,跳到idt表中之后就能找到之后程序跳转的地方,也就是中断服务函数的起始地址,CS就是段选择符(8),ip就是”处理函数入口点偏移“。记不记得setup.s里面有一行
jmpi 0,8
这条指令表示根据gdt表跳转到内核代码的地址0处。CS=8,ip=system_call就是跳到内核的system_call这个函数;另外如果CS=8,那么CPL=0,因为CPL是CS最低两位。也就是说当前程序的特权级变了,变成内核态的了。完整流程:初始化的时候0x80号中断的DPL设成3,让用户态的代码能跳进来,跳进来之后根据CS=8将CPL设为0,到了内核态,到了内核态就什么都能干了,将来int 0x80返回的之后,CS最后两位肯定变成3,变成用户态。
中断处理函数system_call到底做了什么呢?
在linux/kernel/system_call.s中
nr_system_calls=72
.globl _system_call
_system_call: cmpl $nr_system_calls-1,%eax
ja bad_sys_call
push %ds push %es push %fs
pushl %edx pushl %ecx pushl %ebx //调用的参数
movl $0x10,%edx mov %dx,%ds mov %dx,%es //内核数据
movl $0x17,%edx mov %dx,%fs //fs可以找到用户数据
call _sys_call_table(,%eax,4) //a(,%eax,4)=a+4*eax
pushl %eax //返回值压栈,留着ret_from_sys_call时用
... //其他代码
ret_from_sys_call: popl %eax, 其他pop, iret
前面都是压栈和赋值,接着调用了_sys_call_table(,%eax,4)。
a(,%eax,4)=a+4*eax_sys_call_table(,%eax,4)=_sys_call_table+4*%eax;这是一种寻址方式。eax是系统调用号,那_sys_call_table是什么?
在include/linux/sys.h中
fn_ptr sys_call_table[]=
{sys_setup, sys_exit, sys_fork, sys_read, sys_write,
...};在include/linux/sched.h中
typedef int (fn_ptr*)();
sys_call_table是一个fn_ptr类型的全局函数表,fn_ptr是一个函数指针,4个字节,这就是_sys_call_table+4*%eax;这里为什么要*4的原因,sys_call_table的每一项都是4个字节,然后就可以根据eax来知道要调用的真正中断服务函数的入口地址了,对于write系统函数来说,就是sys_write。
总结一下系统调用的实现:
printf ->_syscall3 ->write -> int 0x80 -> system_call -> sys_call_table -> sys_write
printf通用_syscall3这个宏调用write函数,在write函数里面用system_call来处理int 0x80,在system_call中会调用system_call_table这个表,根据eax中存储的系统调用号就可以找到真正的sys_write了。
参考资料
哈工大李志军操作系统
操作系统(二) -- 操作系统的接口与实现相关推荐
- 操作系统(二): 进程与线程
操作系统(二): 进程与线程 本章解读 进程管理是操作系统重点中的重点,涵盖了操作系统中大部分的知识和考点.其主要包括四部分:进程与线程,处理器调度,同步与互斥,死锁.所以我准备分四个部分来解释这四个 ...
- 【操作系统】—操作系统的概念 目标和功能
[操作系统]-操作系统的概念 目标和功能 本章节的思维导图 一.操作系统的概念 操作系统(Operating System,OS)是指控制和管理整个计算机系统的硬件和软件资源,并合理的组织调度计算机的 ...
- 【操作系统】操作系统的概念、功能和目标
目录 一.熟悉的操作系统 二.操作系统的概念和定义 1.结合生活经验来理解计算机系统的层次结构 2.操作系统 三.操作系统的功能和目标 1.作为系统资源的管理者 1.1 提供的功能 1.2 目标 2. ...
- 操作系统之操作系统的作用、目标、发展过程、特性和主要功能
操作系统引论 文章目录 操作系统引论 操作系统的目标和作用 操作系统的目标 操作系统的作用 操作系统的发展过程 未配置操作系统的计算机系统 单道批处理系统 多道批处理系统 分时系统 实时系统 推动操作 ...
- 详解操作系统中的接口
文章目录 前言 一.一个问题 二.从广义上的接口引出操作系统的接口 三.我们的学习任务 不仅要知道接口是什么,还要知道它在内部是怎么实现功能的 四.开始说说操作系统的接口到底是什么 1.什么时候要用到 ...
- 操作系统的接口与实现
文章目录 前言 一.接口 1.接口的定义 2.接口分类 二.系统调用的实现 1.系统调用 2.具体实现 总结 前言 当操作系统运行到main程序中有这样一行代码 if(!fork()){init(); ...
- 【操作系统】操作系统的概述
[操作系统]操作系统的概述 一.操作系统的概念(定义) 二.操作系统的功能和目标 (一).资源的管理者 (二).向上层提供方便易用的服务 GUI:图形化用户接口(Graphical User Inte ...
- 【操作系统】—操作系统的发展与分类
[操作系统]-操作系统的发展与分类 本章的思维导图如下 一.手工操作阶段 手工操作阶段的主要缺点:用户独占全机.人机速度矛盾导致资源利用率很低 二.批处理阶段-单道批处理系统 引入脱机输入/输出技术( ...
- 【操作系统】—操作系统的四个特征
[操作系统]-操作系统的四个特征 本章节的思维导图如下 一.操作系统的特征-并发 并发:是指两个或者多个事件在同一时间间隔内发生.这些事件宏观上是同时发生的,但是微观上是交替发生的. 并行:指两个或者 ...
最新文章
- 【综述】MV3D-Net、AVOD-Net 用于自动驾驶的多视图3D目标检测网络
- 【STM32】低功耗相关函数和类型
- C# 利用反射机制开启控件双缓存
- oracle之三手工不完全恢复
- 如何让Toast响应点击事件等基础Android基础文章N篇
- QT的安装及环境配置
- hdoj6298:Maximum Multiple(找规律,总结)
- 代码实现 | 方程组的实现
- 弹力球C语言课程设计,弹力球游戏c语言代码
- Android 中使用AlarmManager设置闹钟详解
- 360浏览器如何设置默认极速模式
- Excel巧做项目管理
- VS2015中更改项目名称
- 如何在Mac上安全彻底的卸载软件?
- java毕业生设计中小型连锁超市配送中心配送管理计算机源码+系统+mysql+调试部署+lw
- 实验二 Java基础语法练习-基本数据类型、运算符与表达式、选择结构
- $http的使用方式
- 关于RegisterClass的注册位置
- webpack:两小时极速入门
- 企业年会直播方案,看完这份就够了