程序员的自我修养--链接、装载与库笔记:系统调用与API

系统调用(System Call)是应用程序(运行库也是应用程序的一部分)与操作系统内核之间的接口,它决定了应用程序是如何与内核打交道的。无论程序是直接进行系统调用,还是通过运行库,最终还是会到达系统调用这个层面上。

1. 系统调用介绍

什么是系统调用:在现代的操作系统里,程序运行的时候,本身是没有权利访问多少系统资源的。由于系统有限的资源有可能被多个不同的应用程序同时访问,因此,如果不加以保护,那么各个应用程序难免产生冲突。所以现代操作系统都将可能产生冲突的系统资源给保护起来,阻止应用程序直接访问。这些系统资源包括文件、网络、IO、各种设备等。举个例子,无论在Windows下还是Linux下,程序员都没有机会擅自去访问硬盘的某扇区上面的数据,而必须通过文件系统;也不能擅自修改任意文件,所有的这些操作都必须经由操作系统所规定的方式来进行,比如我们使用fopen去打开一个没有权限的文件就会发生失败。此外,有一些行为,应用程序不借助操作系统是无法办到或不能有效地办到的。为了让应用程序有能力访问系统资源,也为了让程序借助操作系统做一些必须由操作系统支持的行为,每个操作系统都会提供一套接口,以供应用程序使用。这些接口往往通过中断来实现,比如Linux使用0x80号中断作为系统调用的入口,Windows采用0x2E号中断作为系统调用入口。

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

系统调用既然作为一个接口,而且是非常重要的接口,它的定义将十分重要。因为所有的应用程序都依赖于系统调用,那么,首先系统调用必须有明确的定义,即每个调用的含义、参数、行为都需要有严格而清晰的定义,这样应用程序(运行库)才可以正确地使用它;其次它必须保持稳定和向后兼容,如果某次系统更新导致系统调用接口发生改变,新的系统调用接口与之前版本完全不同,这是无法想象的,因为所有之前能正常运行的程序都将无法使用。所以操作系统的系统调用往往从一开始定义后就基本不做改变,而仅仅是增加新的调用接口,以保持向后兼容。

Linux系统调用:在x86下,系统调用由0x80中断完成,各个通用寄存器用于传递参数,EAX寄存器用于表示系统调用的接口号,每个系统调用都对应于内核源代码中的一个函数,它们都是以”sys_”开头的,比如exit调用对应内核中的sys_exit函数。Linux内核版本提供了很多个系统调用,这些系统调用都可以在程序里面直接使用,它的C语言形式被定义在/usr/include/unistd.h中,比如我们完全可以绕过glibc的fopen、fread、fclose打开读取和关闭文件,而直接使用open()、read()和close()来实现文件的读取,使用write向屏幕输出字符串(标准输出的文件句柄为0),使用read系统调用来实现读取用户输入(标准输入的文件句柄为1)。不过由于绕过了glibc的文件读取机制,所以所有位于glibc中的缓冲、按行读取文本文件等这些机制都没有了,读取的就是文件的原始数据。我们也可以使用Linux的man命令查看每个系统调用的详细说明,如: man 2 read。

系统调用的弊端:系统调用完成了应用程序和内核交流的工作,事实上,包括Linux,大部分操作系统的系统调用都有两个特点:(1). 使用不便:操作系统提供的系统调用接口往往过于原始,程序员需要了解很多与操作系统相关的细节。如果没有进行很好的包装,使用起来不方便。(2). 各个操作系统之间系统调用不兼容:首先Windows系统和Linux系统之间的系统调用就基本上完全不同,虽然它们的内容很多都一样,但是定义和实现大不一样。即使是同系列的操作系统的系统调用都不一样,比如Linux和UNIX就不相同。

“解决计算机的问题可以通过增加层来实现”,于是运行库挺身而出,它作为系统调用与程序之间的一个抽象层可以保持着这样的特点:(1). 使用简便:因为运行库本身就是语言级别的,它一般都设计相对比较友好。(2). 形式统一:运行库有它的标准,叫做标准库,凡是所有遵循这个标准的运行库理论上都是相互兼容的,不会随着操作系统或编译器的变化而变化。

运行时库将不同的操作系统的系统调用包装为统一固定的接口,使得同样的代码,在不同的操作系统下都可以直接编译,并产生一致的效果。这就是源代码级上的可移植性。但是运行库也有运行库的缺陷,比如C语言的运行库为了保证多个平台之间能够相互通用,于是它只能取各个平台之间功能的交集。

2. 系统调用原理

特权级与中断:现代的CPU常常可以在多种截然不同的特权级别下执行指令,在现代操作系统中,通常也据此有两种特权级别,分别为用户模式(User Mode)和内核模式(Kernel Mode),也被称为用户态和内核态。由于有多种特权模式的存在,操作系统就可以让不同的代码运行在不同的模式上,以限制它们的权力,提供稳定性和安全性。系统调用是运行在内核态的,而应用程序基本都是运行在用户态的。用户态的程序如何运行内核态的代码呢?操作系统一般是通过中断(Interrupt)来从用户态切换到内核态。什么是中断呢?中断是一个硬件或软件发出的请求,要求CPU暂停当前的工作转手去处理更加重要的事情

中断一般具有两个属性,一个称为中断号(从0开始),一个称为中断处理程序(Interrupt Service Routine, ISR)。不同的中断具有不同的中断号而同时一个中断处理程序一一对应一个中断号。在内核中,有一个数组称为中断向量表(Interrupt Vector Table),这个数组的第n项包含了指向第n号中断的中断处理程序的指针。当中断到来时,CPU会暂停当前执行的代码,根据中断的中断号,在中断向量表中找到对应的中断处理程序,并调用它。中断处理程序执行完成之后,CPU会继续执行之前的代码。

通常意义上,中断有两种类型,一种称为硬件中断,这种中断来自于硬件的异常或其它事件的发生,如电源掉电、磁盘被按下等。另一种称为软件中断,软件中断通常是一条指令(i386下是int),带有一个参数记录中断号,使用这条指定用户可以手动触发某个中断并执行其中断处理程序。由于中断号是很有限的,操作系统不会舍得用一个中断号来对应一个系统调用,而更倾向于用一个或少数几个中断号来对应所有的系统调用。

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

(1). 触发中断:首先当程序在代码里调用一个系统调用时,是以一个函数的形式调用的。

(2). 切换堆栈:在实际执行中断向量表的第0x80号元素所对应的函数之前,CPU首先还要进行栈的切换。在Linux中,用户态和内核态使用的是不同的栈,两者各自负责各自的函数调用,互不干扰。但在应用程序调用0x80号中断时,程序的执行流程从用户态切换到内核态,这时程序的当前栈必须也相应地从用户栈切换到内核栈。从中断处理函数中返回时,程序的当前栈还要从内核栈切换回用户栈。

(3). 中断处理程序:在int指令合理地切换了栈之后,程序的流程就切换到了中断向量表中记录的0x80号中断处理程序。内核里的系统调用函数往往以sys_加上系统调用函数名来命名,例如sys_fork、sys_open等。

Linux的新型系统调用机制:使用ldd来获取一个可执行文件ls的共享库的依赖情况,如下表所示,可以看到linux-vdso.so.1没有与任何实际的文件相对应,这个共享库是Linux用于支持新型系统调用的”虚拟”共享库。linux-vdso.so.1并不存在实际的文件,它只是操作系统生成的一个虚拟动态共享库(Virtual Dynamic Shared Library, VDSO)。可以通过Linux的proc文件系统来查看一个可执行程序的内存映像。命令cat  /proc/self/maps可以查看cat命令自己的内存布局。我们可以看见地址0x7ffdc0d43000到0x7ffdc0d45000的地方被映射了vdso,也就是linux-vdso.so.1。

3. Windows API

API的全称为Application Programming Interface,即应用程序编程接口。Windows API是指Windows操作系统提供给应用程序开发者的最底层的、最直接与Windows打交道的接口。在Windows操作系统下,CRT是建立在Windows API之上的。另外还有很多对Windows API的各种包装库,MFC就是很著名的一种以C++形式封装的库。很多操作系统是以系统调用作为应用程序最底层的,而Windows的最底层接口是Windows API。Windows API是Windows编程的基础,尽管Windows的内核提供了数百个系统调用(Windows又把系统调用称作系统服务(System Service)),但是出于种种原因,微软并没有将这些系统调用公开,而在这些系统调用之上,建立了这样一个API层,让程序员只能调用API层的函数,而不是如Linux一般直接使用系统调用。

Windows API概览:Windows API是以DLL导出函数的形式暴露给应用程序开发者的。它被包含在诸多的系统DLL内,规模上非常庞大。微软把这些Windows API DLL导出函数的声明的头文件、导出库、相关文件和工具一起提供给开发者,并让它们成为Software Development Kit(SDK)。SDK可以单独地在微软的官方网站下载,也可以被集成到Visual Studio这样的开发工具中。当我们安装了Visual Studio后,可以在SDK的安装目录下找到所有的Windows API函数声明。其中有一个头文件”Windows.h”包含了Windows API的核心部分,只要我们在程序里面包含了它,就可以使用Windows API的核心部分了。

Windows API随着Windows版本的升级也经历了好几个版本,每次Windows进行大升级的时候,也会引入新版本的API。Windows API现在的数量已经十分庞大,它们按照功能被划分成了几大类别,如下图所示:

由于Windows API所提供的接口还是相对比较原始的,所以直接使用API进行程序开发往往效率较低。Windows系统在API之上建立了很多应用模块,这些应用模块是对Windows API的功能的扩展。

为什么要使用Windows API:系统调用实际上是非常依赖于硬件结构的一种接口,它受到硬件的严格控制,比如寄存器的数量、调用时的参数传递、中断号、堆栈切换等,都与硬件密切相关。如果硬件结构稍微发生改变,大量的应用程序可能就会出现问题(特别是那些与CRT静态链接在一起的)。为了尽量隔离硬件结构的不同而导致的程序兼容性问题,Windows系统把系统调用包装了起来,使用DLL导出函数作为应用程序的唯一可用的接口暴露给用户。除了隔离硬件结构不同之外,Windows本身也有可能使用不同版本的内核,所以系统调用的接口自然也是不一样的。

GitHubhttps://github.com/fengbingchun/Messy_Test

发布了718 篇原创文章 · 获赞 1131 · 访问量 609万+

猜你喜欢

转载自blog.csdn.net/fengbingchun/article/details/102166511