我见过很多疯狂的事,我也做过很多疯狂的事。今天我就给你们讲一个。
一个开发走进一间酒吧。他喝的非常非常醉后跟他的老板聊天。那段对话最终的结果是他接受了一个任务——用C++写一个Linux内核模块。我就是那个开发,不算走进酒吧并喝醉的那部分。当我提倡做C的发展能取得一些成绩时,这个提议被推翻了。随后我只能满怀热情投入到任务中。
回想起来,我不会建议走这条路。但是,你也许会想用C++能做一个跨平台的代码库。也许你觉得你能发现做C++工程师比C工程师简单。也许你会觉得 Linus Torvalds’ 的观点奇怪并且过时了。在我年轻的时候我很无知,并且那些注意事项影响着我。在那个时候我不想说服每一点。也许在未来我会再看一次,但是现在,我只想重申我强烈反对用C++写Linux内核模块。
还在看吗?假设你觉得自己与众不同,可以超越这些条条框框。让我知道后来怎么样了。无论如何,我现在分享一些我的资源和经验。也许你也能在这努力中找到成就感(一脸怀疑)。
雷神短歌
翻译于 2016/11/02 09:28
你可以从阅读《学习通用的Linux内核开发》一书开始。 如果你以前没有涉及过这个部分,那么祝你好运。我不知道是否存在可以解决一切问题的银弹。我找到了一本有用的O'Reilly书,作为我的整个冒险过程中的参考:Jonathan Corbet,Alessandro Rubini和Greg Kroah-Hartman编著的《Linux设备驱动开发》。 通读这本书并加入一些邮件列表。虽然有点简洁和神秘,我还推荐“不可靠指南之破解Linux内核”一文(Unreliable Guide to Hacking the Linux Kernel)。 这个标题应该可以指引你进入你所努力的项目类型中。
一个必要且必要的基础知识是Makefile结构,虽然它看起来很不相干。同时我强烈建议阅读下关于如何使用Linux内核makefile的指南(a guide)(再来一份多个指南和我在最后一段提到的书),它是相当容易开始。 内核模块的makefile与应用程序makefile的不同之处在于内核模块makefile在内核构建环境中读入并运行。 相当一部分烦恼是如何获得你想要的环境到你实际生成对象的环境。
Tocy
翻译于 2016/11/03 11:57
我并不是将C++应用到Linux内核的第一人,因此可以说我是站在前人的肩膀上。Pograph的博文 Porting C++ code to Linux kernel看起来相当不错。我发现我在过去某个版本中寻求帮助的评论如下:
这个示例貌似不适合我。是代码太陈旧还是我做错了什么呢?
其他人在评论中说参考这个博文成功过,所以我认为我可能是错过了什么。一遍遍的重复、尝试。 现在,一个用户名为korisk的GitHub用户设置了一个代码仓库(包含更新),为基于该博客文章的 an empty C++ module scaffolding提供源码。 不幸的是,它在我写这个时候没有明确的授权许可,但至少它是一个东西,对不对?
我在OSDev.org中发现了关于C ++内核模块开发知识的真正宝藏。 他们有一篇关于C ++问题的完整文章。 其中包括如何规避模板和异常、虚函数的处理以及内存操作符的定义。 它甚至包括大量的代码示例。 你可能会问为什么我不以此开篇呢。 好吧,我更想要你像我一样经历同样的遭遇。 实际上,像大多数内核文档一样,该页面上的信息很少或没有上下文。 这使得深入其中相当困难。该页面还主要关心自身的目标,如何构建完整的系统内核。虽然学术上很有趣,但绝大多数人只需要构建一个hook到现有Linux内核的模块即可。
Tocy
翻译于 2016/11/02 10:23
让我们来谈谈我遇到的一些陷阱。
搞清如何处理Linux内核头文件。我不得不将一些头文件包含在内,因为如果它不与内核交互,那运行内核模块的意义何在? Linux内核头文件中包括一些不能与C++完美配合的C代码。我目前主要考虑保留字冲突问题,但也包括编译知识语句以及其他棘手问题。它需要一点手动操作,以使其准备好由C ++代码消费。 除了它不只需要一次。 我不能只是解决这个问题,然后继续。 虽然各种头部有一些一致性,但它们会改变。 所以,我可能需要调整每次一个新的内核版本出来。 乘以内核修订的频率和受支持的分布使用的内核的数量,并且我最终比我想要手动维护(我有其他事情要做)。
为了供C++使用,它需要一些手动调整。只是这不只需要一次。 我不能只是解决这个问题,然后转而处理其他问题。虽然各种不同的头文件拥有某些一致性,但是它们会改变。所以,当每次一个新的内核版本出来时我可能都需要重新调整。考虑到乘以内核修订的频次和受支持的发布版本使用的内核的数量,我最终要做比我手动维护数倍的工作(我有诸多其他事情要做)。
所以,我自动化了这个过程。 我使用了CIL,一个用于C语言的源码的解析器和变换器。它可以读取Linux内核头文件到一个抽象语法树中,操作该树,然后将该树输出为C ++友好的格式。 CIL内置的转换并没有覆盖我所有的需求,所以我写了一个模块,捕获所有的松散端和奇怪的边界情况。当一个新的内核出来时,我只需要通过这个转换结构处理源代码,然后在我的C ++内核模块中使用它们。
我讨厌字符串. 他们看起来像很简单的概念,但最终,他们需要太多的工作。 像字符宽度那样简单的东西很快变得微妙起来。说实话,这里的大部分痛苦来自于在跨平台代码库中的工作,而不是试图使用C ++做可以用C做的事。要考虑OS X、Windows(用户和内核空间)和Linux(用户和内核空间)上的字符宽度,这已经足够烦人了,但后来还要考虑Unicode和全球化,在某种程度上,这超出了我能处理的范围。孩子们,丢掉你关于字符串长度、内存分配、以及null终止符的假定吧。
Tocy
翻译于 2016/11/03 15:30
你可能会认为你不需要了解寄存器,因为你使用的是像C++这样的第三代编程语言(3GL)。那你有可能错了。我发现我的C/C++编译器使用不同的调用规约。我的C++代码可能包含类似下面的函数原型:
int kernel_function(int a1, int a2, int a3);
一年级计算机专业的学生会这样告诉你函数参数是下面这样入栈的。 换句话说,调用这个3GL函数导致以下汇编伪代码:
push a3 push a2 push a1 call kernel_function
这是大多数程序员将参数传递到函数调用中的心理模型。
但是内核开发者并不是大多数程序员。内核开发人员是一种特殊和独特的品种。内核开发人员关注速度和性能。Linux内核使用-mregparm = 3构建,有时称为fastcall。 这意味着,编译器不会执行耗时的栈操作,而只是将参数放入寄存器。
你看到的问题症结了吧。 我的C++代码期望的调用规约是使用栈传递参数,而内核期望我的代码使用寄存器中传递参数。二者的矛盾是无法同时满足的,因此导致诸多未定义的行为。找到问题所在是最困难的部分;修复它只是需要添加编译参数,让编译器使用寄存器传递参数。
Tocy
翻译于 2016/11/02 10:44
到此,嗯?对你有好处。我想我需要回去咨询,重温这个经验。如果你发现自己不得不把C++放在Linux内核中,但愿这里的东西可以帮助你,或者帮助你避免一些重大的麻烦。
好消息:Threat Stack并不是用Linux内核模块!我们通过从用户访问的内核API来收集实例信息。所以,我们支持我们自己