Skip to content

Latest commit

 

History

History
145 lines (105 loc) · 5.8 KB

系统调用.md

File metadata and controls

145 lines (105 loc) · 5.8 KB

系统调用

原有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

生成的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

为什么不需要传参?因为这是上层封装函数需要做的事情。我们之所以不主动把参数放入诸如a0a1……寄存器中,是因为我们想通过上层的函数封装,让编译器自动帮我们完成:

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的内容转为汇编的宏定义,继而汇编器再将汇编宏转为我们最终的代码。

syscall.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

参考文献

  1. __SYSCALL identifier - Linux source code (v5.17.1) - Bootlin

  2. c - Is it possible to define macro inside macro? - Stack Overflow

  3. python读取文件中内容并根据空格进行分割和处理