给NetBSD添加系统调用

NetBSD本来官方文档里( http://www.netbsd.org/docs/internals/en/chap-processes.html ,3.2.5节 )就有添加系统调用的相关指导的,但是实际根据那个指导操作的时候,会发现指导文档过期得有点严重,已经无法按照上面说的来达到目的了。然后我在其他地方找到了关于OpenBSD的教学( http://www.onlamp.com/pub/a/bsd/2003/10/09/adding_system_calls.html ),同样可以用在NetBSD上。选用NetBSD是因为它安装包小,内核编译又快,一来减少下载时间,二来减少编译时间。不过如果x86-32和x86-64都想玩过去的话,总下载量也还是会有七百来兆的:两片ISO(每个300多兆)和一个内核代码包(40兆)。

添加系统调用本来的目的是为了做“x86平台内存分页机制”实验的时候来执行一些特权操作的。这里用的系统是NetBSD 6.1.4 i386版本。

NetBSD的源代码包里有一个syssrc.tgz,编译内核需要的是这个包。到它任意一个镜像FTP服务器上 /pub/NetBSD/NetBSD-6.1.4/source/sets/ 路径即可找到。

我自己操作成功的步骤是:

1、在系统调用表里面添加新系统调用。在 /usr/src/sys/kern/syscalls.master 文件末尾,按照它上面的写法,写一个自己的系统调用。这个文件的开头有各个参数、格式的说明。
其中有一个叫做RUMP的参数,从名字推断不出来是什么,查英语词典也不太有用,因为查出来单词的解释感觉对不上。上网搜了一下,有个叫RUMPKernel的东西,似乎是有点类似用户态跑的内核那种东西,但是和user mode linux又貌似不一样。没找到能理解它到底是做什么的教学,也没从makesyscalls.sh里面看出名堂来。如果定义了RUMP,那么这个系统调用在 /usr/src/sys/rump/librump/rumpkern/rump_syscalls.c 里面会出现;如果没定义,则这里会是另一个占位符。因为不会用,所以先放着……别用这个参数应该不会有什么问题。
例如要写一个能够返回cr3寄存器的值的系统调用,就写这么一个:
475  STD  { int|sys||getcr3(); }
其中int是返回类型,sys是前缀,里面貌似暂时没发现有其他前缀的,写了其他的会怎么样也不知道,试验的结果就是有一些比如syscalls.c文件里面,函数名前面会带着前缀(sys前缀的就不带着),然后在 /usr/src/sys/syscall.h 文件里,在SYS_后面也会继续跟着前缀。到底什么意义不太清楚……后面是函数名和参数。中间空着的那个是兼容性的东西,看了一下之前它们写的,老实说吧也没看懂,加了以后生成的函数名会经过特殊处理。因为不知道怎么用,所以也就先不用它。
所以也就迷迷糊糊跳过了一堆不知道干啥的选择,只挑会用的用,改完了这个文件。

2、生成新的系统调用相关代码。这个 syscalls.master 并不是代码文件,最终要对编译产生影响,还要利用它构建一下系统调用表入口啊啥的东西。有写好的Makefile,只要执行 make init_sysent.c 就可以重新构建好多个文件了,包括系统调用的那些头文件,甚至还有rump那边的代码也重新生成。

3、在文件列表中添加自己的代码文件。到 /usr/src/sys/conf 文件夹下,找一个叫做 files 的文件,里面是会被编译的文件的列表。可以看出 file 是定义一个要被编译的文件,直接在文件中搜索 sys_ 很快能找到它原本放系统调用的地方,往里面添加一个自己的代码文件即可。
但是这样添加的是全局的,想要只针对某一架构,比如cr3啥的是i386、amd64这一类用的,就到 /usr/src/sys/arch/i386/conf/files.i386 里面去添加会比较好。代码也就直接放在 i386 文件夹里面了。
file指令的后面有一些看起来像是用tag来定义文件作用的,使它们在某些配置下文件不会被编译。留空的话应该是什么时候都会被编译吧,不然大部分文件还是总是要被编译,却没有看到大量同一种tag。
如果需要把syscall.h之类的文件复制到 /usr/include/sys 里面,就到 /usr/src/sys/sys 去调用一下 make includes 即可(我没这样做)。

4、因为添加了新的系统调用,还需要实现它们才行。所以要创建一个刚才添加的那个代码文件,写上函数实现。系统调用的函数名,比如刚才那个,是 sys_getcr3。其中sys就是定义的前缀,然后是函数名。对系统调用来说,参数和返回值是固定的(第二个参数“固定”是指针,虽然指针类型会根据系统调用的参数不同而不同),在 /usr/src/sys/sys/syscallargs.h 文件里可以找到函数的原型,三个参数以及返回int。第三个参数一眼应该能看出来,是给你传递返回值的。中间一个参数是给你传递参数的,第一个参数好像是当前进程的一些状态信息,lwp是lightweight process的缩写,具体定义在 sys/lwp.h 里面,字面意思是轻量级进程。要取cr3可以从这里面取(cr3在PCB里面,PCB在那个lwp里面。PCB的定义和CPU类型相关,里面看到的一些 machine/xxxx 文件实际上是 /usr/src/sys/arch/硬件架构名/includes 里面的文件,构建编译环境的时候会被复制到 /usr/src/sys/arch/硬件架构名/conf/配置名/machine 文件夹里。i386的includes文件夹里有一个pcb.h),当然也可以直接用汇编来取。
取传进来的系统调用参数的时候,要用 SCARG 宏来取。SCARG宏的第一个参数是表示参数的结构体的形参,就是系统调用实现的第二个参数:那个struct。第二个参数是成员名。假设有一个这样的系统调用原型:
void getmagicnumber(int* x);
然后变成这样的系统调用:
int sys_getmagicnumber(struct lwp* l, struct sys_getmagicnumber_args* v, register_t* ret);
取参数的时候,就用
int* x = (int*) SCARG(v, x);
即可取到。因为它结构体的定义其实并不是简单把你参数全部排列进去,还考虑到高位优先和低位优先的时候,如果参数比较短,比如参数的类型是2字节的,那么应该放在前两位还是放在后两位这样的事情。所以用这个宏来取会比较通用。
返回值是代表系统调用有没有成功,成功的话返回0就可以了,没成功的话返回错误号。

5、访问用户态传进来的指针的时候,要用copyin和copyout。用man命令可以查到copyin和copyout的说明。其中copyin是从用户态内存复制到内核态内存,后者相反。貌似还有一个版本可以复制到其他进程的内存里去……

这样搞完,就可以直接重新编译内核了。因为没有对内核进行其他配置,所以直接用它原有的 GENERIC的 配置就好了。到 /usr/src/sys/arch/i386/conf 文件夹下面,用命令 config GENERIC 可以生成一个编译环境,然后根据提示 cd ../compile/GENERIC 切换到编译用的文件夹下, make depends 目测是处理依赖关系的,然后再 make 就能编译出内核了。编译非常快,虚拟机里面、第一次编译也就10分钟不到(我原来用的电脑比较慢,又没有硬件虚拟化支持,在虚拟机里要15分钟)。编译之后把生成的 netbsd 文件复制到 / 下,重启即可引导新内核。新内核里面就有自己添加的系统调用了。

 

最后给出我自己添加的getcr3系统调用的实现:

1、在 /usr/src/sys/kern/syscalls.master 文件末尾添加:

475    STD          { void*|sys||getcr3(); }

然后调用 make init_sysent.c 让它重建一下。

2、创建 /usr/src/sys/arch/i386/sys_mycalls.c 文件,内容是

#include <sys/syscallargs.h>
#include <sys/lwp.h>
#include <machine/pcb.h>
#include <sys/types.h>
#include <sys/systm.h>

int sys_getcr3(struct lwp* l, const void* v, register_t* ret)
{
	struct pcb* pcb = (struct pcb*) lwp_getpcb(l);
	*ret = pcb->pcb_cr3;
	return 0;
}

3、修改 /usr/src/sys/arch/i386/conf/files.i386 文件,找个地方加入

file    arch/i386/sys_mycalls.c

在内核构件完成之后,就可以编译这样的代码来试试看啦

#include <unistd.h>
#include <stdio.h>

int main()
{
	printf("cr3 = %08x\n", syscall(475));
	sleep(1);
	return 0;
}

加入sleep是因为如果反复运行一个进程,会发现系统每次都分配同一个页表给它。但是两个不同的进程是不会有相同的页表的,所以两个一起运行就能看到不同的页表指针。但是这个程序结束得太快,有的时候甚至后面的进程还没运行前面的进程就结束了,于是加上了sleep延缓进程结束,达到目的。最终效果是这样的:
image

这给接下去做x86内存分页机制的实验提供了前提条件。

这次搞下来,感想是从它头文件里面貌似能翻出好多好东西出来……

发表评论