12系统调用与API

文章目录

  • 12.1系统调用介绍
    • 12.1.1什么是系统调用
    • 12.1.2 Linux系统调用
    • 12.1.3系统调用的弊端
  • 12.2系统调用原理
    • 12.2.1 特权级与中断
    • 12.2.2基于int的 Linux的经典系统调用实现
      • 1触发中斷
      • 2.切换堆栈
      • 3.中断处理程序
    • 12.2.3 Linux的新型系统调用机制
  • 12.3 Windows APL

  • 从程序如何链接、如何使用运行库到运行库的实现机制,
  • 现在到了用户层面与内核层面的界限了,常说的系统调用
  • 系统调用是应用程序(运行库也是应用程序的一部分)与操作系统内
    核之间的接口,它决定了应用程序是如何与内核打交道的。
  • 无论程序是直接进行系统调用还是通过运行库,最终还是会到达系统调用这个层面上

  • Windows系统完全基于DLL机制,
  • 它通过DLL堆系统调用进行了包装,形成所谓Windows API。
  • 应用程序所能看到的Windows系统的最底层的接口就是 Windows API
  • 上节的fread最终还是到了Readfile这个API
  • Windows的程序相当于在运行库与系统调用之间又多一层APl,
  • 无论如何,API最终还是通过系统调用。
  • 这一章里,会了解到系统调用和API的各方面,包括许多实现的细节。

12.1系统调用介绍

12.1.1什么是系统调用

  • 程序运行时,本身没权利访问系统资源
  • 系统有限的资源有可能被多个不同应用程序同时访
    • 如果不保护,那各个应用程序冲突
  • OS都将可能产生冲突的系统资源给保护起来
    • 阻止应用程序直接访
  • 系统资源包括
    • 文件、网络、IO、各种设备
  • 程序员没机会擅自去访问硬盘的某病区上数据,
    • 必须通过文件系统
  • 不能擅自修改任意文件,这些操作都须经由操作系统所规定的方式
  • fopen打开一个没有权限的文件就失败

  • 要让程序等待一段时间,不借助操作系统的唯一办法

12系统调用与API_第1张图片

  • 白白消耗CPU时间
  • 它随着计算机性能的变化而耗费不同时间
  • 100MHz的CPU中,需1秒
  • 1000MHz的CPU中,只需0.1
  • 用操作系统的定时器
    • 任何硬件上,
    • 代码执行的效果是一样

在这里插入图片描述

  • 没OS帮助,应用程序执行寸步难
  • 为让应用程序有能力访问系统资源
    • 为让程序借OS做一些必须由操作系统支持的行为,
    • 每个操作系统都提供一套接口,
    • 供应用程序用
  • 这些接口通过中断实现
    • Linux用0x80号中断作为系统调用的入口,
    • Windows采用0x2E号

  • 系统调用涵盖功能很广,有程序运行所必需的支持,
  • 例如创建/退岀进程和线程、
  • 进程内存管理,
  • 对系统资源的访问,
    • 如文件、网络、进程间通信、硬件设备的访问,
    • 对图形界面的操作支持,例如 Windows下的GUT机制。

  • 系统调用作为一个接口,它的定义重要
  • 应用程序都依赖于系统调用,
    • 系统调用必须有明确的定义,
    • 即每个调用的含义参数、行为都需要有严格而清晰的定义,
    • 这样应用程序(运行库)オ可正确使用它
  • 必须保持稳定和向后兼容,
    • 如果某次系统更新导致系统调用接口改变,
    • 新的系统调用接口与之前版本不同,
    • 之前能正常运行的程序都将无法用。
  • 操作系统的系统调用往往从一开始定义后就基本不做改变,而仅仅是增加新的调用接口,以保持向后兼容。

  • 对Windows来讲,系统调用不是它与应用程序的最终接口,而是API
  • 上面这段对系统调用的描述同样适用于Windows API,
    • 暂时可把API与系统调用等同
  • Windows系统从 Windows1.0到最新的Windows Vista,
    • API的数量从最初1.0时的450个增加到现在数千个,
    • 很少对已有的API改变。
    • API一且改变,很多应用程序将无法正常运行。

12.1.2 Linux系统调用

  • x86下,系统调用由0x80中断完成,
    • 通用寄存器用于传递参数,
    • EAX表示系统调用的接口号,
    • =1表示退出进程(exit):
    • =2表示创建进程(fork);
    • =3读取文件或I/O(read);
    • 4表示写文件或IO( write)等,
    • 每个系统调用都对应于内核源代码中的一个函数,
      • “sys_”开头,
    • exit调用对应内核中sys_exit函数
  • 系统调用返回时,EAX又作为调用结果的返回值。

  • 内核2.6.19
  • 提供319个系统调用

12系统调用与API_第2张图片

  • 未列举的包括权限管理(sys_ setuid等)、定时器( sys_timer_ create)、信号(sys_ sigaction)、网络(sys_epol)等。
  • 这些系统调用都可在程序里直接用,
    • 定义在“usr/ include/ unistd.h”
  • 绕过glibc的 fopen、 fread、 fclose打开读取和关闭文件,
  • 直接用open()、 read)和 close(来实现文件的读取,
    • 用write向屏幕输出字符串(标准输出的文件句柄为0):
12系统调用与API_第3张图片

  • 以用read系统调用实现读取用户输入(标准输入的文件句柄为1)。
  • 绕过了glibc的文件读取机制,
    • 所有位于 glibc中的缓冲、按行读取文本文件等这些机制都没有了,读取的就是文件的原始数据。
  • 很多时候希望获得更高文件读写性能,
    • 绕过glibc使用系统调用也是一个比较好的办法。

  • man
  • 察看每个系统调用的详细说明,
  • 察看read(man 参数2表示系统调用手册):
    man 2 read

12.1.3系统调用的弊端

  • 系统调用完成应用程序和内核交流,

    • 理论上只需要系统调用就可以完成一些程序,
  • 系统调用都有两个特点

  • 使用不便。

    • 操作系统提供的系统调用接口往往过于原始,
    • 程序员须要了解很多与操作系统相关的细节。
    • 如果没有很好的包装,使用起来不便
  • 操作系统之间系统调用不兼容

    • Windows系统和 Linux系统之间的系统调用基本完全不同,
    • 虽然它们的内容很多都一样,但定义和实现大不一样。
    • 即使同系列的操作系统的系统调用都不一样,Linux和UNX就不相同。
  • 为解决这个问题,第1章中的“万能法则”又可以发挥它的作用了。

  • “解决计算机的问题可通过增加层来实现”,运行库挺身而出,它作为系统调用与程序之间的一个抽象层可以保持着这样的特点:

  • 使用简便。因为运行库本身就是语言级别的,它一般都设计相对比较友好。

  • 形式统一。运行库有它的标准,叫做标准库,凡是所有遵循这个标准的运行库理论上都相互兼容的,不会随着操作系统或编译器的变化而变化。

  • 当我们用运行库提供的接口写程序时,
  • 就不会面临这些问题,至少是可以很大程度上掩盖直接使用系统调用的弊端。

  • C语言里的 fread,读取文件,
  • Windows下这个函数的实现可能是调用Readfile这个API,
  • 如果在 Linux下,则很可能调用了read这个系统调用。
  • 但不管在哪个
    平台,我们都可以使用C语言运行库的 fread米读文件

  • 运行时库将不同操作系统的系统调用包装为统一固定接口,使得同样的代码,在不同的操作系统下都可直接编译,并产生一致效果。
  • 这就是源代码级上的可移植性。

  • 运行库也有缺陷,
  • C语言的运行库为了保证多个平台之间能够相互通用,
  • 它只能取各个平台之间功能的交集。
  • Windows和 Linux都支持文件读写
    • 那么运行库就可有文件读写功能:
  • 但Windows原生支持图形和用户交互系统,Linux却不是原生支持的(通过Xwindows),CRT就只能把这部分功能省去。
  • 一旦程序用到了那些CRT之外的接口,程序就很难保持各个平台之间的兼容性

12.2系统调用原理

12.2.1 特权级与中断

  • 两种特权级别,用户模式和内核模式
  • 由于有多种特权模式,操作系统就可以让不同的代码运行在不同的模式上,以限制它们权力,提高稳定性和安全性
  • 普通应用程序运行在用户态的模式,
    • 诸多操作受到限制,
    • 包括访问硬件设备、开关中断、改变特权模式

  • 运行在高特权级的代码将自已降至低特权级是允许的,但反过来低特权级的代码将自已提升至高特权级则不轻易
  • 将低特权级的环境转为高特权级时,须用一种较为受控和安全的形式,以防止低特权模式的代码破坏高特权模式代码的执行
  • 系统调用是运行在内核态,应用程序运行在用户态
  • 用户态的程序如何运行内核态的代码?
  • 操作系统一般通过中断来从用户态切换到内核态
  • 中断是一个硬件或软件发出的请求
    • 要求CPU暂停当前的工作转手去处理更加重要的事情。
  • 编辑文本时
    • 键盘上的键不断被按下,CPU如何获知?
  • 轮询(PolI),即CPU每隔一小段时间(几十到几百亳秒)去
    • 询问键盘是否有键被按下,
    • 大部分的轮询行为得到的都是“没有键被按下”
  • CPU不理睬键盘
    • 键盘上有键被按下时,
    • 键盘上的芯片发送一个信号给CPU,
    • CPU接收到信号之后就知道键盘被按下了,
    • 然后去询问键盘被按下的键是哪一个。
    • 这样的信号就是一种中断

  • 中断一般有两属性,
    • 中断号(从0)
    • 中断处理程序(ISR)。
  • 不同的中断有不同的中断号
    • 一个中断处理程序一一对应一个中断号。
  • 内核数组称中断向量表,数组的第n项包含了指向第n号中断的中断处理程序的指针
  • 中断到来时,CPU会暂停当前代码,
    • 根据中断号,在中断向量表中找到对应的中断处理程序,并调它
    • 中断处理程序完成后,CPU继续执行之前代码
  • 示意图如图12-2
12系统调用与API_第4张图片

  • 中断有两种类型,
  • 硬件中断,来自硬件的异常或其他事件的发生,
    • 电源掉电、键盘被按下
  • 软件中断,软件中断通常是一条指令(i386下是int),
    • 带一个参数记录中断号
    • 用这条指令用户可以手动触发某中断并执行其中断处理程序。
  • i386下,int0x80这条指令会调用第0x80号中断的处理程序

  • 中断号有限,OS不舍得用一个中断号来对应一个系统调用,
    • 倾向于用一个或少数几个中断号来对应所有的系统调用
  • i386下 Windows里大多数系统调用都是由 int Ox2e触发
  • Linux用int Ox80来触发所有的系统调用
  • 对同一个中断号,如何知道是哪一个系统调用要被调用呢?
  • 和中断一样,系统调用都有一个系统调用号,通常就是系统调用在系统调用表中的位置
  • Linux里fork的系统调用号是2。
  • 这个系统调用号在执行int指令前会被放置在某个固定的寄存器里,对应的中断代码会取得这个系统调用号并且调用正确的函数。
  • Linux的int0x80为例,系统调用号是由eax来传入。
  • 用户将系统调用号放入eax,然后用int0x80调用中断,中断服务程序就可以从eax里取得系统调用号,进而调用对应的函数

12.2.2基于int的 Linux的经典系统调用实现

  • 应用程序调用系统调用时
    • 程序如何一步步进入操作系统内核
    • 调用相应函数
  • 以fork为例的Linux系统调用的流程
12系统调用与API_第5张图片

1触发中斷

  • 调一个系统调用时,
  • 是以函数的形式调用的
12系统调用与API_第6张图片
  • fork函数是一个对系统调用fork的封装,
    • 用宏来定义它:
12系统调用与API_第7张图片

  • __syscall0是宏函数
  • 定义没有参数的系统调用的封装
  • 这个系统调用的返回值类型pid_t
  • 系统调用名称,__syscall0展开后会形成一个与系统调用名称同名的函数
  • i386版的__syscall0定义
12系统调用与API_第8张图片 12系统调用与API_第9张图片

  • 这种AT&T汇编不熟悉

  • __asm__是gcc关键字

    • 接下来要嵌入汇编代码
    • volatile告诉GCC対这段代码不优化
  • __asm__的第一个参数是字符串,代表汇编代码文本

    • 这里的汇编代码只有一句:int 0x80,调Ox80号中断
    • 用eax(a表示eax)输出返回数据并存储在res
    • __NR_##name为输入
    • “0”指示由编译器选择和输出相同的寄存器(即eax)来传递参数。
12系统调用与API_第10张图片

12系统调用与API_第11张图片

  • 宏检査系统调用的返回值,并把它相应地转换为C的errno
  • Linux里,系统调用用返回值传递错误码,
    • 为负,那么表明调用失败,
    • 返回值的绝对值就是错误码。
  • C里则不然,C里的大多数函数都回-1表示调失败,
    • 将出错信息存储在一个名为erno的全局变量(在多线程库中,errno存储于TLS)
  • syscall_return将系统调用的返回信息存储在errno
  • fork函数在汇编之后,形成
12系统调用与API_第12张图片

  • 如果系统调用本身有参数要如何实现呢?
  • x86 Linux下的 syscalls,带1个参数的系统调用
12系统调用与API_第13张图片

  • 它多一个"b”"(long(aug1)
  • 先把argl强制转换为long,然后存放在EBX(b代表EBX)里作为输入。
  • 编译器还会生成相应的代码来保护原来的EBX的值不被破坏。这段汇编可以改写为
12系统调用与API_第14张图片
  • x86下 Linux支持的系统调用参数至多有6,
  • EBX、ECX、EDX、ESI、EDI和EBP

  • 用户调某系统调用时,实际是执行以上一段汇编
  • 执行到int 0x80时,会保存现场以便恢复,
    • 接着将特权状态切换到内核态
  • 然后CPU査找中断向量表中的Ox80号元素

  • 以上是linux实现系统调用入口的思路
  • glibc是否真的是如此封装系统调用的?否
  • glibc用另外一套调用系统调用的方法,原理上仍然是使用0x80号中断,但细节上不一样

2.切换堆栈

根据深入理解哪本书介绍的:从TSS段找到内核栈的位置对吧

  • 执行向量表中的0x80号元素对应函数前
    • CPU先要栈切换
  • 用户态和内核态使用不同的栈,
    • 两者各自负责各自的函数调用
  • 应用程序调0x80中断时,程序的执行流程从用户态切换到内核态,
    • 当前機必须也相应地从用户栈切换到内核栈。
  • 从中断处理函数中返回时,
    • 当前栈还要从内核栈切换回用户栈。
    • “当前機”,
      • ESP的值所在的栈空间。
    • ESP的值位于用户栈的范围内,
      • 程序的当前栈就是用户栈
  • SS的值还应该指向当前栈所在的页
  • 将当前栈由用户栈切换为内核栈就是
  • (1)保存当前的ESP、SS
  • (2)将ESP、SS的值设置为内核栈的相应值
  • 将当前栈由内核栈切换为用户栈的实际行为则是:
  • (1)恢复原来ESP、SS的值
  • (2)用户态的ESP和SS的值保存在哪里呢?
    • 内核機上。
    • 由i386的中断指令自动地由硬件完成。

  • 0x80号中断发生时,
  • CPU除切入内核态外
  • 自动
  • (1)找到当前进程的内核桟(每一进程都有自己的内核機)。
  • (2)在内核機中依次压入用户态的SS、ESP、 EFLAGS、CS、EIP。

自动找到内核栈,内核栈在TSS段啊!!

这个是硬件自动完成的,你不想要显式的做这个活

  • 内核从系统调用中返回时,须调iret来回到用户态
  • iret指令则从内核栈里弹出SS、ESP、 EFLAGS、CS、EIP
    • 使得栈恢复到用户态
12系统调用与API_第15张图片

3.中断处理程序

  • int切换栈后,程序流程就到
    • 中断向量表中记录的0x80号中断处理程序
  • Linux内部的i386中断服务流程如图12-5

先切换栈,再找到0X 80的位置程序执行啊

12系统调用与API_第16张图片

  • i386的中断向量表 Linux源代码的 Linux/ Marchi386 kernel/ traps. c里可见一部分。
  • 该文件的末尾,trap_init,初始化中断向量表:
12系统调用与API_第17张图片

  • set_intr_gate/set_trap_gate/set_system_gate/set_ system_intr_gate设置某个中断号上的中断处理程序。
  • 区分为3种名字,是因为在i386下对中断有更加细致的划分,
    • 限于篇幅这里就不详细介绍了,
    • 可以暂时将它们都等同对待

  • 0~19号中断对应的中断处理程序
    • 包含算数异常(除零、溢出)
    • 页缺失
    • 无效指令
  • 最后一行
    set_system_gate(SYSCALL_VECTOR, &system-call)
  • 这是系统调用对应的中断号,
  • Linux/ include/asm-i386/mach- default/irg_vectors. h里
    可找到 SYSCALL VECTOR的定义
#define SYSCALL_VECTOR 0x80
  • i386下Linux的系统调用对应的中断号确实是0x80
  • 用户调int0x80之后,最终执行的函数是system_call
    • Linux/arch/i386/ kerne/entry.S定义
  • 这段代码由汇编写成较长,一段研究

12系统调用与API_第18张图片
  • 宏SAVE_ALL将各种寄存器压栈
  • 接下来用cmpl比较eax和nr_syscalls
    • 是比最大的有效系统调用号大1
  • 如果eax>=于nr_ syscalls,这个系统调用就是无效的,
    • 如果这样,接着就跳到syscall_badsys
  • 如果系统调用号是有效的就执行
12系统调用与API_第19张图片
  • 确定系统调用号有效且保存寄存器后,
  • 就调用* sys_call_table(O,%eax,4)査找中断服务程序并执行
  • 执行结束后,
  • RESTORE_REGS恢复之前被SAVE_ALL保存的
  • 最后iret从中断处理程序中返回

12系统调用与API_第20张图片

  • Linux的i386系統调用表
    • 每一个元素(long,4字节)都是系统调用函数的地址。
  • *sys_call_table(O,%eax,4)
    • 指sys_call_table上偏移为0+%eax*4上的那个元素的值指向的函数
  • %eax所记录的系统调用号所对应的系统调用函数(见图12-6)
  • 如果%eax=2,那么sys_fork就会调用

在这里插入图片描述

12系统调用与API_第21张图片

  • 内核里以syS开头的系统调用函数如何从用户那里获得参数?
  • 进入系调用的服务程序system_call的时候,
    • system_call保存各个寄存器,没有在正文中仔细SAVE_ALL
    • SAVE_ALL实际与系统调用的参数传递息息相关
12系统调用与API_第22张图片

  • 抛开最后3个mov不看(这3条用于设置内核数据段,它们不
    影响栈)

  • SAVE_ALL的一系列push指令的最后6条所压入機中的寄存器怜好就是用来存放系統调用参数的6个寄存器,连顺序都一样,

  • 这当然不是一个巧

  • 回到system_call代码,发现,执行SAVE_ALL与执行call *sys_ call_table(0,%eax,4)间,没有任何代码会影响到機。

  • 因此刚刚进入Sys开头的内核系统调用函数的動候,栈上恰好是这样的情景,如图12-7

12系统调用与API_第23张图片

  • 系统调用的参数被SAVE_ALL“阴差旧错”放在機上
  • 所有以sys开头的内核系统用函数
    • 都有一个 asmlinkage的标识

在这里插入图片描述

  • 扩展关键字
    • 让这个函数只从機上获参
  • gcc对普通函数有优化措施,会用寄存器传通参数,
  • SAVE_ALL将参数全部放置于援上,
    • 因此必须用asmlinkage来强迫函数从找上获取参数
  • 这样系统调用函数就可正确地获取用户提供的参数了。
  • 整个过程用图12-8
12系统调用与API_第24张图片

12.2.3 Linux的新型系统调用机制

  • int指令的系统调用在奔4代上性能不佳,
  • Linux在2.5起支持一种新型的系统调用机制。
  • 用Intel在奔2代处理器就开始支持的一组专门针对系统调用的指令 sysenter和 sysexit
  • 本节对这种新系统调用机制进行一个初步了解

12.3 Windows APL

  • windows下提到API时
    • 就指Windows系统提供给应用程序的接口,Windows Apl
  • Windows API指Windows供给应用程序开发者的最底层的、
    • 最直接与Windows打交道的接口
  • CRT是建立在 Windows API之上
  • 还有很多对Windows API的各种包装库
    • MFC
    • 很著名的一种以C++形式封装的库

你可能感兴趣的:(自我夕阳)