Linux下的系统调用
2021-11-13 23:19:04 Author: www.freebuf.com(查看原文) 阅读量:21 收藏

之前老师上课讲过系统调用,但是系统调用讲的一点也不系统,看了一些书以后算是明白了。

image.png32位操作系统中,给每一个进程分配了4G大小的虚拟空间。虚拟空间将会映射到物理空间。虚拟空间分为两部分,内核空间和用户空间。它们之间的界限就是这个TASK_SIZE。在Linux系统中TASK_SIZE大小位3G,因此高1G是内核空间,低3G是用户空间。用户空间存放着用户的数据和代码,而内核空间则存放了操作系统相关的数据和代码。
image.png由于每个进程都会或多或少的与内核进行交互,因此内核空间的1G是所有进程共享,剩下的用户空间才是每个进程都有3G,并且彼此不知道对方的存在,彼此隔离。

操作系统的作用就是与硬件打交道。程序员自己分配硬件资源比较困难,因此操作系统被开发出来当作中间商。每当我们需要使用硬件资源的(内存、IO等),我们只需要跟操作系统说一声就可以了。

CPU分为用户态和核心态。用户态的CPU只能访问进程用户空间的数据;内核态才可以访问内核空间的数据。

我们使用C语言的标准库中printf和scanf函数,使用到了键盘和屏幕,其实这已经和硬件在打交道。因此这两个函数中都有系统调用的身影。

CPU想要从用户态转变位内核态,从软件层面只能通过中断(int)指令来进行转换,这也被称为是软中断。而在操作系统层面,则是用户使用陷入指令int,意思就是告诉操作系统将CPU切换位内核态。

CPU只有在内核态才可以访问内核空间,我们才可以进行系统调用。

其实系统调用本质上是API,操作系统的开发者专门为程序员提供的一系列函数。而我们现在用到的C语言库函数其实都是将系统调用给封装起来。就像一层壳一样,包裹着里面的一些数据。我们使用的read、write、lseek等函数就是所谓的外壳函数。

那么我们在使用这些函数的时候究竟发生了什么?

我们使用这些函数的时候,其实这些函数也调用了更里面的函数,简单来说就是我们使用的库函数帮助程序员进行系统调用。

我们会先将参数传入,32位操作系统传参的时候是通过压栈的方式进行传参,然后我们调用的函数会帮助我们把这些参数分别装入不同的寄存器。因为系统调用需要参数,它要求参数全部放入寄存器中。

首先根据我们调用的库函数的功能不同,我们的库函数会首先往eax寄存器中存入系统调用号。由于不同的系统调用功能不同,所以有不同的系统调用号:
64位系统调用表
32位系统调用表
以上就是系统调用号的查询。具体的参数根据系统调用的不同也不一样。

将系统调用号和参数全部存入寄存器以后,外壳函数还会帮助我们使用陷入指令,即int 0x80。我们可以回想一下我们在做pwn的题目的时候,有一种类型为ret2syscall的利用方式。只不过这是我们手动进行寄存器传参,手动调用int 0x80。

当我们使用了该指令以后,将会触发CPU中断并且将其置为内核态。此时的CPU就已经可以调用系统调用了。回到操作系统层面上,我们使用了int 0x80指令以后操作系统会调用system_call()函数。并且对中断进行处理。

内核中其实也有栈空间,就是用来存放我们传入寄存器的参数。内核会将寄存器中的参数压入内核栈中,并且将系统调用号进行比对和索引。内核中其实存放有一张表叫做sys_call_table。内核会查看系统调用号并且根据系统调用号寻找到表中记录的系统调用。并且会检查传入的参数是否合规。并且将检查结果以数值的方式返回到system_call()函数。

检查完成后正式调用系统调用,执行一系列操作。最后保存返回值。并且CPU回到用户态。

一般情况下,调用成功并且正常返回,返回值应该是一个非负整数。

如果系统调用出现错误,外壳函数会使用系统调用的返回值来设置errno。errno是一个全局变量,也就是说如果出现两次错误的话,errno只会显示后面一次的返回值。因此我们也要及时输出errno。

其实在调用成功以后,system_call函数将会返回一个非负值。而如果调用失败,将会根据errno的定义进行取反。errno都是非负数,而该函数会根据错误情况找到对应的errno并且进行取反操作。返回到外壳函数以后外壳函数又会对system_call函数的返回值进行取反并且赋值给errno。而外壳函数自己则返回-1。当然这是绝大多数情况,还有一些例外需要大家自己探索。

image.png

尽量讲全了,这就是系统调用,其实逻辑不是很复杂,就是各种套娃。C语言太难了。


文章来源: https://www.freebuf.com/articles/system/304673.html
如有侵权请联系:admin#unsafe.sh