全面剖析Linux的系统调用(设计c++库第一步)

追求适度,才能走向成功;人在顶峰,迈步就是下坡;身在低谷,抬足既是登高;弦,绷得太紧会断;人,思虑过度会疯;水至清无鱼,人至真无友,山至高无树;适度,不是中庸,而是一种明智的生活态度。

导读:本篇文章讲解 全面剖析Linux的系统调用(设计c++库第一步),希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源:原文

系统调用是什么

简要:

在现代操作系统中,内核提供了用户进程与内核进行交互的一组接口。这些接口让应用程序受限地访问硬件设备,提供了创建新进程并与已有进程进行通信的机制,也提供了申请操作系统其他资源的能力。这些接口在应用程序和内核之间扮演了使者的角色,应用程序发出各种请求,而内核负责满足这些请求(或者无法满足时返回一个错误)。实际上提供这些接口主要是为了保证系统稳定可靠,避免应用程序恣意妄行。

API、POSIX 、C库

一般情况下,应用程序通过在用户空间实现的应用编程接口(API)而不是直接通过系统调用来编程。这点很重要,因为应用程序使用的这种编程接口实际上并不需要和内核提供的系统调用对应。一个API定义了一组应用程序使用的编程接口。它们可以实现成一个系统调用,也可以通过调用多个系统调用来实现,而完全不使用任何系统调用也不存在问题。实际上,API可以在各种不同的操作系统上实现,给应用程序提供完全相同的接口,而它们本身在这些系统上的实现却可能迥异。图下图给出POSIX、API、C库以及系统调用之间的关系。

全面剖析Linux的系统调用(设计c++库第一步)

 在Unix世界中,最流行的应用编程接口是基于POSIX标准的。从纯技术的角度看,POSIX是由IEEE的一组标准组成,其目标是提供一套大体上基于Unix的可移植操作系统标准。在应用场合,Linux尽力与POSIX和SUSv3兼容。

Linux的系统调用像大多数Unix系统一样,作为C库的一部分提供。C库实现了Unix系统的主要API,包括标准C库函数和系统调用接口。所有的C程序都可以使用C库,而由于C语言本身的特点,其他语言也可以很方便地把它们封装起来使用。此外,C库提供了POSIX的绝大部分API。

程序员角度: 系统调用无关紧要,它们只需和API提供

内核角度:只跟系统调用打交道

接口设计:“提供机制而不是策略”。换句话说,Unix的系统调用抽象出了用于完成某种确定的目的的函数。至于这些函数怎么用完全不需要内核去关心.

访问系统调用(syscall)

简要:

要访问系统调用(在Linux中常称作syscall),通常通过C库中定义的函数调用来进行。它们通常都需要定义零个、一个或几个参数〈输入)而且可能产生一些副作用,例如,写某个文件或向给定的指针拷贝数据等。系统调用还会通过一个long类型的返回值来表示成功或者错误。通常,但也不绝对,用一个负的返回值来表明错误。返回一个0值通常(当然仍不是绝对的)表明成功。系统调用在出现错误的时候C库会把错误码写入errno全局变量。通过调用perror()库函数,可以把该变量翻译成用户可以理解的错误宇符串。

系统调用号

在Linux中,每个系统调用被赋予一个系统调用号。这样,通过这个独一无二的号就可以关联系统调用。当用户空间的进程执行一个系统调用的时候,这个系统调用号就用来指明到底是要执行哪个系统调用:进程不会提及系统调用的名称。

系统调用号相当重要,一旦分配就不能再有任何变更,否则编译好的应用程序就会崩溃。此外,如果一个系统调用被删除,它所占用的系统调用号也不允许被回收利用,否则,以前编译过的代码会调用这个系统调用,但事实上却调用的是另一个系统调用。Linux有一个“未实现”系统调用sys_ni_syscall(),它除了返回-ENOSYS外不做任何其他工作,这个错误号就是专门针对无效的系统调用而设的。虽然很罕见,但如果一个系统调用被删除,或者变得不可用,这个函数就要负责“填补空缺”。

因为所有的系统调用陷入内核的方式都一样,所以仅仅是陷入内核空间是不够的。因此必须把系统调用号一并传给内核。在x86上,系统调用号是通过eax寄存器传递给内核的。在陷入内核之前,用户空间就把相应系统调用所对应的号放入 eax中。这样系统调用处理程序一旦运行,就可以从eax中得到数据。其他体系结构上的实现也都类似。

参数传递

除了系统调用号以外,大部分系统调用都还需要一些外部的参数输入。所以,在发生陷入的时候,应该把这些参数从用户空间传给内核。最简单的办法就是像传递系统调用号一样,把这些参数也存放在寄存器里。在x86-32系统上,ebx、ecx、edx、esi和edi按照顺序存放前五个参数。需要六个或六个以上参数的情况不多见,此时,应该用一个单独的寄存器存放指向所有这些参数在用户空间地址的指针。

全面剖析Linux的系统调用(设计c++库第一步)

 总结

用户空间的程序无法直接执行内核代码。它们不能直接调用内核空间中的函数,因为内核驻留在受保护的地址空间上。如果进程可以直接在内核的地址空间上读写的话,系统的安全性和稳定性将不复存在。

所以,应用程序应该软中断通知系统,告诉内核自己需要执行一个系统调用,希望系统切换到内核态,这样内核就可以代表应用程序在内核空间执行系统调用。

系统调用的实现

简要:

1.用途

每一个系统调用都有一个明确的用途,不提倡多用途的接口

2.参数,返回值,错误码

系统调用的接口应该力求简洁,参数尽可能少。系统调用的语义和行为非常关键﹔因为应用程序依赖于它们,所以它们应力求稳定,不做改动。

3.可移植性和健壮性

系统调用设计得越通用越好。不要假设这个系统调用现在怎么用将来也一定就是这么用。系统调用的目的可能不变,但它的用法却可能改变。这个系统调用可移植吗﹖别对机器的字节长度和字节序做假设。。要确保不对系统调用做错误的假设,否则将来这个调用就可能会崩溃。

参数验证

系统调用必须仔细检查它们所有的参数是否合法有效。系统调用在内核空间执行,如果任由用户将不合法的输入传递给内核,那么系统的安全和稳定将面临极大的考验。

最重要的一种检查就是检查用户提供的指针是否有效。试想,如果一个进程可以给内核传递指针而又无须检查,那么它就可以给出一个它根本就没有访问权限的指针,哄骗内核去为它拷贝本不允许它访问的数据,如原本属于其他进程的数据或者不可读的映射数据。在接收一个用户空间的指针之前,内核必须保证:

内核必须保证:

1.指针指向的内存区域属于用户空间。进程决不能哄骗内核去读内核空间的数据。

2.指针指向的内存区域在进程的地址空间里。进程决不能哄骗内核去读其他进程的数据。·

3.如果是读,该内存应被标记为可读﹔如果是写,该内存应被标记为可写;如果是可执行,该内存被标记为可执行。进程决不能绕过内存访问限制。

内核提供了两个方法来完成必须的检查和内核空间与用户空间之间数据的来回拷贝。注意,内核无论何时都不能轻率地接受来自用户空间的指针!这两个方法中必须经常有一个被使用。

为了向用户空间写入数据,内核提供了copy_to_user(),它需要三个参数。第一个参数是进程空间中的目的内存地址,第二个是内核空间内的源地址,最后一个参数是需要拷贝的数据长度(字节数)。
为了从用户空间读取数据,内核提供了copy_from_user(),它和copy_to_user相似。该函数把第二个参数指定的位置上的数据拷贝到第一个参数指定的位置上,拷贝的数据长度由第三个参数决定。

合法权限检查
最后一项检查针对是否有合法权限。在老版本的Linux 内核中,需要超级用户权限的系统调用才可以通过调用suser()函数这个标准动作来完成检查。这个函数只能检查用户是否为超级用户﹔

现在它已经被一个更细粒度的“权能”机制代替。新的系统允许检查针对特定资源的特殊权限。调用者可以使用capable()函数来检查是否有权能对指定的资源进行操作,如果它返回非0值,调用者就有权进行操作,返回0则无权操作。举个例子,capable(CAP_SYS_NICE)可以检查调用者是否有权改变其他进程的nice值。默认情况下,属于超级用户的进程拥有所有权利而非超级用户没有任何权利。例如,下面是reboot()系统调用,注意,第一步是如何确保调用进程具有CAP SYS REBOOT权能。

自己设计一个系统调用

简要:

1)首先,在系统调用表的最后加入一个表项。每种支持该系统调用的硬件体系都必须做这样的工作(大部分的系统调用都针对所有的体系结构)。从0开始算起,系统调用在该表中的位置就是它的系统调用号。如第10个系统调用分配到的系统调用号为9。(系统调用号)
2)对于所支持的各种体系结构,系统调用号都必须定义于<asm/unistd.h>中。
3)系统调用必须被编译进内核映象(不能被编译成模块)。这只要把它放进kernel/下的一个相关文件中就可以了,比如sys.c,它包含了各种各样的系统调用。

参考链接

 

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/129653.html

(0)
飞熊的头像飞熊bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!