找回密码
 骑士注册

QQ登录

微博登录

搜索
❏ 站外平台:

黑客内核:编写属于你的第一个Linux内核模块

译者: LCTT joeren

| 2014-06-24 09:00   评论: 15 收藏: 15 分享: 12    

曾经多少次想要在内核游荡?曾经多少次茫然不知方向?你不要再对着它迷惘,让我们指引你走向前方……

内核编程常常看起来像是黑魔法,而在亚瑟 C 克拉克的眼中,它八成就是了。Linux内核和它的用户空间是大不相同的:抛开漫不经心,你必须小心翼翼,因为你编程中的一个bug就会影响到整个系统。浮点运算做起来可不容易,堆栈固定而狭小,而你写的代码总是异步的,因此你需要想想并发会导致什么。而除了所有这一切之外,Linux内核只是一个很大的、很复杂的C程序,它对每个人开放,任何人都去读它、学习它并改进它,而你也可以是其中之一。

学习内核编程的最简单的方式也许就是写个内核模块:一段可以动态加载进内核的代码。模块所能做的事是有限的——例如,他们不能在类似进程描述符这样的公共数据结构中增减字段(LCTT译注:可能会破坏整个内核及系统的功能)。但是,在其它方面,他们是成熟的内核级的代码,可以在需要时随时编译进内核(这样就可以摒弃所有的限制了)。完全可以在Linux源代码树以外来开发并编译一个模块(这并不奇怪,它称为树外开发),如果你只是想稍微玩玩,而并不想提交修改以包含到主线内核中去,这样的方式是很方便的。

在本教程中,我们将开发一个简单的内核模块用以创建一个/dev/reverse设备。写入该设备的字符串将以相反字序的方式读回(“Hello World”读成“World Hello”)。这是一个很受欢迎的程序员面试难题,当你利用自己的能力在内核级别实现这个功能时,可以使你得到一些加分。在开始前,有一句忠告:你的模块中的一个bug就会导致系统崩溃(虽然可能性不大,但还是有可能的)和数据丢失。在开始前,请确保你已经将重要数据备份,或者,采用一种更好的方式,在虚拟机中进行试验。

尽可能不要用root身份

默认情况下,/dev/reverse只有root可以使用,因此你只能使用sudo来运行你的测试程序。要解决该限制,可以创建一个包含以下内容的/lib/udev/rules.d/99-reverse.rules文件:

SUBSYSTEM=="misc", KERNEL=="reverse", MODE="0666"

别忘了重新插入模块。让非root用户访问设备节点往往不是一个好主意,但是在开发其间却是十分有用的。这并不是说以root身份运行二进制测试文件也不是个好主意。

模块的构造

由于大多数的Linux内核模块是用C写的(除了底层的特定于体系结构的部分),所以推荐你将你的模块以单一文件形式保存(例如,reverse.c)。我们已经把完整的源代码放在GitHub上——这里我们将看其中的一些片段。开始时,我们先要包含一些常见的文件头,并用预定义的宏来描述模块:

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Valentine Sinitsyn <valentine.sinitsyn@gmail.com>");
MODULE_DESCRIPTION("In-kernel phrase reverser");

这里一切都直接明了,除了MODULE_LICENSE():它不仅仅是一个标记。内核坚定地支持GPL兼容代码,因此如果你把许可证设置为其它非GPL兼容的(如,“Proprietary”[专利]),某些特定的内核功能将在你的模块中不可用。

什么时候不该写内核模块

内核编程很有趣,但是在现实项目中写(尤其是调试)内核代码要求特定的技巧。通常来讲,在没有其它方式可以解决你的问题时,你才应该在内核级别解决它。以下情形中,可能你在用户空间中解决它更好:

  • 你要开发一个USB驱动 —— 请查看libusb
  • 你要开发一个文件系统 —— 试试FUSE
  • 你在扩展Netfilter —— 那么libnetfilter_queue对你有所帮助。

通常,内核里面代码的性能会更好,但是对于许多项目而言,这点性能丢失并不严重。

由于内核编程总是异步的,没有一个main()函数来让Linux顺序执行你的模块。取而代之的是,你要为各种事件提供回调函数,像这个:

static int __init reverse_init(void)
{
    printk(KERN_INFO "reverse device has been registered\n");
    return 0;
}

static void __exit reverse_exit(void)
{
    printk(KERN_INFO "reverse device has been unregistered\n");
}

module_init(reverse_init);
module_exit(reverse_exit);

这里,我们定义的函数被称为模块的插入和删除。只有第一个的插入函数是必要的。目前,它们只是打印消息到内核环缓冲区(可以在用户空间通过dmesg命令访问);KERN_INFO是日志级别(注意,没有逗号)。__init__exit是属性 —— 联结到函数(或者变量)的元数据片。属性在用户空间的C代码中是很罕见的,但是内核中却很普遍。所有标记为__init的,会在初始化后释放内存以供重用(还记得那条过去内核的那条“Freeing unused kernel memory…[释放未使用的内核内存……]”信息吗?)。__exit表明,当代码被静态构建进内核时,该函数可以安全地优化了,不需要清理收尾。最后,module_init()module_exit()这两个宏将reverse_init()reverse_exit()函数设置成为我们模块的生命周期回调函数。实际的函数名称并不重要,你可以称它们为init()exit(),或者start()stop(),你想叫什么就叫什么吧。他们都是静态声明,你在外部模块是看不到的。事实上,内核中的任何函数都是不可见的,除非明确地被导出。然而,在内核程序员中,给你的函数加上模块名前缀是约定俗成的。

这些都是些基本概念 - 让我们来做更多有趣的事情吧。模块可以接收参数,就像这样:

# modprobe foo bar=1

modinfo命令显示了模块接受的所有参数,而这些也可以在/sys/module//parameters下作为文件使用。我们的模块需要一个缓冲区来存储参数 —— 让我们把这大小设置为用户可配置。在MODULE_DESCRIPTION()下添加如下三行:

static unsigned long buffer_size = 8192;
module_param(buffer_size, ulong, (S_IRUSR | S_IRGRP | S_IROTH));
MODULE_PARM_DESC(buffer_size, "Internal buffer size");

这儿,我们定义了一个变量来存储该值,封装成一个参数,并通过sysfs来让所有人可读。这个参数的描述(最后一行)出现在modinfo的输出中。

由于用户可以直接设置buffer_size,我们需要在reverse_init()来清除无效取值。你总该检查来自内核之外的数据 —— 如果你不这么做,你就是将自己置身于内核异常或安全漏洞之中。

static int __init reverse_init()
{
    if (!buffer_size)
        return -1;
    printk(KERN_INFO
        "reverse device has been registered, buffer size is %lu bytes\n",
        buffer_size);
    return 0;
}

来自模块初始化函数的非0返回值意味着模块执行失败。

123下一页
查看其它分页:
LCTT 译者
joeren 💎
共计翻译: 160.0 篇 | 共计贡献: 1039
贡献时间:2014-05-15 -> 2017-03-19
访问我的 LCTT 主页 | 在 GitHub 上关注我

收藏


最新评论

我也要发表评论

[1]
发表于 2014-08-27 23:08 的评论:
为什么kali linux make 出现make: ***/lib/modules/3.12-kali1-686-pae/build:没有那个文件或目录。停止。
Zhang_Longqi 2015-02-13 16:11 回复
没有安装内核源码
[1]
发表于 2014-08-27 23:08 的评论:
为什么kali linux make 出现make: ***/lib/modules/3.12-kali1-686-pae/build:没有那个文件或目录。停止。
来自 - 山东威海 的 Chrome/Windows 用户 2014-11-27 16:45 1 回复
我也遇到了这个问题,你搞明白了嘛???
[1]
wuanshou 发表于 2014-07-22 13:51 的评论:
不明觉厉呀。我只会编译,还真不会编写
linux 2014-07-22 15:05 2 回复
啊哈,我觉得照着这篇文章,可以写一个~
wuanshou 2014-07-04 13:12 回复
不明学厉呀!我也就编辑过自己笔记本的驱动,还是就是升级了一个LINUX内核。
[1]
Zhang_Longqi 发表于 2014-06-26 15:37 的评论:
对于译文最好加上原文链接http://www.linuxvoice.com/be-a-kernel-hacker/有时候英较好的的同学可以去查看原文以及弥补翻译的不足,
还有就是文章里曼涉及到的重要链接不要丢失了,这是其中项目的github链接https://github.com/vsinitsyn/reverse
[2]
linux 发表于 2014-07-03 13:17 的评论:
有译文的原文链接的,只是文章较长,原文链接和翻译者等信息,都在最后一页。
linux 2014-07-03 13:24 回复
文末的这种信息,在我们的CMS里面没有单独拎出来,所以很难每页都显示,我想想办法~
另外,这个GIT链接,我查看了原文了,可能是翻译过程给漏了,我现在给补上,谢谢您。
breakersun 2014-07-03 11:32 回复
新装的ubuntu10.04, git上的源码编不通, 什么情况
root@leo-laptop:/home/root/code/git/reverse# make
make -C /lib/modules/2.6.32-53-generic/build M=/home/root/code/git/reverse modules
make[1]: Entering directory `/usr/src/linux-headers-2.6.32-53-generic'
  CC [M]  /home/root/code/git/reverse/reverse.o
/home/root/code/git/reverse/reverse.c:206: error: ‘noop_llseek’ undeclared here (not in a function)
make[2]: *** [/home/root/code/git/reverse/reverse.o] Error 1
make[1]: *** [_module_/home/root/code/git/reverse] Error 2
make[1]: Leaving directory `/usr/src/linux-headers-2.6.32-53-generic'
make: *** [all] Error 2
oxfffO 2014-06-24 17:03  新浪微博网友评论 回复
//@Linux_cn:转发微博
很笨很笨的晰一 2014-06-24 10:33  新浪微博网友评论 回复
马克杯
TinyOS开发者-ytc 2014-06-24 10:03  新浪微博网友评论 回复
Repost
cc诗诗ss-disylee 2014-06-24 10:03  新浪微博网友评论 4 回复
这篇真是累死人了,但是必须学习~因为正在的精华就是这类文章[good]
东莞文老师 2014-06-24 10:03  新浪微博网友评论 2 回复
[ok]
返回顶部

分享到微信

打开微信,点击顶部的“╋”,
使用“扫一扫”将网页分享至微信。