文章目录
- 自定义系统调用
- 1.实验基本环境
- (1).基本系统环境
- (2).选择替换WSL内核的起因
- (3).我尝试的改进措施
- 2.添加系统调用
- (1).系统调用位置
- (2).系统调用函数编写
- (3).添加系统调用号
- (4).添加编译参数并编译
- #1.一次极其失败的尝试
- #2.推倒重来
- (5).尝试调用sys_mysyscall
- 3.后记
- 参考文献
- 附录I. 我实现的println函数
自定义系统调用
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中可以直接找到:
从仓库下载代码后,在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内核的时候,出现了以下的问题:
事实上就是编译器在此检测到了可能发生的无限递归,这个问题本来是不该出现的,但貌似是因为一些编译参数而导致了这个问题,所以我们需要在这里通过__attribute__(())的方式告诉编译器,runcmd函数明确不存在返回值,也就是改为下图的情况:
这样一来,重新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);
编辑后的结果:
(4).添加编译参数并编译
#1.一次极其失败的尝试
请注意,下面的一部分,全部都是错误操作!请不要跟着操作!
请注意,下面的一部分,全部都是错误操作!请不要跟着操作!
请注意,下面的一部分,全部都是错误操作!请不要跟着操作!
接下来就要编译内核了!首先清理编译信息:
make mrproper
安装必要的库:
sudo apt-get install libncurses-dev flex bison libelf-dev libssl-dev dwarves
然后配置.config文件:
make menuconfig
完成.config文件的配置,保存并退出,然后开始make:
make -j16
非常顺利的在我还没开始take a seat and relax的时候,就报错了,非常好:
看了看报错信息,回去看了以下syscalls_64.tbl,发现自己复制450号系统调用下来之后忘记把调用号改成451了,然后改回去了
然后就重新配置,然后重新make -j16开始编译,这下看起来比较顺畅,可以take a seat and relax啦!
我还真以为就结束了,结果说完这句话就error了:
在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++) {
内核的编译果然很严格哈,修改完了之后我又开始make -j16,结果没多久又出问题了,这次是在block/blk-iocost.c下的两个seq_printf的format字符串中有两个和后续打印内容的数据类型不匹配的,我也在[PATCH] block/blk-iocost (gcc13): cast enum members to int in prints (kernel.org)找到了解答,修改了如下的内容:
这一次,编译通过啦!
接下来在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
然后我重写了一次系统调用,然后使用上面的命令重新编译,完成替换,最后的最后:
泪目了,内核替换成功了,以前喜欢给手机刷机的时候,大家说:不要在晚上八点之后给手机刷机,现在我也想说:不要在晚上八点之后尝试编译大型项目,内核的替换成功了,这也就说明,我们的代码应该基本没有问题了,接下来就可以尝试在用户态调用系统调用了。
(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
最后的最后,我们可以看到:
调用信息里出现了我们刚刚写的系统调用会打印的两条信息,并且它也觉得20240307是个很好的数字,至此,自定义的sys_mysyscall已经顺利完成。
3.后记
说实话,这个实验不太难,但是的确出现了很多问题,其实我早在一年之前就已经通过微软的WSL内核仓库更新过WSL内核,当时是跟着一篇教程完成的,非常顺利,过程中一个ERROR都没有出现过,而这一次就显得磕磕绊绊的,甚至中间还出了个大错误,不过这倒的确是个有意思的过程:我真的在我经常用到的系统内核里加入了自己的代码,成就感相当足啊。
参考文献
-
- 操作系统概述 (操作系统的历史;学习建议) [南京大学2023操作系统-P1] (蒋炎岩)
-
- WSL2编译内核并更改替换内核版本_linux-msft-wsl-5.15.123.1+linux-msft-wsl-6.1.21.2-CSDN博客
-
- Advanced settings configuration in WSL | Microsoft Learn
-
- 为linux添加一个系统调用_利用内核模块法为linux添加一个系统调用-CSDN博客
附录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;
}