Linux系统调用
转自http://www.tinylab.org/linux-system-calls/
by Pingbo Wen of TinyLab.org
2013/09/12
系统调用是系统内核提供给用户态程序的一系列API,这样应用程序就可以通过系统调用来请求操作系统内核管理的资源[1]。本文尝试分析在Linux下是如何使用linux内核给我们提供的API,并分析其实现过程。
一、用户态
不管我们是打开一个文件,接收一个socket包,还是获取当前进程信息,都需要调用内核给我们提供的API。这里,我们可以通过strace这个工具,来跟踪一个程序调用的系统函数。比如下面是命令”strace whoami”的输出结果:
我们发现whoami首先调用getuid来获取当前有效用户ID,然后打开”/etc/passwd”文件,利用之前获取到的用户ID来获取对应的用户名,最后打印到当前终端上。
在这个过程中,就调用了execve, open, geteuid, read, write等系统调用。一个系统调用就意味着一次用户态到内核态的切换,并且每一个系统调用都会和一个内核中的函数相对应。这里我们就以geteuid这个系统调用为例,来跟踪整个系统调用关系。
首先,我们先自己实现一个调用geteuid的小程序,代码如下:
编译执行后,我这里得到的结果是1000,也就是当前用户id为1000。
然后,通过查看/usr/include/unistd.h头文件,我们知道geteuid的实现在libc中。那么我们可以先反汇编一下libc这个库,这里写了一个脚本来从一个共享库中反汇编指定的函数,脚本如下:
运行这个脚本:
我们发现这个函数的反汇编指令很简单:
先把0x6b放到寄存器eax中,然后就执行一个syscall的指令。最后是返回指令。
syscall是什么指令?这里的syscall指令就是在x86架构下,专门为系统调用准备的指令(SYSCALL/SYSENTER and SYSRET/SYSEXIT)。
那0x6b又是什么?这是系统调用号,用来区分其他的系统调用。现在我们只是看到了反汇编的代码,而这些系统调用的真真实现可以在glibc的源码中找到[2]。其实在glibc中并没有去实现系统调用,而是对不同系统内核的系统调用的wrapper。在sysdeps/unix/sysv/linux/syscalls.list下,就列出了linux下面所有的系统调用,部分如下:
这个文件指定了每一个系统调用对应的内部实现函数名,以及对应的文件名。在编译glibc的时候,syscalls.list文件会被sysdep/unix/make-syscalls.sh脚本处理,这个脚本会利用sysdeps/unix/syscall-template.S这个模板文件,来生成每一个系统调用wrapper的汇编代码,最后生成我们刚才反汇编的那样的代码[3]。
现在我们知道geteuid()函数最后调用的是一个syscall指令。那么我们能不能跳过glibc的wrapper,直接调用linux内核的系统调用呢?在以前的老版本内核中,确实提供了_syscall,_syscall0等宏,但是现在已经没有了。只保留了glibc中给我们提供的syscall函数,其函数声明在/usr/include/unistd.h文件中,原型如下:
我们可以通过syscall来实现和之前一样的效果,代码如下:
这里,107就是我们刚才在反汇编代码中看到的0x6b。而这些系统调用也在头文件/usr/include/asm/unistd_64.h中找到,如果我们包含文件,我们可以通过”syscall(__NR_geteuid)”来实现和之前一样的效果,但是更直观一点。
如果我们想完全跳过glibc,我们可以写一个汇编代码:
我们可以通过如下命令,来编译执行:
由于并没有调用输出函数,所以我们只能通过strace来跟踪具体的系统调用,运行后,输出如下:
可以看到,通过syscall调用了geteuid系统调用,并返回了正确的结果。
要注意的是,这里的实现是64位的版本,32位的linux系统调用号和64位的是不一样的,具体可以通过/usr/include/asm/unistd_32.h来获取具体的系统调用号。并且32位下面是用int 0×80软件中断来实现系统调用的分发,而64位是通过syscall指令来实现的。
二、内核态
现在,我们已经知道了用户态程序是如何调用linux内核提供的系统调用,但是真真的实现却是在kernel中。所以现在我们要进入kernel源码,来分析具体系统调用实现,最后我们还要往kernel中添加我们自己的系统调用。
这里还是以geteuid为例。
以前的linux kernel的系统调用是在arch/$(arch)/kernel/syscall_table.S文件定义的,但是在3.2以后,就已经改变了,相关patch可以到lkml.org中找到[4]。x86下最新的syscall定义在arch/x86/syscalls中。其中的syscall_64.tbl就是64位下所有的系统调用表,部分如下:
我们可以发现geteuid的系统调用号和之前是一致的。而在编译的时候,就会通过syscallhdr.sh和syscalltbl.sh两个脚本读入对应的系统调用表,来生成unistd_64.h和其他头文件。而这些文件,就是我们刚才在系统里看到的。
通过syscall_64.tbl文件,我们发现geteuid对应的内核函数是sys_geteuid。而这个函数的实现是在kernel/sys.c文件中,源码如下:
其中SYSCALL_DEFINE0是一个宏,0代表这个函数不带参数。这些宏的定义在include/linux/syscalls.h文件中。
知道了一个系统调用的实现,我们就可以利用kernel给我们提供的SYSCALL_DEFINEn宏来添加我们自己的函数,并把我们自定义的函数添加到syscall_64.tbl文件中就可以了。这里,实现了一个很简单的函数,每次调用,都会返回一个字符串。源码如下:
这里函数mysyscall带一个参数,注意参数的声明,中间有一个逗号。
然后在syscall_64.tbl中添加自己的函数:
现在,我们可以编译我们定制的内核,并加载这个内核。然后我们可以在系统中写一个程序,来调用我们自己写的系统调用。源码如下:
运行这个程序,你应该会看到”str: strings from kernel”。
三、系统调用加速
现在,我们应该很清楚一个系统调用是怎样从用户程序传递到内核中的。但是,我们知道,从用户态陷入到内核态是一个比较昂贵的切换,如果一个系统中,同时有很多系统调用,这将会严重拖慢整个系统。系统调用的设计初衷就是做为一个系统门卫,只让用户态程序访问它应该访问的资源。但是,有一些系统调用是无害的(比如,获取时间),如果能够让这些系统调用存在于用户态,那就会极大的减少用户态到内核态的切换,从而提高系统性能。
Linux kernel中,有vdso和vsyscall的机制,用来加速特定的系统调用。两者的基本原理都是把一些特定的系统调用放到一个专门的page中,然后把这个page映射到用户程序空间,这样用户态程序就可以不用切换到内核态就可以调用这些函数。
如果你用ldd查看任意一个动态链接程序的库依赖,你将会发发现每一个程序都会依赖一个linux-{vdso, gate}.so.1的库,但是这个库却没有任何文件与之想关联。比如,下面是”ldd /bin/true”的输出:
这里的linux-vdso.so.1就是之前所说的vdso机制。这个库是内核虚拟的,然后映射到所有用户态进程。你也可以通过查看/proc/self/maps查看具体的内存映射,下面是”cat /proc/self/maps”的输出:
这里,你可以看到vdso和vsyscall的内存映射。需要指出的是vdso和vsyscall的最大的区别是vdso映射到用户态的内存地址是随机的,而vsyscall确实固定的。你可以通过运行多次”cat /proc/self/maps”来比较它们的地址。
由于vsyscall的地址是固定的,这就给内核留下一个巨大的内存溢出漏洞。所以在最新的内核,vsyscall已经逐渐废除,但是你还是能在很多系统中,看到两者的共存,这只是为了向后兼容罢了。并且最新的内核中,vsyscall中已经没有任何指令了,取代的是内核的一个trap,当以前的老程序调用vsyscall里的内容时,会被导向到正常的系统调用。这也是为什么在读取vsyscall的时候,发现里面是空的[5]。
vdso的具体实现在arch/x86/vdso中,其中的vdso.lds.S就定义了具体加速的系统调用。你甚至可以往vdso添加自定义的函数,具体添加方法见这里[6]。
REFERENCE
- [1]. System Call: http://en.wikipedia.org/wiki/System_calls
- [2]. Glibc Wrapper: https://sourceware.org/glibc/wiki/SyscallWrappers
- [3]. Syscall: http://www.ibm.com/developerworks/library/l-system-calls/
- [4]. Kernel Syscalltbl: https://lkml.org/lkml/2011/11/17/388
- [5]. vsyscall vs vdso: http://lwn.net/Articles/446528/
- [6]. Customize vdso: http://www.linuxjournal.com/content/creating-vdso-colonels-other-chicken
相关推荐
本实验指导——“操作系统实验指导——基于Linux内核(第2版)”旨在帮助学习者深入理解操作系统的原理,并通过实际操作来增强对Linux内核的熟悉度。 在学习操作系统时,我们首先会接触到进程管理、内存管理、文件...
【操作系统课程设计——Linux系统调用可视化实验报告】 在操作系统的学习过程中,系统调用是连接用户空间和内核空间的重要桥梁。本实验旨在通过可视化Linux系统调用的过程,加深对系统调用的理解,同时锻炼动手实践...
在本篇Linux内核实验报告——实验4中,主要涵盖了两个关键知识点:动态模块设计和proc文件系统。实验目的是让学生理解和掌握Linux内核模块的编写,以及如何通过proc文件系统来与用户空间交互。 首先,实验A部分涉及...
操作系统实验一的主要目标是熟悉Linux操作系统环境,包括安装配置、基本操作、文件系统管理以及文本编辑。这个实验将涵盖以下几个核心知识点: 1. **Linux操作系统安装与配置**:实验要求参与者掌握Linux操作系统的...
- **递归编译各对象**: 内核编译涉及大量源文件,通过递归调用Makefile实现自动化编译。 - **链接vmlinux**: vmlinux是未压缩的内核映像,通过链接步骤生成。 - **制作bzImage**: bzImage是压缩后的内核映像,...
1. 用户态到内核态的切换:用户程序执行系统调用指令后,CPU进入陷阱模式,由用户态转换为内核态。 2. 系统调用号解析:处理器将系统调用号放入特定寄存器,内核根据这个号码调用相应的处理函数。 3. 执行服务:内核...
Linux系统调用是用户程序与操作系统内核交互的主要途径。它们提供了访问操作系统功能的接口,如创建进程、打开文件、读写文件等。通过系统调用,程序员可以实现高级的程序功能。常见的系统调用包括`fork()`用于创建...
6. **系统调用接口**:内核通过系统调用接口提供服务,允许用户空间的程序请求内核执行特定操作,如读写文件、创建进程等。 学习“Linux内核”时,你需要掌握以下几个关键知识点: 1. **内核编译与配置**:了解...
Linux® 系统调用 —— ...不过您清楚系统调用是如何在用户空间和内核之间执行的吗?本文将探究 Linux 系统调用接口(SCI),学习如何添加新的系统调用(以及实现这种功能的其他方法),并介绍与 SCI 有关的一些工具。
在Linux系统中,当程序从用户态切换到内核态时,会发生堆栈切换。这是由于用户态和内核态拥有不同的权限级别,因此它们各自的堆栈也必须分开,以确保安全性。具体来说,当发生系统调用或异常时,处理器会自动保存...
本主题将深入探讨这两个重要领域,特别是针对"19——Linux的系统调用与文件IO(二)"这一视频教程内容进行详细阐述。 首先,让我们理解什么是系统调用。系统调用是用户程序与操作系统内核进行通信的唯一合法方式,它...
Linux系统调用是操作系统提供给用户空间程序与内核交互的一种机制,它是操作系统核心功能的直接接口。在Linux中,当用户程序需要执行如文件操作、进程管理、网络通信等低级任务时,就需要通过系统调用来实现。这是...
在IT领域,Linux内核是操作系统的核心部分,它负责管理硬件资源、提供系统调用接口以及维护系统的稳定性与安全性。"边干边学——LINUX内核指导"这个标题暗示了这是一份实用型的学习资料,旨在帮助读者通过实践来理解...
《深度探索Linux操作系统——系统构建和原理解析》是一本专为那些希望深入了解Linux操作系统内核和系统构建的读者量身打造的专业书籍。它详细阐述了Linux操作系统的内部工作机制,涵盖了从内核设计到系统服务的...
### Linux内核中断详解 #### 一、中断基础 ...通过深入了解Linux内核中断和异常处理的原理及其实现细节,开发者可以更好地理解Linux操作系统是如何管理和响应各种硬件事件的,这对于编写高效的驱动程序至关重要。
操作系统实验七——系统调用实验主要目标是让学生深入理解Linux内核中的系统调用机制,包括系统调用的实现框架、用户界面、参数传递、进入和返回过程。实验内容涉及向Linux内核添加一个新的系统调用,即创建一个名为...
#### 二、Linux内核实验内容 ##### 2.1 Proc 文件系统实验 **2.1.1 Proc 文件系统简介** - **定义**: `/proc`是一个特殊的文件系统,用于提供内核和进程信息。 - **特点**: 该文件系统是虚拟的,其内容由内核动态...
- **系统调用参数**:系统调用是内核与用户空间交互的主要方式之一。了解系统调用的参数传递机制有助于更好地理解内核与应用程序之间的通信方式。 - **fork()系统调用**:`fork()`是创建新进程的关键系统调用,其...