操作系统—自定义系统调用

自定义系统调用

1.实验基本环境

(1).基本系统环境

  之前看了一会儿MIT的xv6的第一个lab,跟着做了两个用户态程序(pingpong和sleep),不过我还是不太熟悉xv6本身的系统调用,因此在这里首先尝试对Linux内核进行自定义系统调用的操作,这里我采用的环境如下:

  • 系统:Windows Subsystem for Linux(WSL 2) Ubuntu-22.04 LTS
  • 编译时Linux内核:Linux-5.15.133.1-microsoft-standard-WSL2
  • 被替换Linux内核:Linux-6.1.21.2-microsoft-standard-WSL2

(2).选择替换WSL内核的起因

  这个Lab中我决定尝试在自定义完系统调用后,编译并替换掉wsl中的内核,我之前同上个学期修石亮老师操作系统的一个同学聊过,他在完成这个实验尝试替换wsl内核的时候遇到了障碍,我后来了解他的步骤之后发现可能问题出在内核代码本身:wsl内核的代码和kernel.org下的linux原版代码不太一致,微软对其进行了一定程度的定制

(3).我尝试的改进措施

  在完成这个实验的时候,我猜想他实验失败可能问题就出在这里,因此我在下载内核源码时直接从微软的WSL2-Linux-Kernel仓库中下载源码,并且因为考虑到我本机的Linux内核已经到了Linux 5.15;并且Linus对Rust持支持态度,在Linux 6系列的内核当中引入了很多由Rust实现的代码,所以这次实验中,我决定将内核替换为6.1.21.2。当然,我不是Rust的支持者,我只是想试一下新版本的内核罢了,所以6.1.21.2版本的内核代码在仓库的Release中可以直接找到:

p14

  从仓库下载代码后,在wsl终端内输入:

tar -xzf WSL2-Linux-Kernel-linux-msft-wsl-6.1.21.2.tar.gz

  这样就完成了解压操作,接下来就是具体的代码修改以及编译、替换工作了

2.添加系统调用

  一上来就修改内核,我其实还是有点害怕的,要是我代码有点什么问题,到时候我的wsl会不会出什么问题,就不得而知了

(1).系统调用位置

  Linux内核的所有syscall都被定义在./kernel/sys.c当中,我们打开sys.c可以看到如211行中定义的SYSCALL_DEFINE3:

SYSCALL_DEFINE3(setpriority, int, which, int, who, int, niceval)
{
	struct task_struct *g, *p;
	struct user_struct *user;
	const struct cred *cred = current_cred();
	int error = -EINVAL;
  ...

  所以我们后续要定义自己的系统调用,也需要再sys.c当中完成对应的函数,不过有个问题,这个SYSCALL_DEFINE3看起来并不是很直观,它和我们平时见到的C语言函数定义不太一样,因此我觉得,这肯定是一个宏定义,在我详细地阅读了sys.c文件的包含目录后,发现了一行代码:

#include <linux/syscalls.h>

  这个名字,听起来就和我想知道的系统调用内容非常相符,因此我在./include/linux下找到了这个syscalls.h,并且找到了这个SYSCALL_DEFINEx对应的宏定义:

#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)

#define SYSCALL_DEFINE_MAXARGS	6

#define SYSCALL_DEFINEx(x, sname, ...)				\
	SYSCALL_METADATA(sname, x, __VA_ARGS__)			\
	__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)

  分析一下这一串代码,SYSCALL_DEFINE1~6的实现都是基于SYSCALL_DEFINEx这个宏定义完成的,而SYSCALL_DEFINEx是一个利用可变参数实现的宏,其中的x实际上是后续可变参数的数量,它的出现应当是为了后续我们完成一系列自定义的系统调用中,参数的数量是可以自定的,之前我也曾经基于stdarg.h和fprintf实现了一个小的println函数,因为当时比较懒,就只做了format字符串解析和可变参数的部分,还没有完全实现像屏幕输出的操作,具体的代码我附在最后了,可以参考一下。

  回到关键任务上,我们发现,SYSCALL_DEFINEx这个宏还有两个调用的宏定义,一个是 __SYSCALL_DEFINEx,一个是SYSCALL_METADATA,所以我们可以在下面先找到 __SYSCALL_DEFINEx的定义:

#ifndef __SYSCALL_DEFINEx
#define __SYSCALL_DEFINEx(x, name, ...)					\
	__diag_push();							\
	__diag_ignore(GCC, 8, "-Wattribute-alias",			\
		      "Type aliasing is used to sanitize syscall arguments");\
	asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))	\
		__attribute__((alias(__stringify(__se_sys##name))));	\
	ALLOW_ERROR_INJECTION(sys##name, ERRNO);			\
	static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));\
	asmlinkage long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__));	\
	asmlinkage long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__))	\
	{								\
		long ret = __do_sys##name(__MAP(x,__SC_CAST,__VA_ARGS__));\
		__MAP(x,__SC_TEST,__VA_ARGS__);				\
		__PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__));	\
		return ret;						\
	}								\
	__diag_pop();							\
	static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))
#endif /* __SYSCALL_DEFINEx */

  我尝试读了一下,然后发现,我好像真的读不懂,虽然里面一些什么__VA_ARGS__这样的熟悉面孔,但是整体的宏定义太多,难度过大,于是我交给了ChatGPT帮我解释这段代码,我总结了一下它的意思,它说:这个宏的目的是生成系统调用函数的相关代码,首先将当前诊断状态推入栈中,以便后续诊断,之后忽略GCC的警告8类型的-Wattribute-alias,即属性别名,之后还定义了一个sys##name函数,这里的##用到了C语言宏定义的连接机制,例如:

#define CONCAT(a, b) a##b

  经过预编译后,CONCAT(aaa, bbb)会被展开为aaabbb,所以对于给定的name参数,假设name为mysyscall,那么这个函数会负责生成一个sysmysyscall的函数,之后调用了__attribute__(())这个在gcc编译器对C语言做的扩展,这个扩展可以允许我们对编译过程提供额外信息,控制编译器行为,这个其实已经用到了,在第一次环境配置的时候,部分同学在最后一步make xv6内核的时候,出现了以下的问题:

p2

  事实上就是编译器在此检测到了可能发生的无限递归,这个问题本来是不该出现的,但貌似是因为一些编译参数而导致了这个问题,所以我们需要在这里通过__attribute__(())的方式告诉编译器,runcmd函数明确不存在返回值,也就是改为下图的情况:

p2

  这样一来,重新make就不会再报错了。好了,回到内核,之后它允许对sys##name进行错误注入(这一步我没有看懂),之后的静态内联函数__do_sys##name比较关键,它将当初传入的可变参数传入,并且进行了对应的展开操作。

	asmlinkage long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__));	\
	asmlinkage long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__))	\
	{								\
		long ret = __do_sys##name(__MAP(x,__SC_CAST,__VA_ARGS__));\
		__MAP(x,__SC_TEST,__VA_ARGS__);				\
		__PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__));	\
		return ret;						\
	}								\

  之后定义__se_sys##name函数,根据参数展开后,完成真正的系统调用函数的调用,首先执行系统调用,获得返回值,赋值到ret;然后对传入参数进行测试,然后对参数和返回值进行保护操作(这里我也不知道保护是干啥的),然后再返回系统调用的返回值,这样一整个操作就结束了,__SYSCALL_DEFINEx完成了对于某一个系统调用的函数生成,以我的理解,作为内核态的程序,代码如果过于随意,实际上可能会导致很多问题,因此需要一个比较完善的保护机制来完成代码的生成,而__SYSCALL_DEFINEx完成的就是这个过程。

  然后我又找到了SYSCALL_METADATA,这是它的定义:

#define SYSCALL_METADATA(sname, nb, ...)			\
	static const char *types_##sname[] = {			\
		__MAP(nb,__SC_STR_TDECL,__VA_ARGS__)		\
	};							\
	static const char *args_##sname[] = {			\
		__MAP(nb,__SC_STR_ADECL,__VA_ARGS__)		\
	};							\
	SYSCALL_TRACE_ENTER_EVENT(sname);			\
	SYSCALL_TRACE_EXIT_EVENT(sname);			\
	static struct syscall_metadata __used			\
	  __syscall_meta_##sname = {				\
		.name 		= "sys"#sname,			\
		.syscall_nr	= -1,	/* Filled in at boot */	\
		.nb_args 	= nb,				\
		.types		= nb ? types_##sname : NULL,	\
		.args		= nb ? args_##sname : NULL,	\
		.enter_event	= &event_enter_##sname,		\
		.exit_event	= &event_exit_##sname,		\
		.enter_fields	= LIST_HEAD_INIT(__syscall_meta_##sname.enter_fields), \
	};							\
	static struct syscall_metadata __used			\
	  __section("__syscalls_metadata")			\
	 *__p_syscall_meta_##sname = &__syscall_meta_##sname;

  这一段代码用于定义系统调用的元数据信息,大概目的是用于系统调用的一些跟踪,也有可能会产生其它的用途,这里不再赘述。

  虽然上面的代码很复杂,但是实际上编写一个系统调用函数没有很复杂,只要写完函数之后再使用对应的宏即可。

(2).系统调用函数编写

  所以我们在sys.c中加入以下代码:

asmlinkage long sys_mysyscall(long num)
{
    printk("This is Voltline's syscall!\n");
    printk("I think %ld is a good number!\n", num);
    return 0;   
}

SYSCALL_DEFINE1(mysyscall, long, num)
{
    return sys_mysyscall(num);
}
  • sys_mysyscall是我的系统调用函数,我在其中使用了printk这个内核打印消息的函数打印了两条信息
  • 然后使用了SYSCALL_DEFINE1完成单一参数的系统调用代码生成
  • asmlinkage要求函数采用和内核调用约定相匹配的参数传递方式

(3).添加系统调用号

  为了方便后续调用我自己的系统调用,我还需要给内核添加这个系统调用对应的系统调用号,查阅资料之后发现我应该去./arch/x86/entry/syscalls下的syscall_64.tbl中添加一个系统调用号:

cd arch/x86/entry/syscalls

  翻到最后的最后,看到了下面的调用号表:

448	common	process_mrelease	sys_process_mrelease
449	common	futex_waitv		sys_futex_waitv
450	common	set_mempolicy_home_node	sys_set_mempolicy_home_node

#
# Due to a historical design error, certain syscalls are numbered differently
# in x32 as compared to native x86_64.  These syscalls have numbers 512-547.
# Do not add new syscalls to this range.  Numbers 548 and above are available
# for non-x32 use.
#

  下面的512-547它不让我改,所以我就在450后加上一个451号的系统调用:

450	64		mysyscall		sys_mysyscall

  然后要去./include/linux下的syscalls.h中加入我的sys_mysyscall的函数声明:

asmlinkage long sys_mysyscall(long num);

  编辑后的结果:

p3

(4).添加编译参数并编译

#1.一次极其失败的尝试

请注意,下面的一部分,全部都是错误操作!请不要跟着操作!

请注意,下面的一部分,全部都是错误操作!请不要跟着操作!

请注意,下面的一部分,全部都是错误操作!请不要跟着操作!

  接下来就要编译内核了!首先清理编译信息:

make mrproper

  安装必要的库:

sudo apt-get install libncurses-dev flex bison libelf-dev libssl-dev dwarves

  然后配置.config文件:

make menuconfig

p4

  完成.config文件的配置,保存并退出,然后开始make:

make -j16

  非常顺利的在我还没开始take a seat and relax的时候,就报错了,非常好:

p5

  看了看报错信息,回去看了以下syscalls_64.tbl,发现自己复制450号系统调用下来之后忘记把调用号改成451了,然后改回去了
p6

  然后就重新配置,然后重新make -j16开始编译,这下看起来比较顺畅,可以take a seat and relax啦!

  我还真以为就结束了,结果说完这句话就error了:

p8

  在Google之后发现有人在kernel.org上反映了这个问题:tg3: fix array subscript out of bounds compilation error (kernel.org),于是我又对这个tg3.c修改了一下:

>> -	for (i = 0; i < tp->irq_max; i++) {
>> +	for (i = 0; i < tp->irq_max && i < TG3_IRQ_MAX_VECS; i++) {

p9

  内核的编译果然很严格哈,修改完了之后我又开始make -j16,结果没多久又出问题了,这次是在block/blk-iocost.c下的两个seq_printf的format字符串中有两个和后续打印内容的数据类型不匹配的,我也在[PATCH] block/blk-iocost (gcc13): cast enum members to int in prints (kernel.org)找到了解答,修改了如下的内容:

p10
  这一次,编译通过啦!
p11
  接下来在arch/x86/boot下找到bzImage,这就是内核文件了,拷贝出来更名为kernel,然后在powershell中输入

wsl --shutdown
#2.推倒重来

  关闭WSL,然后去替换kernel,结果发现,我的wsl再也打不开了,我开始非常迷茫、混乱、崩溃,在查询了各种资料之后发现:我的问题好像根本没有人遇到过,于是我决定重新解压一遍压缩包,然后找到了另一篇资料,发现它的make只需要用到下面一行指令:

sudo make KCONFIG_CONFIG=Microsoft/config-wsl -j16

  当然,首先还是需要安装编译内核需要的依赖:

sudo apt-get install libncurses-dev flex bison libelf-dev libssl-dev dwarves

  然后我重写了一次系统调用,然后使用上面的命令重新编译,完成替换,最后的最后:
p12
  泪目了,内核替换成功了,以前喜欢给手机刷机的时候,大家说:不要在晚上八点之后给手机刷机,现在我也想说:不要在晚上八点之后尝试编译大型项目,内核的替换成功了,这也就说明,我们的代码应该基本没有问题了,接下来就可以尝试在用户态调用系统调用了。

(5).尝试调用sys_mysyscall

  接下来我在~/Document/syscall_test下创建了syscall_test.c文件,写入了下面的代码:

#include <stdio.h>
#include <unistd.h>
#include <sys/syscall.h>

int main()
{
    syscall(451, 20240307);
}

  然后编译运行程序,之后使用dmesg查看系统调用信息:

gcc syscall_test.c -o syscall_test 
./syscall_test
dmesg

  最后的最后,我们可以看到:

p13

  调用信息里出现了我们刚刚写的系统调用会打印的两条信息,并且它也觉得20240307是个很好的数字,至此,自定义的sys_mysyscall已经顺利完成。

3.后记

  说实话,这个实验不太难,但是的确出现了很多问题,其实我早在一年之前就已经通过微软的WSL内核仓库更新过WSL内核,当时是跟着一篇教程完成的,非常顺利,过程中一个ERROR都没有出现过,而这一次就显得磕磕绊绊的,甚至中间还出了个大错误,不过这倒的确是个有意思的过程:我真的在我经常用到的系统内核里加入了自己的代码,成就感相当足啊。

参考文献

附录I. 我实现的println函数

  println函数的行为与printf函数基本一致,返回值为通过format字符串打印参数的数量,可以接受一个format字符串以及对应的各种参数,在下面的实现当中,我利用有限自动机和va_list实现了对应的解析和打印操作,不过最后的输出还是简单地调用了fprintf,以后我一定会改进的(如果在网上看到这段代码的话,应该也是我写的,毕竟一般会发到博客上的都是实现得比较完整的代码):

int println(const char* format, ...)
{
	const char* p = format;
	va_list ptr;
	va_start(ptr, format);

	int state{ 0 };
	int _sum{ 0 };
	char kf[15] = "%.1";
	char pt{ 2 };
	// 0 for default char
	// 1 for %  just %
	// 2 for .(.kf) 
	// 3 for l(lf, ld, lu)
	// 4 for ll(lld, llu, llf=lf)
	// 5 for k(.kf)
	for (; *p; p++) {
		switch (*p) {
		case '%':
			if (state) {
				state = 0, putchar(*p);
			}
			else state = 1;
			break;
		case 'l':
			if (state) {
				if (state == 1) state = 3;
				else if (state == 3) state = 4;
				else if (state == 4) {
					return -1;
				}
			}
			else putchar(*p), state = 0;
			break;
		case '.':
			if (state == 1) {
				state = 2;
			}
			else putchar(*p), state = 0;
			break;
		case '0': case '1': case '2': case '3': case '4': 
		case '5': case '6': case '7': case '8': case '9':
			if (state) {
				if (state == 2) {
					state = 5;
				}
				kf[pt++] = *p;
			}
			else putchar(*p), state = 0;
			break;
		case 'd':
			if (state) {
				switch (state) {
				case 1:
					fprintf(stdout, "%d", va_arg(ptr, int));
					break;
				case 3:
					fprintf(stdout, "%ld", va_arg(ptr, long));
					break;
				case 4:
					fprintf(stdout, "%lld", va_arg(ptr, long long));
					break;
				}
				_sum++, state = 0;
			}
			else  putchar(*p), state = 0;
			break;
		case 'u':
			if (state) {
				switch (state) {
				case 1:
					fprintf(stdout, "%u", va_arg(ptr, unsigned int));
					break;
				case 3:
					fprintf(stdout, "%lu", va_arg(ptr, unsigned long));
					break;
				case 4:
					fprintf(stdout, "%llu", va_arg(ptr, unsigned long long));
					break;
				}
				_sum++, state = 0;
			}
			else  putchar(*p), state = 0;
			break;
		case 'c':
			if (state == 1) {
				putchar(va_arg(ptr, char));
				_sum++, state = 0;
			}
			else  putchar(*p), state = 0;
			break;
		case 'f':
			if (state) {
				switch (state) {
				case 1:
					fprintf(stdout, "%f", va_arg(ptr, float));
					break;
				case 3:
					fprintf(stdout, "%lf", va_arg(ptr, double));
					break;
				case 4:
					fprintf(stdout, "%lf", va_arg(ptr, double));
					break;
				case 2: case 5:
					kf[pt++] = 'f';
					kf[pt] = 0;
					fprintf(stdout, kf, va_arg(ptr, double));
					break;
				}
				_sum++, state = 0;
			}
			else putchar(*p), state = 0;
			break;
		case 's':
			if (state == 1) {
				const char* str = va_arg(ptr, const char*);
				while (*str) {
					putchar(*str++);
				}
				_sum++, state = 0;
			}
			else putchar(*p), state = 0;
			break;
		default:
			putchar(*p);
			if (state) state = 0;
		}
	}
	putchar('\n');
	va_end(ptr);
	return _sum;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值