基于内核模块的测试代码编写

简介:

1. 背景 

压缩卡驱动提供给文件系统KAPI,供文件系统对文件数据进行压缩和解压。在测试中,最初采用的方法是通过文件系统提供的系统调用,利用文件系统在处理系统调用时,会调用到驱动的KAPI,来完成对压缩卡KAPI及其更下层(包含硬件)正确性的测试。考虑到这种方法,可能会由于文件系统对KAPI的具体使用方式而屏蔽一些问题的发现,因此展开了对KAPI的直接测试。由于KAPI是内核态的接口,无法在用户态直接调用,因此要最终完成对KAPI的更直接测试,需要借助编写内核模块(Kernel Module),来实现用户进程对KAPI的访问;此外,还要解决用户态和内核态两者的交互。下图中表示了引入基于内核模块的测试方法,可以使我们的测试程序在调用关系上更加接近被测模块,从而在测试效率和覆盖性上得到改进。

在本文中,将主要介绍实现基于内核模块的测试的相关知识与代码编写方式。 

 





2. 用户态和内核态 

操作系统中,虚拟内存被分为内核空间和用户空间。内核空间被用于内核,内核扩展功能和一些设备驱动的运行;用户空间是用户进程在用户态下运行时所使用。在 linux系统中,内核拥有地址为3G到4G的内存空间,并且它是被共享的,而用户进程拥有0到3G的用户空间,每个用户进程拥有独立的空间。 

 




CPU根据PSW寄存器中的模式bit,可以工作在用户态(Ring 3)或内核态(Ring 0),用户态能执行有限的非特权的CPU指令。在内核态可以执行所有指令。工作在用户态的进程无法直接访问硬件和内核的内存空间,这是出于运行安全的考虑。当然,操作系统也会也提供系统调用或中断等方法,让用户进程可以切换到内核态,以访问内核空间或硬件。系统调用或中断,可以看作是有限度的开放给用户进程对内核和硬件的访问,从而保证了系统安全。 

在驱动项目中,驱动程序工作在内核空间中,它直接提供KAPI给内核,这样我们就无法通过用户进程完成对该KAPI的测试。当然,通过执行内核提供的系统调用,当发生磁盘读写时可以间接的调用到该KAPI,但是内核对其有限或具体的调用方式往往屏蔽了底层驱动的部分缺陷,这种测试方法也带来发现问题后追查定位的困难。要改变这些不足,最直接的思路就是跨越对内核特殊逻辑间接调用的依赖,通过直接对KAPI进行调用的方法完成测试。 

为了实现对工作在内核态的KAPI的访问,必须向内核置入内核态的代码以发起调用,这将通过kernel module来完成,会在第三部分具体介绍。而为了实现对测试数据输入,执行发起,结果输出采集等的灵活控制,还必须实现用户态和内核态的交互,具体方法将在第四部分介绍。 


3. Kernel Module 

要达成对KAPI的直接调用和测试,需要添加工作在内核态的代码实现对该函数的访问,这里我们借助kernel module来完成。kernel module提供了不需要重新编译kernel image,就向内核引入新代码和逻辑的支持。Kernel Module通常是以ko为扩展名的文件,通过lsmod命令可以查看已经加载的Kernel Module,通过insmod/rmmod命令完成指定的的模块的加载和卸载。Kernel Module最基本的编程模式是编程者通过宏定义module_init和module_exit,指定在模块加载和卸载时进行的初始化或清理等工作。一个基本的hello world程序如下。向init中就可以加入对KAPI进行调用的函数,当模块被insmod时就会在内核态被执行。但是这与灵活的完成各种测试任务还有差距,为了能在用户态完成测试数据的准备和输入,测试结果的获取和对执行的控制,还需要引入用户态和内核态的交互。关于Kernel Module更详细的编译方式和编程模式介绍,可以参考 http://www.tldp.org/LDP/lkmpg/2.6/html/ 


#include <linux/module.h> /* Needed by all modules */
#include <linux/kernel.h> /* Needed for KERN_INFO */
#include <linux/init.h> /* Needed for the macros */

static int __init hello_init(void){
printk(KERN_INFO "Hello, world 2\n");
return 0;
}
static void __exit hello_exit(void){
printk(KERN_INFO "Goodbye, world 2\n");
}

module_init(hello_init);
module_exit(hello_exit);

4. 用户空间和内核空间的交互 

在解决了在内核空间置入可运行代码后,需要解决的是用户空间和内核空间的交互。具体来说,需要达到以下三个功能:用户空间的程序向内核空间下的程序控制,用户空间到内核空间的数据传递,内核空间到用户空间的数据传递。以下小节,都旨在利用系统提供给我们的各种接口,实现以上三个目标中的一个或几个。 


4.1 printk 

printk是内核用来记录系统运行日志的方法。对于用户,可以通过dmesg命令查看近期的系统日志信息,或者直接访问/var/log/kernel 查看内核输出的所有历史log。在kernel module中调用printk是最简单的传递信息到用户空间的方法。printk函数的使用方法和用户态下的printf类似,区别是可以通过 KERN_INFO等宏输出从0-7的指定级别的log信息。常见的使用方式如下: 

char myname[] = "chinacodec\n";
printk(KERN_INFO "Hello, world %s!\n", myname);


4.2 伪字符设备 

在linux中,用户对设备的操作往往被抽象为对文件的操作。利用这一特性,可以通过注册和实现伪字符设备到内核,来实现用户进程和内核空间的交互。当在用户空间执行对该伪设备的open/read/write/ioctl/mmap/release等操作时,这些被复用的系统调用就会使进程从用户态进入到内核态,从而在内核中完成事先注册的操作,当然可以包括对KAPI的调用等。 

具体方法是,首先,在kernel module中通过register_chrdev注册一种伪字符设备到内核,参数包括:设备的major号,需要和系统已有设备不冲突;设备的名称name;文件操作函数集fops。register_chrdev的定义如下: 


int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);

其中,结构file_operations中包含一系列的函数指针,对应每种系统调用(包含read/write/open等),使用中,可以用如下的方式赋值(而不必为每一个元素都赋值),那些未显式赋值的元素被赋值为null。 


static struct file_operations fops = {
.read = device_read,
.write = device_write,
.ioctl = device_ioctl,
.open = device_open,
.release = device_release
};

register_chrdev一般在kernel module中的init函数中执行,当insmod这个module时,设备就被注册到内核中。然后,用户就可以通过mknod命令可以创建对应的字符设备文件。然后通过open/write/read/ioctl/mmap/release等系统调用以访问设备文件的方式,访问该设备。每种调用都会执行到在注册设备时注册的对应的文件操作函数。此外,对应于register_chrdev,从内核卸载该伪设备驱动的函数为 unregister_chrdev。 

以ioctl为例,当以上面的file_operations注册了伪字符设备后,当用户对伪设备文件执行ioctl后,调用会进入内核态,执行 device_ioctl函数。如果我们在自定义的device_ioctl函数中去调用KAPI,就实现了用户进程对KAPI的访问。 


4.3 普通文件读写 

内核态中,可以完成对用户文件系统任意文件的访问。因此,可以在内核态将要输出的信息写入文件,写入后用户态程序直接读取文件就可以完成从内核空间向用户空间的数据传递。但是在内核态下,对文件进行访问的调用函数和用户态下的系统调用有所区别。通常的使用方法是通过filp_open打开文件,然后利用获得的文件指针得到文件操作函数,以读取和写入文件,基本代码如下: 


tmp _filp = filp_open(dst_file_name, O_RDWR | O_CREAT, 00);

tmp _copied = dst_filp->f_op->write(tmp_filep, buffer, size, offset);

tmp _len = dst_filp->f_op->read(tmp _filep, buffer, size, offset);


因为是在内核中对文件操作,所以为了通过系统调用中对缓冲区内存地址参数的检查,需要修改检查允许范围。方法是在read或write操作前通过set_fs扩大允许空间,操作后再通过setfs恢复到此前的允许范围,具体方法是: 


orig_fs = get_fs();
set_fs(KERNEL_DS);
//write or read
set_fs(orig_fs);


4.4 Proc文件系统 

proc文件系统,是当前内核或内核模块,和用户交互的主要方式,它通过将虚拟的文件系统挂载在/proc下,利用虚拟文件读写在用户和内核态间传递信息。通过内核模块,可以向/proc下注册新的文件,指定用户读写该文件时的回调函数;这样,当用户读写该文件时,工作在内核态的回调函数就可以执行信息交互的有关工作。 

向内核中注册/proc下文件的调用是create_proc_entry,创建中需要指定文件名,访问权限和父节点名,返回为指向 proc_dir_entry结构的指针。通过该返回指针,可以进一步修改文件的用户id,组id,绑定的内核数据等;但最为关键的是可以指定用户读或写该文件时,在内核中被执行的回调函数。下面是一个向proc文件系统中注册新文件的示例: 

static int __init proc_module_init(void){
entry = create_proc_entry("astring", 0644, myprocroot);
if (entry) {
entry->data = &string_var;
entry->read_proc = &string_read_proc;
entry->write_proc = &string_write_proc; 
}
return 0
}
static void __exit procfs_exam_exit(void){
remove_proc_entry("astring", myprocroot);
remove_proc_entry("myproctest", NULL);
}
//read proc
int string_read_proc(char *page, char **start, off_t off,
int count, int *eof, void *data){
count = sprintf(page, "%s", (char *)data);
return count;
}
//write proc
int string_write_proc(struct file *file, const char __user *buffer,
unsigned long count, void *data){
if (count > STR_MAX_SIZE) {
count = 255;
}
copy_from_user(data, buffer, count);
return count;
}

4.5 af_netlink 

netlink是一种特殊的socket,用于用户态与内核态的双向通讯。在实现用户和内核交互的各种方式中,netlink的主要特点得意于它继承了 socket的一些基本特性,包括异步通讯,多播,双向性,不需要额外的文件。在用户态中,netlink的使用与标准的socket API相同,在内核态,则需要使用专门的API。下面介绍具体的使用方法: 

在用户态中,首先通过要创建socket,其中指定domain必须为AF_NETLINK,协议为通常SOCK_RAW,协议类型为NETLINK_GENERIC或其它自定义类型 

sd = socket(AF_NETLINK, SOCK_RAW,NETLINK_GENERIC);

然后通过bind绑定源端的地址,地址结构定义如下,其中nl_family为AF_NETLINK,nl_pad 目前无用填充0,nl_pid为进程id,若为0代表内核;nl_groups用于组播时的组号。 


struct sockaddr_nl { 
sa_family_t nl_family; 
unsigned short nl_pad; 
__u32 nl_pid; 
__u32 nl_groups; 
} saddr; 

bind(sd, (struct sockaddr*)&saddr, sizeof(saddr));

通过sendmsg可以发送消息msg到指定的地址。 

ret = sendmsg(sd, &msg, 0); 

在msg的所有元素中,msg_name需要指向一个sockaddr_nl 结构的首地址,用来表示发送的目的端的地址,如果是发送到内核,其中的nl_pid字段置为0;msg_iov是要发送消息集合的向量,向量中的每一项代表一条消息。每一项指向数据的首部为一个nlmsghdr结构,其字段定义了该条消息长度,消息类型,序号,发送者进程id等;随后跟随的是消息的主体数据部分。 当要接收消息时,通过recvmsg可以获得类似的消息向量,从而获得数据及发送者等有关信息。 

在内核态,通过netlink_kernel_create可以在内核中新建socket结构并注册接收到消息的回调函数input,其原型为: 

struct sock * netlink_kernel_create(int unit, void (*input)(struct sock *sk, int len)); 

当接收到消息时,回调函数input中的sk指针就指向了刚刚创建的socket在内核中的结构,通过对该结构的访问,可以获得要接收的数据。一种基本的input实现如下: 


void input (struct sock *sk, int len) { 
struct sk_buff *skb; 
struct nlmsghdr *nlh = NULL; 
u8 *data = NULL; 
while ((skb = skb_dequeue(&sk->receive_queue)) != NULL) { 
nlh = (struct nlmsghdr *)skb->data; 
data = NLMSG_DATA(nlh); 



此外,sock_release是在内核中释放socket的方法;而通过netlink_unicast和netlink_broadcast可以在内核中令netlink socket发送数据,具体的方法可以参考 http://blog.chinaunix.net/u/19940/showart_144827.html 


4.6 其它方法 

以上所介绍的方法具有一个共同特点,它们不需要较高版本的内核支持,添加新的功能不需要重新编译内核或替代内核中的原有功能。当然,还有一些其它方法,可能会需要内核版本或重新编译内核等条件的支持,但同样能达到用户态和内核态交互这一目标,比如修改或添加新的系统调用,或利用sysfs,relayfs 等特殊的虚拟文件系统。这里不再一一介绍。 


5 总结 

本文主要解决了如何对内核态的函数接口进行测试,其中包括置入内核态代码以调用到内核API接口的方法,使测试程序从用户态进入内核态的方法,以及如何实现用户空间和内核空间的交互等。目前,已经基于上述方法实现了对压缩卡KAPI的直接测试,测试代码已经应用到压缩卡基本功能(压缩和解压)的测试,异常测试和压力测试。未来将会视需求实现或完善出更通用的,使用方式也更为灵活的内核态接口的测试工具。 

(全文完)

 











本文转自百度技术51CTO博客,原文链接:http://blog.51cto.com/baidutech/743543,如需转载请自行联系原作者

相关文章
|
3月前
|
数据采集 机器学习/深度学习 大数据
行为检测代码(一):超详细介绍C3D架构训练+测试步骤
这篇文章详细介绍了C3D架构在行为检测领域的应用,包括训练和测试步骤,使用UCF101数据集进行演示。
114 1
行为检测代码(一):超详细介绍C3D架构训练+测试步骤
|
7天前
|
安全 Linux 测试技术
Intel Linux 内核测试套件-LKVS介绍 | 龙蜥大讲堂104期
《Intel Linux内核测试套件-LKVS介绍》(龙蜥大讲堂104期)主要介绍了LKVS的定义、使用方法、测试范围、典型案例及其优势。LKVS是轻量级、低耦合且高代码覆盖率的测试工具,涵盖20多个硬件和内核属性,已开源并集成到多个社区CICD系统中。课程详细讲解了如何使用LKVS进行CPU、电源管理和安全特性(如TDX、CET)的测试,并展示了其在实际应用中的价值。
|
12天前
|
数据采集 算法 测试技术
【硬件测试】基于FPGA的16psk调制解调系统开发与硬件片内测试,包含信道模块,误码统计模块,可设置SNR
本文介绍了基于FPGA的16PSK调制解调系统的硬件测试版本。系统在原有仿真基础上增加了ILA在线数据采集和VIO在线SNR设置模块,支持不同信噪比下的性能测试。16PSK通过改变载波相位传输4比特信息,广泛应用于高速数据传输。硬件测试操作详见配套视频。开发板使用及移植方法也一并提供。
26 6
|
18天前
|
数据采集 算法 数据安全/隐私保护
【硬件测试】基于FPGA的8PSK调制解调系统开发与硬件片内测试,包含信道模块,误码统计模块,可设置SNR
本文基于FPGA实现8PSK调制解调系统,包含高斯信道、误码率统计、ILA数据采集和VIO在线SNR设置模块。通过硬件测试和Matlab仿真,展示了不同SNR下的星座图。8PSK调制通过改变载波相位传递信息,具有高频谱效率和抗干扰能力。开发板使用及程序移植方法详见配套视频和文档。
33 7
|
25天前
|
数据采集 算法 测试技术
【硬件测试】基于FPGA的QPSK调制解调系统开发与硬件片内测试,包含信道模块,误码统计模块,可设置SNR
本文介绍了基于FPGA的QPSK调制解调系统的硬件实现与仿真效果。系统包含测试平台(testbench)、高斯信道模块、误码率统计模块,支持不同SNR设置,并增加了ILA在线数据采集和VIO在线SNR设置功能。通过硬件测试验证了系统在不同信噪比下的性能,提供了详细的模块原理及Verilog代码示例。开发板使用说明和移植方法也一并给出,确保用户能顺利在不同平台上复现该系统。
64 15
|
25天前
|
算法 Java 测试技术
使用 BenchmarkDotNet 对 .NET 代码进行性能基准测试
使用 BenchmarkDotNet 对 .NET 代码进行性能基准测试
56 13
|
1月前
|
数据采集 算法 数据安全/隐私保护
【硬件测试】基于FPGA的2FSK调制解调系统开发与硬件片内测试,包含信道模块,误码统计模块,可设置SNR
本文介绍了基于FPGA的2FSK调制解调系统,包含高斯信道、误码率统计模块及testbench。系统增加了ILA在线数据采集和VIO在线SNR设置模块,支持不同SNR下的硬件测试,并提供操作视频指导。理论部分涵盖频移键控(FSK)原理,包括相位连续与不连续FSK信号的特点及功率谱密度特性。Verilog代码实现了FSK调制解调的核心功能,支持在不同开发板上移植。硬件测试结果展示了不同SNR下的性能表现。
71 6
|
2月前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
83 1
|
3月前
|
安全 Java 数据库
shiro学习一:了解shiro,学习执行shiro的流程。使用springboot的测试模块学习shiro单应用(demo 6个)
这篇文章是关于Apache Shiro权限管理框架的详细学习指南,涵盖了Shiro的基本概念、认证与授权流程,并通过Spring Boot测试模块演示了Shiro在单应用环境下的使用,包括与IniRealm、JdbcRealm的集成以及自定义Realm的实现。
66 3
shiro学习一:了解shiro,学习执行shiro的流程。使用springboot的测试模块学习shiro单应用(demo 6个)
|
3月前
|
计算机视觉
目标检测笔记(二):测试YOLOv5各模块的推理速度
这篇文章是关于如何测试YOLOv5中不同模块(如SPP和SPPF)的推理速度,并通过代码示例展示了如何进行性能分析。
174 3

热门文章

最新文章