实现OpenBSD的系统调用

2020-06-30 21:28:09

TL;DR:我发现了一份作业的打印本,那是我在学生时代为OpenBSD和Linux实现系统调用时必须做的。我丢失了原始的LaTeX文件,所以我决定重写它,这样我就有了一份数字副本。这篇文章最初介绍了OpenBSD中不再存在的可加载内核模块(LKM),但我删减了这一部分。我还删减了Linux部分,因为我当时并不关心它,只做了最少的及格;-)。

像往常一样,非常感谢在Patreon或GitHub上赞助我的人,你可以在这里阅读更多关于我的赞助的信息。

这里的内容是基于我在2005年写的一个任务,这绝不是鼓励您编写系统调用并将其发送到OpenBSD的教程。

人们经常互换使用这两个词,但理解程序和进程之间的区别很重要,特别是因为可能允许同一个程序在一个进程中使用系统调用,而不允许在另一个进程中使用系统调用,而且还因为该进程是Syscall API的一部分。

程序是一个可执行文件,其中包含一组旨在执行和执行某些操作的指令,它以结构化文件的形式驻留在文件系统中,该文件对谁可以或不可以执行它施加了限制。

进程是该程序的一个实例,运行在它自己的内存空间中,拥有自己的特权。

如果我们使用/bin/ls,它是一个列出目录和文件的程序。当用户执行它时,将创建一个进程,该进程将在不与其他进程共享的内存空间中以该用户的权限实际运行该程序。

类UNIX系统有一个体系结构,其中代码在两个主要区域执行:内核和用户区域。

内核负责提供和限制对设备的访问,对正在执行的程序可以做什么实施限制,并为程序提供可以在其中执行的虚拟内存空间。

程序在用户土地上执行,并在进程初始化期间对内核分配给它的内存执行操作。当程序需要访问设备或需要内核执行它自己不允许执行的操作时,它会请求内核触发系统调用。系统调用是内核的一部分,并代表进程作为内核的一部分运行。

系统调用是内核提供的一种服务,以便用户域进程可以请求内核代表它做一些事情,通常是用户域程序不能或不允许自己做的事情。

从程序的角度来看,它是一个有点特殊的函数,它可以像调用任何其他函数一样调用它,但是它不在进程内存空间中运行。程序只知道系统调用接口,但没有访问它的实现的权限,所以它可以调用它,向它传递参数,从它获得结果,但不检查它运行时系统调用内部发生了什么。它不能对它进行调试。

这会带来副作用。在性能方面,系统调用会将执行切换到代价高昂的内核。然后,系统调用中的bug与函数调用中的bug具有不同的影响:内存损坏bug可能导致进程终止,而系统调用中的相同内存损坏bug可能导致系统崩溃。

系统调用实现(调用时将在内核中运行的系统调用的实际代码)和系统调用接口(系统调用接口是从用户端应用程序调用系统调用的方式)。区分两者很重要,因为在OpenBSD中,系统调用实现的原型与系统调用接口的原型不匹配,这一点我们很快就会看到。

显然,非特权帐户不允许更改内核,因为它在系统上强制执行权限。因此,至少在安装修改后的内核时,必须使用特权帐户。

系统源可直接从OpenBSD项目获得。对于此分配,我们需要以下档案:

访问系统源后,可以使用以下命令重新构建内核:

重建只需要几分钟,并且在新内核不稳定的情况下会自动执行前一个内核的备份副本。

如果对内核的更改影响userland工具,则可能需要重新构建系统。例如,如果您更改ps、top或unam等工具使用的struct proc,则可能会出现这种情况。重新构建非常简单,如下所示:

重建需要比内核多得多的时间,根据您的体系结构的不同,重建的时间可能从几分钟到几个小时不等。

首先,我们将实现不带参数的sys_告别()系统调用。它的原型是:

#include<;sys/tyes.h>;#include<;sys/param.h>;#include<;sys/systemm.h>;#include<;sys/kernel.h>;#include<;sys/proc.h>;#include<;sys/mount t.h>;#include<;sys/syscallargs.h>;/*。在控制台上*/sys_再见(struct proc*p,void*v,register_t*retval){printf(";再见,残酷世界!\n";);return(0);}。

我们的第一个系统调用只显示句子“再见,残酷的世界!”它允许我们看到系统调用的原型在用户端和内核之间是不同的。OpenBSD为所有系统调用提供了唯一的API,无论它们向用户端公开的原型是什么。

此处包含的标头是syscall API正确操作所需的最小集合。有些可能看起来没有被我们的函数使用,但将在构建时用于内核的内部管路。系统调用并不局限于其实现,正如我们稍后将看到的那样,一些元素将间接地、自动地累加起来。

我们的第一个系统调用将丢弃其参数(struct proc*p、void*v和register_t*retval),使用printf()并返回0以指示调用者执行正常。这里,printf()对于userland printf()不会被误解,前者用于输出到控制台,而不是标准输出。

我们的第二个系统调用sys_showparams()接受一个int参数并将其值打印到控制台。它的原型如下所示:

#include<;sys/tyes.h>;#include<;sys/param.h>;#include<;sys/systemm.h>;#include<;sys/kernel.h>;#include<;sys/proc.h>;#include<;sys/mount t.h>;#include<;sys/syscallargs.h>;/*向控制台显示整型参数值*/sys_showparams(struct proc*p,void*v,register_t*retval){struct sys_showparams_args/*{syscallarg(Int)val;}*/*uap=v;printf(";showparams(%d)\n";,scarg(uap,val));return(0);}。

与前一个函数不同,此函数不会忽略其参数,因为它必须提取在userland接口中传递的整数参数。为此,它声明了一个指向struct sys_showparams_args结构的指针,并使其指向其第二个参数void*v。很明显,此参数shomehow表示系统调用的userland参数。struct sys_showparams_args的定义不是我们实现的一部分,因为它是在构建时自动生成的。它的每个字段都对应于。而不必担心体系结构的对齐或字节顺序。

系统调用sys_retparam()接受int参数,小于等于1024则返回,否则失败返回-1,将errno设置为EINVAL。其原型与sys_showparams()类似:

#include<;sys/tyes.h>;#include<;sys/param.h>;#include<;sys/systemm.h>;#include<;sys/kernel.h>;#include<;sys/proc.h>;#include<;sys/mount t.h>;#include<;sys/syscallargs.h>;/*小于等于1024*/sys_retparam(struct proc*p,void*v,register_t*retval){struct sys_retparam_args/*{syscallarg(Int)val;}*/*uap=v;unsign int val;val=scarg(uap,val);if(val>;1024)return(EINVAL);*retval=v;unsign int val;/sys_retparam(struct proc*p,void*v,register_t*retval){struct sys_retparam_args/*{syscallarg(Int)val;}*/*uap=v;unsign int val;val=scarg。

事情变得稍微复杂一些,我们需要深入到函数外部发生的事情来了解发生了什么。问题是:如果我们必须在成功的情况下返回0,在错误的情况下返回正值,那么如何让系统调用在成功的情况下返回正值呢?

解决方案驻留在系统调用的第三个参数中。系统调用返回的值没有映射到userland中的系统调用接口返回的值。系统调用中的返回值仅用于确定执行是否正确或设置错误。userland接口返回的值实际上放在系统调用实现的第三个参数中,这实际上是一个由两个寄存器组成的数组。

该数组的第一个索引表示EAX寄存器,在syscall API调用我们的实现(可能会对其进行修改)之前将其初始化为0。第二个索引很少使用,它允许解决fork()的情况,fork()返回两个值,一个用于父进程,另一个用于子进程。

我们的最后一个系统调用sys_retpid()接受一个int参数,如果为0,函数将返回进程PID,如果为1,则返回父进程PID,在所有其他情况下返回FAIL,并将errno设置为EINVAL。它的原型如下所示:

#include<;sys/tyes.h>;#include<;sys/param.h>;#include<;sys/systemm.h>;#include<;sys/kernel.h>;#include<;sys/proc.h>;#include<;sys/mount t.h>;#include<;sys/syscallargs.h>;/**如果val==0,则返回当前PID*如果val==1*return-1,则返回父PID,否则将errno设置为EINVAL*/sys_retpid(struct proc*p,void*v,register_t*retval){struct sys_retpid_args/*{syscallarg(Int)val;}*/*uap=v;unsign int val;val=scarg(uap,val);if(val。如果(val==0)*retval=p->;p_pid;Else*retval=p->;p_pptr->;p_pid;return(0);}。

最后一个调用允许说明函数不是在Userland中执行,而是真正在内核中执行,它允许我们访问当前进程以外的内存。在这里,我们取消引用与我们的进程相关联的struct proc,但也引用了一个指向不同struct proc的指针,我们可以使用struct proc中的各种链表来访问当前进程在userland中不可用的资源。

请注意,这只是一个示例,在需要时应小心进行适当的锁定,如果系统调用访问已释放的资源,则结果不是进程崩溃,而是系统崩溃。

本文的初始版本始于2005年,提供了静态链接和可加载内核模块。从那时起,LKM接口从OpenBSD中删除,我也删除了这些部分,因为它们现在没有实际用途。

/usr/src/sys/kern.syscalls.master是用于添加系统调用的主文件。它用于重新生成syscall API使用的一组数组和内部结构。

/usr/src/sys/kern/init_sysen.c包含系统发送表。表中的每个元素都描述了syscall的参数数量、与这些参数相关联的结构以及实现系统调用的函数。

第一步是编辑/usr/src/sys/kern.syscalls.master并找到一个未使用的系统调用号,如果没有新的系统调用号,则添加一个新的调用号。文件格式非常简单,它由一个syscall号、一个系统调用类型和一个伪原型组成。

重新构建上述文件以考虑新的系统调用并生成其参数所需的结构,剩下的工作就是在添加了包含系统调用实现的文件之后重新构建内核。

此时,一旦使用新内核重新引导系统,我们的系统调用就可以由通过使用syscall()系统调用知道其编号的userland应用程序使用。

为了能够按名称使用它们,您必须用make init_sysen.c阶段生成的文件更新include文件/usr/include/sys/syscall.h和/usr/include/sys/sycallargs.h,然后在/usr/src/lib/libc/sys/Makefile.inc中添加我们的对象文件(不带sys_前缀)后重新构建libc。