野仙生活网

野仙生活网

linux内核编程:实现自己的系统调用

民生 0

前言

本实验是在imx6ull芯片,4.1内核版本内核做的,内核是在不断更新的,所以如果是其他按本内核,且遇到调试不通的地方,欢迎留言讨论,我这也是照着书上一步一步做的,文字描述看着比较有水平的,就是书上写的,看着像大白话的,就是我自己拼凑的。

root@hehe:~# cat /proc/version
Linux version 4.1.15 (lkmao@ubuntu) (gcc version 5.4.0 20160609 (Ubuntu/Linaro 5.4.0-6ubuntu1~16.04.9) ) #2 SMP PREEMPT Sun Dec 18 16:12:04 CST 2022
root@hehe:~#

一个系统调用的实现并不需要去关心如何从用户空间转换到内核空间,以及系统调用处理程序如何去执行,你需要做的只是遵循几个固定的步骤。

一 如何实现一个新的系统调用 

为Linux添加新的系统调用是件相对容易的事情,主要包括有4个步骤:

  • 编写系统调用服务例程;
  • 添加系统调用号;
  • 修改系统调用表;
  • 重新编译内核并测试新添加的系统调用。

下面以一个十分珍贵的hello_lkmao系统调用为例,来演示上述几个步骤。

(1)编写系统调用服务例程。 

系统调用除了都具有“sys_”前缀之外,所有的系统调用服务例程命名与定义还必须遵守其他的一些规则。

首先,函数定义中必须添加asmlinkage标记,通知编译器仅从堆栈中获取该函数的参数。

其次,必须返回一个long类型的返回值表示成功或错误,通常返回0表示成功,返回负值表示错误。

当然,getpid系统调用非常简单,不可能会失败,通过命令“man 2 getpid”可以查看它的手册,里面也明确指出了这一点。每个系统调用的系统调用号、命名以及操作目的都是固定的,但内核如何去实现并没有明确规定,不同版本、不同架构的内核实现都有可能会有所变化。

我们自己的hello_lkmao系统调用的服务例程实现为:

SYSCALL_DEFINE0(hello_lkmao)
{printk("%s:%s:%d -- sys_hello_lkmao is called\n",__FILE__,__func__,__LINE__);return 0;
}

        我把这个函数放在了kernel/sys.c文件中,这个函数和其他系统调用不一样啊,先按照书上的步骤继续实验。 

通常,应该为新的系统调用服务例程创建一个新的文件进行存放,但也可以将其定义在其他文件之中并加上注释做必要说明。同时,还要在include/linux/syscalls.h文件中添加原型声明:

asmlinkage long sys_hello_lkmao(void);

放到尾巴上: 

sys_hello_lkmao函数非常简单,仅仅打印一条语句,并没有使用任何参数。如果我们希望hello_lkmao系统调用不仅能打印“hello!”欢迎信息,还能够打印出我们传递过去的名称,或者其他的一些描述信息,再添加一个sys_hello_lkmao2,函数可以实现为:也放在kernel/sys.c文件中

SYSCALL_DEFINE1(hello_lkmao2,const char __user *,_name)
{char *name;long ret;name = strndup_user(_name, PAGE_SIZE);if (IS_ERR(name)) {ret = PTR_ERR(name);goto error;}printk("%s:%s:%d -- sys_hello_lkmao2 is called\n%s\n",__FILE__,__func__,__LINE__,name);return 0;
error:return ret;
}

sys_hello_lkmao2函数使用了一个参数,在这种有参数传递发生的情况下,编写系统调用服务例程时必须仔细检查所有的参数是否合法有效。因为系统调用在内核空间执行,如果不加限制任由用户应用传递输入进入内核,则系统的安全与稳定将受到影响。

参数检查中最重要的一项就是检查用户应用提供的用户空间指针是否有效。比如上述sys_hello_lkmao2函数参数为char类型指针,并且使用了__user标记进行修饰。__user标记表示所修饰的指针为用户空间指针,不能在内核空间直接引用,原因主要如下。

● 用户空间指针在内核空间可能是无效的。

● 用户空间的内存是分页的,可能引起页错误。

● 如果直接引用能够成功,就相当于用户空间可以直接访问内核空间,产生安全问题。

因此,为了能够完成必须的检查,以及在用户空间和内核空间之间安全地传送数据,就需要使用内核提供的函数。比如在sys_hello_lkmao2函数的name = strndup_user(_name, PAGE_SIZE)行,就使用了内核提供的strndup_user函数(在mm/util.c文件中定义)从用户空间复制字符串name的内容。

在include/linux/syscalls.h文件中添加原型声明: 

asmlinkage long sys_bpf(int cmd, union bpf_attr *attr, unsigned int size);asmlinkage long sys_execveat(int dfd, const char __user *filename,const char __user *const __user *argv,const char __user *const __user *envp, int flags);
/*下面两个是我加的*/
asmlinkage long sys_hello_lkmao(void);
asmlinkage long sys_hello_lkmao2(const char __user *str);#endif

 (2)添加系统调用号。

每个系统调用都会拥有一个独一无二的系统调用号,所以接下来需要更新arch\arm\include\uapi\asm\Unistd.h 文件,为sys_hello_lkmao和sys_hello_lkmao2系统调用添加一个系统调用号。

#define __NR_memfd_create		(__NR_SYSCALL_BASE+385)
#define __NR_bpf			(__NR_SYSCALL_BASE+386)    /*这是啥?????哈利路亚*/
#define __NR_execveat			(__NR_SYSCALL_BASE+387)
#define __NR__hello_lkmao			(__NR_SYSCALL_BASE+388) /*我们的无参的*/
#define __NR__hello_lkmao2			(__NR_SYSCALL_BASE+389) /*我们的有参的*/
#define __NR_hello_lkmao_bak1			(__NR_SYSCALL_BASE+390)
#define __NR_hello_lkmao_bak2			(__NR_SYSCALL_BASE+391)

__NR_hello_lkmao_bak1和__NR_hello_lkmao_bak1是为了让数量满足后两位为0,如果不为0会报错 :实测证明,不多加两条确实会报错。依据就是下面这条:

.equ syscalls_padding, ((NR_syscalls + 3) & ~3) - NR_syscalls

(3)修改系统调用表。 

为了让系统调用处理程序system_call函数能够找到hello系统调用,我们还需要修改系统调用表sys_call_table,放入服务例程sys_hello_lkmao和sys_hello_lkmao2函数的地址。修改文件arch/arm/kernel/calls.S

/* 385 */	CALL(sys_memfd_create)CALL(sys_bpf)CALL(sys_execveat)
/*下面两个也是我加的*/CALL(sys_hello_lkmao)CALL(sys_hello_lkmao2)
/* 390 */	CALL(sys_ni_syscall)CALL(sys_ni_syscall)
#ifndef syscalls_counted
.equ syscalls_padding, ((NR_syscalls + 3) & ~3) - NR_syscalls
#define syscalls_counted
#endif
.rept syscalls_paddingCALL(sys_ni_syscall)
.endr

CALL宏是什么,它的定义在arch/arm/kernel/entry-common.S中:

#undef CALL
#define CALL(x) .long x

新的系统调用hello的服务例程被添加到了sys_call_table的末尾。我们可以注意到,sys_call_table每隔5个表项就会有一个注释,表明该项的系统调用号,这个好习惯可以在查找系统调用对应的系统调用号时提供方便。

(4)修改系统调用总数

位于arch/arm/include/asm/unistd.h 文件中,注意这个unistd.h和上面的unistd.h不是一个文件。

/** This may need to be greater than __NR_last_syscall+1 in order to* account for the padding in the syscall table*/
#define __NR_syscalls  (388)

如果不修改这个宏,会报错,报错信息还是很详细的,如果不知道这个宏在哪里,就先编译,看报错信息

arch/arm/kernel/entry-common.S: Assembler messages:
arch/arm/kernel/entry-common.S:108: 错误: __NR_syscalls is not equal to the size of the syscall table
scripts/Makefile.build:294: recipe for target 'arch/arm/kernel/entry-common.o' failed
make[1]: *** [arch/arm/kernel/entry-common.o] Error 1
Makefile:947: recipe for target 'arch/arm/kernel' failed
make: *** [arch/arm/kernel] Error 2

我们新加了两个系统调用,将这个宏改为390,为什么是390呢?因为是388+2呀。(#^.^#)

/** This may need to be greater than __NR_last_syscall+1 in order to* account for the padding in the syscall table*/
#define __NR_syscalls  (388)

修改文件Sys_ni.c (kernel)   

/* access BPF programs and maps */
cond_syscall(sys_bpf);/* execveat */
cond_syscall(sys_execveat);
/*下面两个是我加的*/
cond_syscall(sys_hello_lkmao);
cond_syscall(sys_hello_lkmao2);

修改文件Unistd.h (include\uapi\asm-generic) 

#define __NR_bpf 280
__SYSCALL(__NR_bpf, sys_bpf)
#define __NR_execveat 281
__SC_COMP(__NR_execveat, sys_execveat, compat_sys_execveat)
//下面8行是新加的
#define __NR_hellolkmao 282
__SYSCALL(__NR_hellolkmao, sys_hello_lkmao)#define __NR_hellolkmao2 283
__SYSCALL(__NR_hellolkmao2, sys_hello_lkmao)#define __NR_hello_lkmao_bak1 284
__SYSCALL(__NR_hello_lkmao_bak1, sys_ni_syscall)#define __NR_hello_lkmao_bak2 285
__SYSCALL(__NR_hello_lkmao_bak2, sys_ni_syscall)#undef __NR_syscalls
#define __NR_syscalls 286    //在原来的值的基础上加4

 

(5)重新编译内核并测试。

(⊙o⊙)…,先整理先,我们都改了那几个文件:

  • 系统调用实现:kernel/sys.c
  • 系统调用声明:include/linux/syscalls.h
  • 注册系统调用表:arch/arm/kernel/calls.S
  • 添加系统调用号:arch\arm\include\uapi\asm\unistd.h 
  • 修改系统调用总数:arch/arm/include/asm/unistd.h 
  • 修改文件unistd.h  (include\uapi\asm-generic) 

为了能够使用新添加的系统调用,需要重新编译内核,并使用新内核重新引导系统。然后,我们还需要编写测试程序对新的系统调用进行测试。针对hello系统调用的测试程序如下:

#include 
#include 
#include #define __NR_hello 388
#define __NR_hello2 389
int main(int argc, char *argv[])
{
syscall(__NR_hello);
syscall(__NR_hello2,"gulugulu");
return 0;
}

然后使用arm-linux-gnueabihf-gcc编译:

lkmao@ubuntu:~$ arm-linux-gnueabihf-gcc main.c -o syscall
lkmao@ubuntu:~$ scp syscall root@192.168.0.33:/home/root
lkmao@ubuntu:~$ 

 执行:

root@hehe:~# ./syscall
[   28.171310] kernel/sys.c:sys_hello_lkmao:837 -- sys_hello_lkmao is called
[   28.178313] kernel/sys.c:SYSC_hello_lkmao2:851 -- sys_hello_lkmao2 is called
[   28.178313] gulugulu
root@hehe:~#

由执行结果可见,系统调用添加成功。

二 什么时候需要添加新的系统调用

虽说添加一个新的系统调用非常简单,但这并不意味着用户有必要这么做。添加系统调用需要修改内核源代码、重新编译内核,如果更进一步希望自己添加的系统调用能够得到广泛的应用,就需要得到官方的认可并分配一个固定的系统调用号,还需要将该系统调用在每个需要支持的体系结构上实现。因此我们最好使用其他替代方法和内核交换信息,如下所示。

● 使用设备驱动程序。创建一个设备节点,通过read和write函数进行读写访问,使用ioctl函数进行设置操作和获取特定信息。这种方法最大的好处在于可以模块式加载卸载,避免了编译内核等过程,而且调用接口固定,容易操作。

● 使用proc虚拟文件系统。利用proc接口获取系统运行信息和修订系统状态是一种很常见的手段,比如读取/proc/cpuinfo可以获得当前系统的CPU信息,通过设备驱动提供的proc接口还可以设置硬件寄存器。

● sysfs文件系统。sysfs文件系统在2.6内核被引入,是一个类似于proc文件系统的特殊文件系统,用于对系统的设备进行管理,它把实际连接到系统上的设备和总线组织成层次结构,并向用户提供详细的内核数据结构信息,用户可以利用这些信息以实现和内核的交互。

小结