A developer walks into a bar. He then gets completely and totally plastered before talking to his boss. That conversation then results in him accepting the task of writing a Linux kernel module in C++. I was that developer, minus the walking into a bar and getting plastered part. While I did put up a token effort to advocate doing the development in C, I got overruled. I then threw myself to the task with gusto.
In hindsight, I would recommend never going down this route. Ever. You might think that using C++ might enable a cross-platform codebase. Maybe you think you can find C++ engineers easier than C engineers. Perhaps you find Linus Torvalds’ opinions quaint and out of date. I did not know better in my youth, and all those considerations swayed me. I do not want to argue each point at this time. Maybe I'll revisit that in the future, but for now I just want to reiterate that I strongly discourage writing Linux kernel modules in C++.
Still with me? I assume you think you must be the special snowflake; the exception to the rule. Let me know how that goes. Nevertheless, let me share some of my resources and learnings. Perhaps you too can find success in this endeavor (but I doubt it).
Learn Linux kernel Development
You might as well start by learning generic Linux kernel development. If you have not previously waded into that world then: good luck to you. I do not know of a silver bullet for figuring that all out. I did acquire a useful O'Reilly book that served as a reference during my entire adventure: Linux Device Drivers by Jonathan Corbet, Alessandro Rubini, and Greg Kroah-Hartman. Read through that and join some mailing lists. Although a bit terse and cryptic, I also recommend the Unreliable Guide To Hacking The Linux Kernel. The name should clue you into the type of project on which you endeavor.
One necessary piece of infrastructure that may seem a bit foreign is the Makefile structure required. While I strongly recommend reading a guide — make that multiple guides and that book I mentioned in the last paragraph — about how to use the Linux kernel Makefiles, it’s fairly easy to get started. The basic idea of how kernel module makefiles differ from application makefiles is that the kernel module makefiles read in and execute in the kernel build environment. A significant part of the trouble consists of getting the environment you want into the environment where you actually make the objects.
C++ in the kernel
I am not the first person to put C++ in the Linux kernel, so I could stand on the shoulders of my predecessors. Pograph’s Weblog post of Porting C++ code to Linux kernel looked particularly promising. I found a past version of myself calling for help in the comments:
This example does not seem to work for me. Is this code rot or am I doing something wrong?
Others in the comments had success with the post, so I assume I just missed something. Over. And over. Again. And again. Now a Github user named korisk has actually set up a repository (with updates) to provide code for an empty C++ module scaffolding based on that blog post. Unfortunately, it has no clear licensing at the time that I write this, but at least it’s something, right?
I found the real gold mine of C++ kernel module development knowledge in OSDev.org. They have an entire article on C++ issues. That includes avoiding templates and exceptions, dealing with virtual functions, and defining memory operators. It even includes great code examples. You might be asking why I didn’t just lead off with that. Well, I mostly want you to suffer like I did. Actually, like most kernel documentation, the information on that page comes with little or no context. That makes diving right into the middle of it fairly difficult. The page also concerns itself primarily with building a complete kernel. While academically interesting, the vast majority of people just need to build a module that hooks into the existing Linux kernel.
Let’s talk about a few of the pitfalls I ran across:
- Figuring out how to deal with Linux kernel headers. I had to include some, because what’s the point of running a kernel module if it does not interact with the kernel? The Linux kernel headers include some C code that does not play nicely with C++. I’m mainly thinking of keyword collisions right now, but also compiler pragmas and other trickiness. It needs a little bit of hand-holding to get it ready for consumption by C++ code.
Except. . . it doesn’t just need it once. I could not just fix the problem and then move on. While the various headers have some consistency, they do change. So I might have to tweak each time a new kernel version comes out. Multiply that by the frequency of kernel revisions and the number of kernels used by supported distributions, and I end up with more than I wanted to maintain manually (I had other things to do).
So I automated it. I used CIL, a source-to-source parser and transformer for C. It could read the Linux kernel headers into an abstract syntax tree, manipulate that tree, and then output that tree into a C++ friendly format. The built-in transformations did not take care of everything I needed, so I wrote a module that caught all the loose ends and weird edge cases. When a new kernel came out, I just had to run the source through the transformation and then use the results in my C++ kernel module.
- I hate strings. They seem like such a simple concept, but in the end they require too much work. Something as simple as character width becomes nuanced very quickly. To be honest, most of my pain here came from working in a cross-platform code base rather than in trying to use C++ to do a job for C. Having to consider the character widths of OS X, Windows (user and kernel space), and Linux (user and kernel space) would be bad enough, but then think about unicode and globalization, and it becomes nearly intractable from any level at which I want to deal with it. Leave your assumptions about length, memory allocation, and null termination at the door, kids.
- You might assume that you do not have to worry about registers because you use a 3GL (third-generation programming language) like C++. You would be wrong. I found that my C and C++ compilers used different calling conventions. Consider this function call prototype that might get included in my C++ code:
int kernel_function(int a1, int a2, int a3);
A first-year computer science student can tell you that the arguments get pushed onto the stack. In other words, a call to this 3GL function results in the following assembly pseudo code:
That’s the mental model most programmers use for how variables get passed into a function call.
Kernel developers are not most programmers. Kernel developers are a special and unique breed. Kernel developers obsess about speed and performance. The Linux kernel is built using -mregparm=3, which is sometimes called fastcall. This means that instead of doing expensive stack operations, the compiler will just put the arguments in registers.
You see the problem. My C++ code expected the calling convention that pushed arguments on the stack, and the kernel expected my code to pass arguments in the registers. Never the twain would meet, and undefined behavior resulted. Finding the problem was the hard part, fixing it just required adding the compile argument to pass the arguments through registers.
Wrapping it up . . .
Still here, huh? Good for you. I think I need to go back to counseling for reliving this experience. If you find yourself having to put C++ in the Linux kernel, hopefully something here can either help you get going or help you avoid some major problems.
The good news: Threat Stack does not use a Linux kernel module! We collect instance information through kernel APIs that we access from user space. So we have that going for us.