【操作系统基础】系统接口与系统调用

本文参考MOOC哈工大操作系统课程与课件
主要基于Linux 0.11系统展开
”Author:Mayiming“

一、操作系统接口

1. 什么是操作系统接口?

如众多接口一样,接口的存在主要是为使用者提供一种方便易懂的途径使用一件东西(不必搞懂这件东西背后的实现原理),接口屏蔽了其背后的复杂性,提供了方便和简洁的使用方法。
操作系统中的接口体现的形式就是函数,函数的存在给使用者提供了方便的调用方法,屏蔽了函数内部实现的细节。

2. 操作系统接口体现在哪?

在linux中体现在命令行、shell、代码中执行的函数等,在windows中体现在界面按钮、各种应用程序。
这些内容的本质都是通过代码实现的,这些代码又是通过操作系统提供的接口来操作计算机硬件实现的具体功能。

3. 命令行是怎么执行代码的?

例如:我们执行以下命令

hostname:~$ ./output "hello"
ECHO:hello

其中output.c代码为

#include <stdio.h>
int main(int argc, char * argv[])
{ printf(“ECHO:%s\n”, argv[1]); }

output由output.c编译而来

gcc –o output output.c

命令在shell中执行,而shell依然是一段代码 /bin/sh

int main(int argc, char * argv[])
{ char cmd[20];
while(1) { scanf(%s”, cmd);  % 无限循环  shell永不退出
if(!fork()) {exec(cmd);}   % 创建进程-执行命令 (这里的cmd就是output中的代码)
else {wait();} } //while(1)

4. 图形界面是怎么执行代码的?

在这里插入图片描述
图像界面的框架主要由上图所示,主要流程为:

  1. 硬件输入(比如鼠标点击,键盘输入等)引发中断
  2. 进入系统消息队列,此时在操作系统内核层面
  3. 应用程序从内核中将消息队列取出,进入WinMain()消息循环
  4. 进入WinProc程序,执行程序

5. POSIX接口

POSIX: Portable Operating System Interface of Unix(IEEE制定的一个标准族)
POSIX 提供了一组标准的操作系统接口,使得软件可以在支持POSIX接口的操作系统之间移植。
在这里插入图片描述

二、系统调用的实现

1. 什么是系统调用?

系统调用(system call)其中system指的就是操作系统内核(前文【操作系统启动过程】中的system模块),call就如在汇编中call指令一样(跳转到call后面标签指向的程序地址进行执行)。
那么将上述两者结合起来,system call就是指一段代码(用户态)需要跳转到system模块中执行某一段代码(内核态)的过程,至于用户态和内核态到后面会解释。

2. 为什么要使用系统调用?

下面红色代码代表在内存低地址(system 系统内核的地址段中),蓝色代码代表在用户程序所在的地址段中。
在这里插入图片描述
为什么main()不能直接调用whoami()?

答案是为了系统安全考虑,如果用户程序可以任意访问任意内存,那么你的计算机、操作系统、各种信息将全部暴露在你下载的应用程序面前,如果遇到恶意程序这是非常恐怖的。另一方面,是为了系统稳定性考虑,系统内核中有许多重要的代码和数据(IDT,GDT),如果遇到应用程序纂改也会导致系统崩溃。

3.内核态和用户态

在这里插入图片描述

如何实现内核态和用户态的隔离的?

答案是通过硬件设计实现的,也只能通过硬件设计实现。
具体是通过对寄存器的检查实现的,CPL代表CS寄存器的最低两位,RPL为DS寄存器的最低两位。

4. 如何进入内核执行代码?

硬件提供了"主动进入内核的方法",如果无法进入内核执行代码则意味着应用程序无法调动计算机硬件,所以需要一种方法让应用程序安全的进入内核执行代码。

对于Intel x86,那就是中断指令int,int指令将使CS中的CPL从 3 改成 0 ,“进入内核”,还记得CPL = 0意味着内核态,此时代表内核已经打开了大门,DPL >= CPL检查通过即可JMP到系统内核段中执行代码。

5. 系统调用的过程

系统调用的大致过程分为以下三步:
在这里插入图片描述
接下来,又出现几个疑问?

  1. 除汇编外似乎没有直接写过INT指令的代码,那么INT是在哪执行的?
  2. INT中断后操作系统又做了什么事情?

6. 系统调用的实现

此时我们用C语言中的printf()函数作为例子,解析以下系统调用的过程。
在这里插入图片描述1. 代码是最上层是C语言代码printf()
2. 接下来printf()函数内部由C语言函数库解释,其中调用了__syscall3()函数
3. __syscall3() 函数中给出INT的名字为write
4. __syscall3() 函数内部执行了一段内嵌汇编,执行了中断 INT 0x80
5. 执行了INT 0x80引发中断后,就来到了OS内核中,中断返回后会回到用户态
上述过程中,INT 0x80之外的代码都由C库函数解释(在用户态执行),只有INT 0x80中断过程中会在OS内核中执行。

下面对上述中断过程展开介绍:
在linux/include/unistd.h中

#define _syscall3(type,name,atype,a,btype,b,ctype,c)\ % syscall3代表3个参数
type name(atype a, btype b, ctype c) \ 
{ long __res;\   % 定义返回变量
__asm__ volatile(int 0x80:=a”(__res):””(__NR_##name), % __NR_write 为输入,因为name参数为write ,__res为输出
”b”((long)(a)),”c”((long)(b)),“d”((long)(c)))); if(__res>=0) return % fd传给ebx, char *buf传给ecx, count 传给edx
(type)__res; errno=-__res; return -1;}
””(__NR_##name) 等价于代码 a”(__NR_##name) % 意思是将__NR_##name值赋给eax寄存器

eax寄存器放返回值(即__NR_write系统调用号),ebx/ecx/edx存放参数。

在linux/include/unistd.h中
#define __NR_write 4 //一堆连续正整数(数组下标,函数表索引)

6.1 INT 0x80的处理过程

回忆一下在前文【操作系统启动过程】中,head.s代码执行后main函数代码执行了一系列的初始化过程

void sched_init(void) % 初始化系统调用入口 
{ set_system_gate(0x80,&system_call); } % 表示INT 0x80就要调用system_call来处理

在这里插入图片描述
下面这段代码的主要功能就是初始化上面这个IDT表,该表第一行为edx,第二行为eax,将中断处理函数的入口写入到IDT中。
在linux/include/asm/system.h中

#define set_system_gate(n, addr) \
_set_gate(&idt[n],15,3,addr); //idt是中断向量表基址,从该表中找到0x80偏移地址
#define _set_gate(gate_addr, type, dpl, addr)\ //这里dpl就是内核态那里的DPL,gate_addr就是上面那个表的地址,addr是&system_call
__asm__(“movw %%dx,%%ax\n\t” “movw %0,%%dx\n\t”\  // dx=ax dx=%0
“movl %%eax,%1\n\t” “movl %%edx,%2:\ //  %1 = eax, %2 = edx
:”i”((short)(0x8000+(dpl<<13)+type<<8))),“o”(*(( \ // "i"代表立即数,"o"代表内存变量
char*)(gate_addr))),”o”(*(4+(char*)(gate_addr))),\
“d”((char*)(addr),”a”(0x00080000))
// %1 为0x8000 + dpl左移13位 + type左移8位 = 0xEF00 
// %1为gate_addr的低4位赋
// %2 为gate_addr的高四位
// 0x00080000赋给eax
// 将addr赋给edx
// movw位字传送16位,movl为字传送32位
// movl指令以寄存器作为目的时,它会把该寄存器的高4位字节设置为0

此时段选择符为0x0008,即CS=0x0008(CPL=0),回忆前文【操作系统启动过程】中 jmpi 0,8即为跳转到内核代码段,执行其中system_call
在这里插入图片描述

6.2 system_call

在linux/kernel/system_call.s中

nr_system_calls=72
.globl _system_call
_system_call: cmpl $nr_system_calls-1,%eax //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 //内核数据 edx=0x10,ds=dx=0x10,es=dx=0x10 这个段对应内核数据段
movl $0x17,%edx mov %dx,%fs //fs可以找到用户数据
call _sys_call_table(,%eax,4) //a(,%eax,4)=a+4*eax 
// 4代表每个_sys_call的函数地址占4字节
// _sys_call_table为系统调用表的基地址
pushl %eax //返回值压栈,留着ret_from_sys_call时用
... //其他代码
ret_from_sys_call: popl %eax, 其他pop, iret
// _sys_call_table+4*%eax就是相应系统调用处理函数入口

在include/linux/sys.h中

fn_ptr sys_call_table[]=  // 函数指针表
{sys_setup, sys_exit, sys_fork, sys_read, sys_write,
...};
// sys_write对应的数组下标为4,__NR_write=4

call _sys_call_table(,%eax=4,4)就是call sys_write

7. 整体流程

  1. 开机,执行6.1中的初始化流程,将INT 0x80中断处理函数设为system_call
  2. 用户调用printf()
  3. C库函数展开到_sys_call3()函数
  4. _sys_call3()函数执行INT 0x80中断
  5. 执行system_call,查表得到__NR_write=4
  6. 执行call _sys_call_table(,%eax,4)调用sys_write
    在这里插入图片描述
    在这里插入图片描述
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值