前言

学校的操作系统实验课虽然很糟糕(就没有不糟糕的实验课),但是这个实验还是很有意思的。

如果只是简单的跟着2016年堂堂更新的PPT来完成的话,这个实验还是很简单的。毕竟目标内核版本是2.6.32😁。

所以我决定给自己上点强度,在真正的现代Linux内核(5.15.153,虽然还是有点旧)上增加系统调用。

工具

不过话说回来,真正在一个裸机编译和加载一个Linux内核还是挺复杂的,再加上本人是一个WSL享受者。所以这次的实验就借助巨硬提供的好工具来做。

编译并更换WSL2的内核

巨硬为WSL2提供了非常好的内核支持,只需前往microsoft/WSL2-Linux-Kernel: The source for the Linux kernel used in Windows Subsystem for Linux 2 (WSL2)下载最新的release就可以获得到微软为WSL2特制的Linux Kernel。

在准备好编译后,在内核文件路径下输入

sudo make KCONFIG_CONFIG=Microsoft/config-wsl -j8

即可进行编译,其中-j8选项指示最多使用8个cc1进程进行编译,可以自行更改。

编译过程需要很多额外的库,如果遇到文件缺失尝试google一下是否要装某个库。

巨硬同时为WSL提供了非常方便的内核更换方式,编译后能在在目录的arch/x86/boot下找到编译后的bzImage。将其下载到windows中,在windows中的User目录下添加一个名为.wslconfig的文件,在其中添加以下内容配置WSL2的内核使用哪一个。其中字符串填入下载的bzImage的windows路径,注意使用双反斜杠。

[wsl2]
	kernel="C:\\XXX\\XXX\\XXX\\bzImage"

接着在powershell或者cmd使用wsl --shutdown关闭wsl实例,然后重启wsl即可以新的内核启动。

添加函数

最核心的工作之一,在kernel/sys.c的末尾添加一个函数,来实现我们需要的系统调用。

这里有一个需要注意的点。自2.6时代以后,随着Linux的规模越来越大,为了便于维护,所有的通用的函数定义模版都使用宏进行了包装,以便进行类型检查等操作。

使用SYSCALL_DEFINE1来进行函数定义,其中末尾的1表示该函数接受一个参数,_user宏指示编译器该指针来自用户空间不应该在内核空间直接解引用。

如果不使用该宏进行定义会导致copy_to_user函数总是返回非零值。

SYSCALL_DEFINE1(pedagogictime, struct timespec64 __user *, tv)
{
	if(likely(tv)) {
		struct timespec64 ktv;
		ktime_get_real_ts64(&ktv);

		if (copy_to_user(tv, &ktv, sizeof(ktv)))
			return -EFAULT;
		else
			return 0;
	}
	return -1;
}

该服务函数基本功能与gettimeofday基本一致。原本的实验内容就是直接使用do_gettimeofday来获取当前的Unix时间。但该函数在内核中已被弃用(因为y2038问题),需要使用更加现代的ktime_get_real_ts64

添加用户系统调用

include/uapi/asm-generic/unistd.h中模仿其他实现加入我们自己的系统调用编号以及服务函数,同时修改最大系统调用号个数,让其增加一(在5.15.153上默认系统调用的个数是449个)

#define __NR_pedagogictime	449
__SYSCALL(__NR_pedagogictime, sys_pedagogictime)

#undef __NR_syscalls
#define __NR_syscalls 450

添加架构相关的Entry

Linux内核仓库维护系统调用表来自动生成于系统调用相关的汇编代码。两个表分别为arch/x86/entry/syscalls/syscall_32.tbl arch/x86/entry/syscalls/syscall_64.tbl

考虑到我自己的处理器架构是x86-64,所以我只修改了syscall_64.tbl。根据该文件里注释的提示,在448后面加上一条,其中64代表该条目的abi为64位

449 64      pedagogictime        sys_pedagogictime

测试内核

加载完新内核后,输入uname -a来确认是否真的加载了新内核。

Linux Amadeus 5.15.153.1-microsoft-standard-WSL2 #17 SMP Mon Jun 3 23:44:57 CST 2024 x86_64 x86_64 x86_64 GNU/Linux

编译时间显示确实是刚编译好的内核。

接着编写一个简单的函数来测试内核的修改是否生效。

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

struct timespec64
{
    long long int tv_sec;
    long tv_nsec;
};

int main ()
{
    struct timespec64 tv;
    long tmp = syscall(449, &tv);

    if (tmp == -1)
    {
        printf("Syscall return wrong\n");
        return 1;
    }

    printf("First, user get tv_sec:%lld\n", tv.tv_sec);
    return 0;
}

编译运行符合预期,确实输出了Unix时间。

结语

抛开无聊的实验课程和实验报告来说,这个实验本身还是很有意思的。让人亲手编译一次Linux内核,看着不断滚动的编译语句总是给人一种Super Hacker的快乐呢。

虽然给内核添加系统调用在XV6实验中已经做过很多次了。但实际到一个真实的系统还是很困难的,毕竟真实的Linux还是过于庞大了,如果没有网络上良好的指导和博客,还是很难做到的。

感谢巨硬提供这么好用的WSL,赞美WSL!!!

参考

Linux 内核 | 内核的时间函数 - 一丁点儿 (dingmos.com)

Linux内核之 printk 打印 - 皓然123 - 博客园 (cnblogs.com)

WSL2(Ubuntu)下添加新的Linux(5.7.9)系统调用_wsl2 系统调用-CSDN博客

linux - How can i print current time in kernel? - Stack Overflow

microsoft/WSL2-Linux-Kernel: The source for the Linux kernel used in Windows Subsystem for Linux 2 (WSL2) (github.com)