Linux驱动编程 step-by-step (六)

时间:2022-08-28 16:33:40

说点上节没有讲完的话题

用户地址检测 简单模块调试 以及一些杂项

检测用户空间地址的有效性

上一节中提到在read write时候要检测用户空间传递的参数地址是否是有效地址,有的内核函数会自行检测,但是在调用轻量级的内核函数时候,就可能不去检测用户空间的地址是否有效,如果此时用户无传递一个无效地址,而内核函数去操作了它,这时棘手的问题出现了,轻则内核oops 关机重启就OK了,在特别严重的情况下,可能你的系统就崩溃了(我又遇到过),所以,我们在驱动程序中操作用户空间地址时候要小心加小心。如果电脑配置可以就在虚拟机中玩, 或者在开发板上试,当然这边的测试代码我都有试过,不至于让你系统崩溃的。
如何检测呢?
调用一个access_ok函数去检测
#define access_ok(type,addr,size)
type 标识读写操作VERIFY_READ表示地址可读,VERIFY_WRITE表示地址可写
addr 用户传入的地址
size 读写的长度
此代码在有内存管理的芯片与无内存管理之间有区别

我们 看一段内核代码 (path : arch/arm/include/asm/uaccess.h)

#define access_ok(type,addr,size)	(__range_ok(addr,size) == 0)
#ifdef CONFIG_MMU
...
#define __range_ok(addr,size) ({ \
	unsigned long flag, roksum; \
	__chk_user_ptr(addr);	\
	__asm__("adds %1, %2, %3; sbcccs %1, %1, %0; movcc %0, #0" \
		: "=&r" (flag), "=&r" (roksum) \
		: "r" (addr), "Ir" (size), "0" (current_thread_info()->addr_limit) \
		: "cc"); \
	flag; })
#else
...
#define __range_ok(addr,size)	(0)

即在有内存管理并配置了内存管理的芯片内调用次函数会执行检测操作,而在没有配置内存管理的芯片中此函数总是返回真,而做驱动的不应该做这些假设,所以传入的参数在有必要的情况下还是要自行检测再看看copy_to_user函数

static inline unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned long n)
{
	if (access_ok(VERIFY_WRITE, to, n))
		n = __copy_to_user(to, from, n);
	return n;
}
可以看到他在函数内部做了这种检测

而当我们调用

__copy_to_user
__copy_from_user
get_user
__get_user
put_user
__put_user
时都需要检测用户地址是否可用

简单模块调试技术

为什么要加简单呢? 因为这边只介绍了用打印来调试程序。
看了LDD3上边介绍的很多调试技术  查询调试 观察调试之类
我觉得 打印调试来的最简单最直接 虽然他有一些限制
1、大量的使用printk会使系统变慢
2、没次打印一行都会引起磁盘操作
...
在printk中有7中 消息的选项 表示着不同的消息等级
KERN_GMERG<0> 用于紧急消息, 常常是那些崩溃前的消息.
KERN_ALERT<1> 需要立刻动作的情形.
KERN_CRIT<2> 严重情况, 常常与严重的硬件或者软件失效有关.
KERN_ERR<3> 用来报告错误情况; 设备驱动常常使用 来报告硬件故障.
KERN_WARNING<4> 有问题的情况的警告, 这些情况自己不会引起系统的严重问题
KERN_NOTICE<5> 正常情况, 但是仍然值得注意. 在这个级别一些安全相关的情况会报告.
KERN_INFO<6> 信息型消息. 比如 :打印它们发现的硬件的信息.
KERN_DEBUG<7> 用作调试消息.
内核中定义了 DEFAULT_MESSAGE_LOGLEVEL(在printk.c中)默认数值小于它的消息类型才会被答应到终端,我们可以把他设置为8则所有的信息都会被终端打印出来。
在系统中我们 可以使用 echo 8 > /proc/sys/kernel/printk 来调整这个数值(要root权限) 使信息全部被打印出来。
当然我们 也可以通过dmesg来查看所有的打印信息 (有一点不适用,就是当系统出现oops的时候 就不行了 因为你已经死机了 也就输不了这个命令 就看不到打印信息了)
#if SIMPLE_DEBUG
#define D(...) printk(KERN_DEBUG __VA_ARGS__)
#define WAR(...) printk(KERN_WARNING __VA_ARGS__)
#else
#define D(...) ((void)0)
#define WAR(...) ((void)0)
#endif
在需要调试的时候,我们去定义SIMPLE_DEBUG这个宏,在驱动代码测试都OK可以发行时候,去掉这个定义。
在需要打印的地方我们就使用
D(“print the log int func:%s line:%d”, __func__ ,__LINE__);
当然要修改成有意义的debug信息

打印当前进程信息
内核模块不像应用程序一样顺序执行,只用应用进程调用到想关联的函数才会到内核模块中call这个接口,那可不可以 打印调用进程的信息呢?
答案是肯定的,linux中定义了 current这个变量(<linux/sched.h>)current指向了当前的进程,他是一个 task_struct类型
其中有两个重要的成员
comm 表示了 当前的命令名名
pid 表示了当前进程号
D("[process: %s] [pid: %d] xxx\n" , current->comm, current->pid);

内核的container_of函数
在写字符设备驱动时候我们都会去自定义一个结构,其中包含了cdev结构
struct simple_dev{
	char *data;
	loff_t count;
	struct cdev cdev;
	struct semaphore semp;
};
在open方法中除了上一节看到的那样用一个次设备好来获取结构(有时候会出错 如果我们次设备号不是从0开始分配)
所以需要寻求一种安全的方法
container_of为我们提供了这么一个很好的接口
/**
 * container_of - cast a member of a structure out to the containing structure
 * @ptr:	the pointer to the member.
 * @type:	the type of the container struct this is embedded in.
 * @member:	the name of the member within the struct.
 *
 */
#define container_of(ptr, type, member) 
再解释一下 
ptr是指结构体一个成员的地址
type 指要获得的结构体
member ptr指向的成员在结构体中的名字
/*container_of(pointer, container_type, container_fild)
 we can get the container_type with one number in the container
 by this function. container_fild is the one of the number in the
 container_type , pointer point to confain_field type 
*/
temp_dev = container_of(inodp->i_cdev, struct simple_dev, cdev);
上边的E文 是我写的 poor English

container_of 的实现基本原理是这样的:
知道了结构体中某个成员的地址,  又可以求的该成员在改结构体中得偏移,拿成员的地址减去这个偏移,就得到了整个结构的地址,太佩服写内核的人了
#define container_of(ptr, type, member) ({			\
	const typeof(((type *)0)->member) * __mptr = (ptr);	\
	(type *)((char *)__mptr - offsetof(type, member)); })