实验四 增加Linux系统调用
实验目的:学习如何产生一个系统调用以及怎样同过往内核中增加一个新函数从而在内核空间中实现对用户空间的读/写。
注意:直接看实验步骤即可,4.2问题陈述了解即可,不怎么重要
下载一个新内核,可以去下方的网站:
点击我查看如何下载新内核
4.1 介绍
从这个实验开始,我们开始真正的接触Linux内核。Linux系统调用是用于导出可供用户空间程序调用的内核函数的名称,它们不能像普通的libc库中的函数一样调用,而必需通过0x80陷阱进核调用。这样,如果用户创建了一个新的内核函数,就必须在内核陷阱表中创建一个新的表项来引用该函数。
所有的内核函数入口表集中在/user/src/linux/arch/i386/kernel/entrys.S中。该表具有如下所示的形式:
.data
ENTRY(sys_call_table) /* 入口 */
.long SYMBOL_NAME(sys_ni_call) /* 0, 空项 */
.long SYMBOL_NAME(sys_exit) /* 1 */
.long SYMBOL_NAME(sys_fork)
.long SYMBOL_NAME(sys_read)
.long SYMBOL_NAME(sys_write)
.long SYMBOL_NAME(sys_open) /* 5 */
…
.long SYMBOL_NAME(sys_signalstack)
.long SYMBOL_NAME(sys_sendfile)
…
.long SYMBOL_NAME(sys_ni_call) /* 空项 */
.long SYMBOL_NAME(sys_ni_call)
…
.endr /* 结束 */
表项1包含了exit()系统调用(其实现为sys_exit()内核函数),表项2包含了fork()系统调用,以此类推。
系统调用是在sys_call_table中定义的,这样当增加一个新的系统调用是,就必须在这个表中增加一个新的表项。编辑该文件,增加自己的系统调用如下:
…
.long SYMBOL_NAME(sys_ni_call) /* 222 */
.long SYMBOL_NAME(sys_my_new_call) /223,用户自定义系统调用/
…
注意,linux系统自身保留了221个系统调用。这就意味着,你自己增加的系统调用至少要在第222项以后开始,还要小于256,(内核2.4的有的版本可能多达237个,因此,进建议你选择较为靠后的序号)。另外,这个操作也只有超级用户才有权限完成。
为了使普通的C函数也能够在重新编译内核之后也能调用你的内核函数,最好还要编辑 内核目录下的include/asm/unistd.h文件。
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
#define __NR_open 5
…
#define __NR_my_new_call 223
接下来,就要实现自己的内核函数了。你可以单独在一个文件添加该函数,亦可以在以有的内核源码文件中添加。内核函数的实现体具有以下的形式:
asmlinkage <func_name> (paramlist){}
其中,asmlinkage关键字指名了函数的参数的传递方式——必须使用堆栈进行。这里涉及到一个问题——int 80 到底做了些什么?事实上,它要完成一系列的动作:
1. 在系统调用表中找出对应于系统调用号的表项;
2. 确定系统调用的参数条目;
3. 将参数从用户地址空间拷贝到u区;
4. 保存当前进程的上下文;
5. 调用内核中的系统调用代码;
6. 返回用户进程。
这就决定了实现内核函数时,并不能直接使用引用型参数——例如:指针型参数或者引用型参数(想一想为什么?),而须使用copy_from_user和copy_to_user这两个内核API(也可以使用memcpy_fromfs和memcpy_tofs)实现参数的读入/写回。
如何在用户态调用内核系统函数呢?原则上只有使用致限引发0x80中断一种方法。例如:当你调在用一个具有两个参数的你同调用sys_xxx_call时,若要传入参数param1和param2并且将返回值存放在res中,就要采用如下方法:
long res;
asm volatile (“int $0x80” \
:”=a”(res) \
:”0” (__NR_sys_xxx_call), “b”((long)(param1)),\
“c”((long)(param2)));
return res;
另外,系统定义了syscall0~syscall5这六个宏,分别封装对就有0个到5个参数的内核函数的调用。
最后要注意的是,在编译使用内和函数的C文件时要通知编译器把这些代码当作内和代码而不是普通的用户代码来编译,这可以通过向编译器传递__KERNEL__标志来实现,使用-W编译选项来向装载程序传递all。如下所示:
gcc –Wall –D__KERNEL__ xxx.c
当然,如果你采用重新编译内核的方法,并不需要这样做——只要修改内核源码目录的Makefile,并将你的文件添加至O_OBJS列表中就可以了。因为Makefile的FLAGS标志已经包含上述编译选项了。
4.2 问题陈述
部分A
添加一个新的内核系统调用,具体完成某个你希望实现的功能。
部分B
重新编译内核,使你的系统调用可用。
部分C
编写一个用户态的程序,验证你增加的系统调用。
4.3实验步骤
部分A: 添加新的内核系统调用
1)在最新版本的 Linux 内核源代码中,系统调用入口表通常不再在 entry_64.S文件中。系统调用入口表的位置在不同的内核版本中可能会有所变化。通常,它会在 arch/x86/entry/syscalls/syscall_64.tbl 文件中定义。
2)下载一个新内核并解压,如下图所示
打开终端,使用cd linux-6.0.1进入内核源目录,使用命令find . -name syscalls.h查找系统调用入口表,找到以后使用下述命令打开系统调用入口表,即vim ./arch/x86/include/asm/syscalls.h,打开该文件之后,添加asmlinkage long sys_helloworld(void);声明,如下图所示:
3)添加系统调用id:文件为 syscall_64.tbl它包含系统调用号和系统调用函数之间的映射
先打开需要修改的文件:vim ./arch/x86/entry/syscalls/syscall_64.tbl
在文件中找到一个空的系统调用入口,通常为sys_ni_syscall(通常是156号)
使用命令 /sys_ni_syscall 搜索,找到以后回车,然后 点击 i 即可修改
修改内容,如图所示:
这表示将系统调用号 156映射到名为 sys_helloworld 的系统调用函数。
然后输入:wq保存并退出。
4)编写新的系统调用函数,先打开sys.c文件,先使用find . -name sys.c查找系统调用文件,找到之后,如下图所示:
使用命令vim kernel/sys.c,在文件末尾添加你的新系统调用函数 sys_helloworld的定义,代码如下:
SYSCALL_DEFINE0(helloworld) {
printk("你这猴子真令我欢喜!!!,系统调用成功!!!\n");
return 24;}
添加之后,如下图所示:
修改完之后,输入:wq保存并退出
5)在终端中编译新内核,编译之前使用make clean指令清理一下旧文件,重新配置.config文件 make menuconfig
make menuconfig 时将general setup -> localversion 修改成新的名称,如 “NewKernel”
修改完之后,如下图所示:
部分B: 重新编译和安装内核
1)然后使用make -j4开始正式编译,编译完之后,没有报错,如下图所示:
2)执行sudo make modules_install安装模块,安装完之后,如下图所示:
3)执行sudo make install安装内核,安装完之后,如图所示:
4)然后重启机器,弹出内核选项,默认选择第一个即可,如下图所示:
然后就是等待重启即可。
部分C: 编写一个用户态的程序
1)新建一个hello.c文件,输入一下代码:
#include <stdio.h>
#include<linux/kernel.h>
#include<sys/syscall.h>
#include<unistd.h>int main()
{long int a = syscall(156);printf("获得的系统调用的返回值是: %ld\n", a);return 0;
}
输入完之后,如下图所示:
然后输入:wq,保存并退出
2)使用gcc -o hello hello.c开始编译hello.c文件,编译完之后,使用./hello执行此文件,执行完如下图所示:
返回的值和当初写在系统调用表里的返回值是一样的,说明系统调用成功了
3)输入dmesg指令,输入完之后,如下图所示:
显示的是自己写的打印语句,说明系统调用是成功的。
4.4 留下一个干净的环境
修改内核目录中的文件内容。但在完成测试之后,应该记得把环境恢复到最初的状态。如果你在一个环境中工作,但是似乎有什么不对劲,那么可能已经有人污染了这个环境。如果试图在已经污染过环境中工作,那么你的解决方案恐怕就有问题了。因此在能够完成自己的工作之前,一定将工作恢复到原始状态。
4.5实验总结
-
在本次实验中,我成功地实现了 Linux 系统调用的添加,并通过重新编译内核和编写用户态程序来实现对用户空间的读/写功能。在这个过程中,我遇到了一些问题和挑战。
-
在添加新系统调用时,需要深入理解 Linux 内核的机制和源代码,这对初学者来说具有一定的难度。此外,重新编译内核时,需要安装并配置相应的编译工具和环境,这要求对操作系统有较为深入的了解。在编写用户态程序时,熟悉 Linux 的系统调用接口以及如何与内核交互也是必须的,这需要额外的学习和理解。
-
在实验过程中,安全性原则至关重要,必须避免对操作系统内核的破坏或数据的篡改。在修改和编译内核时,务必备份原有的内核文件,以防止因操作失误导致系统无法启动。在进行用户态程序开发时,也要考虑异常情况的处理和错误信息的输出,以确保程序的健壮性和可靠性。
-
我需要进一步理解和掌握操作系统的内核机制和体系结构,提升对内核源代码的理解能力。同时,熟悉 Linux 系统调用的接口和使用方法,以便灵活进行系统调用操作。此外,我还需学会使用编译工具和环境,能够独立完成内核的编译和安装过程。提高代码编写能力和调试技巧,增强对程序异常情况的处理能力也是我需要努力的方向。最后,培养独立思考和解决实际问题的能力,寻找更多实践机会,以不断提升自己的技术水平。