原有xv6的系统调用声明极其复杂,复杂的点在于声明一个系统调用需要同时修改syscall.c中的两处,以及syscall.h中的一处,此外还有用户空间下的usys.pl
脚本代码(加入新增entry),有时仅仅想测试一些功能就要花上大量的气力来增删。因此,我想探寻一种新的模式来帮助我们自动化这个过程。
我们主要借鉴了Linux相关的系统调用声明自动化流程,通过新增一个简单的.tbl
文件以及一小段python脚本代码从而实现了整个系统调用的自动化流水线。
tbl
文件其实是一个简单的文本文件,它其中描述了系统调用相关的基本信息:
- 系统调用号
- 系统调用名
- 系统函数名
中间使用空白字符分隔:
1 fork sys_fork
2 exit sys_exit
3 wait sys_wait
4 pipe sys_pipe
5 read sys_read
...
根据这些信息,我们可以利用脚本来生成我们想要的头文件:syscall.h syscall_gen.h,下面主要介绍这些文件的作用。
生成的syscall_gen.h文件格式如下:
__SYS_CALL(1, fork, sys_fork)
__SYS_CALL(2, exit, sys_exit)
__SYS_CALL(3, wait, sys_wait)
__SYS_CALL(4, pipe, sys_pipe)
__SYS_CALL(5, read, sys_read)
__SYS_CALL(6, kill, sys_kill)
__SYS_CALL(7, exec, sys_exec)
// ...
乍看__SYS_CALL
似乎未被定义?让我们看看它是如何被使用的。主要有两处代码使用到它:一个是在syscall.c内核代码中,我们使用它来声明系统调用函数以及函数数组初始化;另外一处地方是在在用户程序库,我们使用它来批量生成我们的系统调用函数。
读者可能会感到很奇怪,为何一个.h
可以同时实现两个功能?我们是通过#define
以及#undef
来实现的:
// 系统调用函数声明
#define __SYS_CALL(NUM, NAME, FUNC) extern uint64_t FUNC(void);
#include "syscall_gen.h"
#undef __SYS_CALL
//...
// 系统调用函数数组初始化
#define __SYS_CALL(NUM, NAME, FUNC) [NUM] FUNC,
static uint64_t (*syscalls[])(void) = {
#include "syscall_gen.h"
};
#undef __SYS_CALL
可以看到,我们前后两次#include
相同的头文件,其中通过重复的#define
来实现了不同的功能!
为了方便用户的系统调用,我们需要给用户程序封装相应的系统调用代码,其中需要用到诸如ecall
指令,使用汇编其实是最佳的选择,一个系统调用的函数如下:
.global syscall_name
syscall_name:
li a7, syscall_id
ecall
ret
为什么不需要传参?因为这是上层封装函数需要做的事情。我们之所以不主动把参数放入诸如a0
,a1
……寄存器中,是因为我们想通过上层的函数封装,让编译器自动帮我们完成:
int write(int fd, char* addr, int size) { // a0: fd addr: a1 size:a2
syscall_name();
}
尽管上述代码不算正确(没有处理返回值)不过该思想也足以体现了。那么现在我们的问题是:如何通过syscall_gen.h
这个头文件来帮助我们批量地生成这些代码?聪明的读者一定想到了,我们是不是也可以通过上文提到的#define
的形式来帮助我们完成呢?答案是否定的。因为在汇编代码中,换行是作为指令的分隔符,尽管在.S
文件中支持C的预处理器,但是在C语言的宏定义中,它不支持输出换行,可能你会疑惑我们平时宏定义也可以使用\
反斜杠来换行啊?这只是方便可读性,在实际预处理中并不是真正意义上的换行,因此该方法out。
那我们还有其他办法吗?答案是有的。既然C的宏定义不行,我们是不是能试试汇编的宏定义:
.macro syscall num name
.global \name
\name:
li a7, \num
ecall
ret
.endm
在这个基础之上,我们再结合上C的宏定义,从而实现美妙的联动!
.macro syscall num name
.global \name
\name:
li a7, \num
ecall
ret
.endm
#define __SYS_CALL(NUM, NAME, FUNC) syscall NUM, NAME
#include "kernel/syscall_gen.h"
过程大致为:C预处理器先将syscall_gen.h
的内容转为汇编的宏定义,继而汇编器再将汇编宏转为我们最终的代码。
此文件主要是为了提供系统调用号的相关宏,定义为:
#define NR_fork 1
#define NR_exit 2
#define NR_wait 3
#define NR_pipe 4
#define NR_read 5
#define NR_kill 6
#define NR_exec 7
// ...
既然有了syscall_gen.h
,为什么还需要这个文件呢?或者说,我们不能通过宏函数的骚操作再次实现我们的伟大壮举吗?确实不行,因为在 C 2011 [N1570] 6.10.3.4 3中,已经规定了:
The resulting completely macro-replaced preprocessing token sequence is not processed as a preprocessing directive even if it resembles one,…”
所以我们想通过「宏」定义「宏」的企图就泡汤了,无路可走,所以当下采取的是同时生成这两个文件来满足我们的需求。
by 杨宗振
2022/4/3