• 前言
  • 操作系统的接口
    • 什么是操作系统的接口
    • 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了。

参考资料

哈工大李志军操作系统

操作系统(二) -- 操作系统的接口与实现相关推荐

  1. 操作系统(二): 进程与线程

    操作系统(二): 进程与线程 本章解读 进程管理是操作系统重点中的重点,涵盖了操作系统中大部分的知识和考点.其主要包括四部分:进程与线程,处理器调度,同步与互斥,死锁.所以我准备分四个部分来解释这四个 ...

  2. 【操作系统】—操作系统的概念 目标和功能

    [操作系统]-操作系统的概念 目标和功能 本章节的思维导图 一.操作系统的概念 操作系统(Operating System,OS)是指控制和管理整个计算机系统的硬件和软件资源,并合理的组织调度计算机的 ...

  3. 【操作系统】操作系统的概念、功能和目标

    目录 一.熟悉的操作系统 二.操作系统的概念和定义 1.结合生活经验来理解计算机系统的层次结构 2.操作系统 三.操作系统的功能和目标 1.作为系统资源的管理者 1.1 提供的功能 1.2 目标 2. ...

  4. 操作系统之操作系统的作用、目标、发展过程、特性和主要功能

    操作系统引论 文章目录 操作系统引论 操作系统的目标和作用 操作系统的目标 操作系统的作用 操作系统的发展过程 未配置操作系统的计算机系统 单道批处理系统 多道批处理系统 分时系统 实时系统 推动操作 ...

  5. 详解操作系统中的接口

    文章目录 前言 一.一个问题 二.从广义上的接口引出操作系统的接口 三.我们的学习任务 不仅要知道接口是什么,还要知道它在内部是怎么实现功能的 四.开始说说操作系统的接口到底是什么 1.什么时候要用到 ...

  6. 操作系统的接口与实现

    文章目录 前言 一.接口 1.接口的定义 2.接口分类 二.系统调用的实现 1.系统调用 2.具体实现 总结 前言 当操作系统运行到main程序中有这样一行代码 if(!fork()){init(); ...

  7. 【操作系统】操作系统的概述

    [操作系统]操作系统的概述 一.操作系统的概念(定义) 二.操作系统的功能和目标 (一).资源的管理者 (二).向上层提供方便易用的服务 GUI:图形化用户接口(Graphical User Inte ...

  8. 【操作系统】—操作系统的发展与分类

    [操作系统]-操作系统的发展与分类 本章的思维导图如下 一.手工操作阶段 手工操作阶段的主要缺点:用户独占全机.人机速度矛盾导致资源利用率很低 二.批处理阶段-单道批处理系统 引入脱机输入/输出技术( ...

  9. 【操作系统】—操作系统的四个特征

    [操作系统]-操作系统的四个特征 本章节的思维导图如下 一.操作系统的特征-并发 并发:是指两个或者多个事件在同一时间间隔内发生.这些事件宏观上是同时发生的,但是微观上是交替发生的. 并行:指两个或者 ...

最新文章

  1. 【综述】MV3D-Net、AVOD-Net 用于自动驾驶的多视图3D目标检测网络
  2. 【STM32】低功耗相关函数和类型
  3. C# 利用反射机制开启控件双缓存
  4. oracle之三手工不完全恢复
  5. 如何让Toast响应点击事件等基础Android基础文章N篇
  6. QT的安装及环境配置
  7. hdoj6298:Maximum Multiple(找规律,总结)
  8. 代码实现 | 方程组的实现
  9. 弹力球C语言课程设计,弹力球游戏c语言代码
  10. Android 中使用AlarmManager设置闹钟详解
  11. 360浏览器如何设置默认极速模式
  12. Excel巧做项目管理
  13. VS2015中更改项目名称
  14. 如何在Mac上安全彻底的卸载软件?
  15. java毕业生设计中小型连锁超市配送中心配送管理计算机源码+系统+mysql+调试部署+lw
  16. 实验二 Java基础语法练习-基本数据类型、运算符与表达式、选择结构
  17. $http的使用方式
  18. 关于RegisterClass的注册位置
  19. webpack:两小时极速入门
  20. 企业年会直播方案,看完这份就够了

热门文章

  1. winform新手学习小结
  2. jsf 的导航演示(navigation)
  3. Simulink信号源及信号处理
  4. SpringCloud---搭建Eureka服务治理和发现
  5. Camera sensor bring up
  6. When Does Label Smoothing Help?
  7. 最高院《关于修改民诉证据的决定》施行!一文get可靠电子数据存证
  8. Tsukinai的第四十九个程序(在字符串每个字符间插入一个空格)
  9. unity shader forge右侧方法栏
  10. python制作购物网站开题报告_网上购物网站的开题报告