查看: 63|回复: 0

剖析Linux系统调用的执行路径

[复制链接]

该用户从未签到

发表于 2019-11-3 21:15:53 | 显示全部楼层 |阅读模式
什么是操作系统这篇文章中,先容过操作系统像是一个署理一样,为我们去管理计算机的众多硬件,我们需要计算机的一些计算服务、数据管理的服务,都由操作系统提供接口来完成。如许做的利益是让一般的计算机使用者不消关心硬件的细节。
1. 什么是操作系统的接口

既然使用者是通过操作系统接口来使用计算机的,那到底是什么是操作系统提供的接口呢?
接口(interface)这个词来源于电气工程学科,指的是插座与插头的毗连口,起到将电与电器毗连起为的功能。后来延伸到软件工程里指软件包向外提供的功能模块的函数接口。以是接口是用来毗连两个东西、信号转换和屏蔽细节。
那对于操作系统来说:操作系统通过接口的方式,建立了用户与计算机硬件的沟通方式。用户通过调用操作系统的接口来使用计算机的各种计算服务。为了用户友好性,操作系统一般会提供两个重要的接口来满意用户的一些一般性的使用需求:

  • 下令行:实际是一个叫bash/sh的端终步伐提供的功能,该步伐底层的实质照旧调用一些操作系统提供的函数。
  • 窗口界面:窗口界面通过编写的窗口步伐汲取来自操作系统消息队列的一些鼠标、键盘动作,进而做出一些响应。
对于非一般性使用需求,操作系统提供了一系列的函数调用给软件开发者,由软件开发者来实现一些用户需要的功能。这些函数调用由于是操作系统内核提供的,为了有别于一般的函数调用,被称为系统调用。比如我们使用C语言举行软件开发时,常常用的printf函数,它的内部实际就是通过write这个系统调用,让操作系统内核为我们把字符打印在屏幕上的。
为了规范操作系统提供的系统调用,IEEE订定了一个标准接口族,被称为POSIX(Portable Operating System Interface of Unix)。一些我们熟悉的接口比如:fork、pthread_create、open等。
2. 用户模式与内核模式

计算机硬件资源都是操作系统内核举行管理的,那我们可以直接用内核中的一些功能模块来操作硬件资源吗?可以直接访问内核中维护的一些数据结构吗? 当然不可!有人会说,为什么不可呢?我买的电脑,内核代码在内存中,那内存不都是我自己买的吗?,我自己不能访问吗?
如今我们运行的操作系统都是一个多任务、多用户的操作系统。如果每个用户历程都可以任意访问操作系统内核的模块,改变状态,那整个操作系统的稳定性、安全性都大大低落了。
为了将内核步伐与用户步伐隔离开,在硬件层面上提供了一次机制,将步伐执行的状态分为了不同的级别,从0到3,数字越小,访问级别越高。0代表内核态,在该特权级别下,全部内存上的数据都是可见的,可访问的。3代表用户态,在这个特权级下,步伐只能访问一部分的内存地区,只能执行一些限定的指令。
操作系统在建立GTD表的时候,将GTD的每个表项中的2位(4种特权级别)设置为特权位(DPL),然后操作系统将整个内存分为不同的段,不同的段,在GDT对应的表项中的DPL位是不同的。比如内核内存段的全部特权位都为00。而用户步伐访存时,在掩护模式下都是通过段寄存器+IP寄存器来访问的,而段寄存器里则用两位表示当前历程的级别(CPL),是位于内核态照旧用户态。
既然云云,那我们还有什么办法可以调用操作系统的内核代码呢?操作系统为了实现系统调用,提供了一个主动进入内核的惟一方式:停止指令int。int指令会将GDT表中的DPL改为3,让我们可以访问内核中的函数。以是全部的系统调用都必须通过调用int指令来实现,大致的过程如下:

  • 用户步伐中包含一段包含int指令的代码
  • 操作系统写停止处理,获取相调步伐的编号
  • 操作系统根据编号执行相应的代码
3. 分析printf函数

下面我们以printf函数的调用为例,说明该函数是怎样一步一步最终落在内核函数上去的。
图1:应用步伐、库函数和内核系统调用之间的关系printf函数是C语言的一个库函数,它并不是真正的系统调用,在Unix下,它是通过调用write函数来完成功能的。
write函数内部就是调用了int停止。一般的系统调用都是调用0x80号停止。而操作系统中一般不会的显式的写出write的实现代码,而是通过_syscall3宏睁开的实现。_syscall3是专门用来处理有3个参数的系统调用的函数的实现。同理还有_syscall0、_syscall1和_syscall2等,目前最大支持的参数个数为3个,这三个参数是通过ebx, ecx,edx传递的。如果有系统调用的参数超过了3个,那么可以通过一个参数结构体来举行传递。
  1. // linux/lib/write.c#define __LIBRARY__#include // _syscall3(int,write,int,fd,const char *,buf,off_t,count)
复制代码
  1. // linux/include/unistd.h#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) \    : "0" (__NR_##name),"b" ((long)(a)),"c" ((long)(b)),"d" ((long)(c))); \if (__res>=0) \    return (type) __res; \errno=-__res; \return -1; \}
复制代码
以是宏睁开后,write函数的实实际现为:
  1. int write(int fd, const char *buf, off_t count){     long __res;     __asm__ volatile ("int $0x80"         : "=a" (__res)         : "0" (__NR_write),"b" ((long)(a)),"c" ((long)(b)),"d" ((long)(c)));     if (__res>=0)         return (type) __res;     errno=-__res;     return -1; }
复制代码
我们看到实际函数内部并没有做太多的事情,主要就是调用int 0x80,将把相干的参数传递给一些通用寄存器,调用的结果通过eax返回。其中一个很重要的调用参数是__NR_write这个也是一个宏,就是wirte的系统调用号,在linux/include/unistd.h中被定义为4,同样还有很多其他系统调用号。由于全部的系统调用都是通过int 0x80,那怎么知道详细需要什么功能呢,只能通过系统调用号来辨认。
下面我们来看看int 0x80是怎样执行的。这是一个系统停止,操作系统对于停止处理流程一般为:

  • 关停止:CPU关闭中段响应,即不再担当别的外部停止哀求
  • 保存断点:将发生停止处的指令地点压入堆栈,以使停止处理完后能正确地返回。
  • 辨认停止源:CPU辨认停止的来源,确定停止范例号,从而找到相应的停止服务步伐的入口地点。
  • 掩护现场所:将发生停止处理有关寄存器(停止服务步伐中要使用的寄存器)以及标志寄存器的内存压入堆栈。
  • 执行停止服务步伐:转到停止服务步伐入口开始执行,可在适当时刻重新开放停止,以便允许响应较高优先级的外部停止。
  • 恢复现场并返回:把“掩护现场”时压入堆栈的信息弹回原寄存器,然后执行停止返回指令(IRET),从而返回主步伐继续运行。
前3项通常由处理停止的硬件电路完成,后3项通常由软件(停止服务步伐)完成。
图2:系统调用停止处理流程那0x80号停止的处理步伐是什么呢,我们可以看一下操作系统是怎样设置这个停止向量表的。在操作系统初始化时shecd_init函数里,调用了
  1. set_system_gate(0x80, &system_call);
复制代码
我们深入看一下set_system_gate函数做了什么
[code]#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

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?用户注册

x

相关技术服务需求,请联系管理员和客服QQ:2753533861或QQ:619920289
您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

帖子推荐:
客服咨询

QQ:2753533861

服务时间 9:00-22:00

快速回复 返回顶部 返回列表