Linux kernel, Linux system development, Software

C++ in Linux kernel

Linux kernel is written in C (and in Assembly in platform-specific portions) language. C language is only one allowed language to write kernel modules. And there is no problem, in most of the cases.

But sometimes some stranger things may be required. Let’s see how to use C++ for the Linux kernel modules.

Of course it’s not common situation. But image that you have some big code, with some logic and algorithms implementations and you need to reuse this code inside the Linux kernel, and this code is written in C++…

There are two options: completely rewrite this code to C language or somehow use it as is. This article can helps those brave hearts who chose the second option.

Have to say that it’s impossible to write pure C++ kernel module. You still need a lot of C routines and interlayers to communicate with kernel and use resources. But all this is possible because C++ is basically based on C and can link with C functions and structures.
C code is also can use C++ methods if it’s not classes or something C++ specific.

Actually when talking about C++ it’s very important to understand that language functionality is very limited. Standard libraries, like libstdc++ with all containers is not available. Exceptions and RTTI is also can’t be used because of overhead and required resources to add support of these features.
Since standard library is not available we need to implement some basic runtime functionality like ‘new‘ and ‘delete‘ functions, virtual functions support and so on, everything that can cause linker errors. It might be sounds not so easy but actually everything is quite simple.

To support C++ runtime required two components. I called this components “cpp support” and “kernel library“.
First component “cpp support” implements C++ specific things like memory allocators and so on.
Second component “kernel library” is lightweight implementation of the standard library with low-level functions, but actually this is only “bridge” between C “library” (in-kernel implementation) and C++ world.

C and C++ are working together

When talking about this “bridge” it’s very important to know that it’s impossible to directly mix C and C++ code without little trick.
There is a difference in function name mangling in C and C++ compilers. In C++ world function can be overloaded so compiler adding some special information about specific function prototypes. This code can’t be linked by C code and vise versa. That’s why can’t directly include C headers in to C++ file.
To solve this problem there is a little instruction to the C++ compiler called ‘extern “C”‘. This instructions disables C++ specific name mangling and allows to mixing the code.
C-style code must be wrapped with ‘extern “C”‘ for C++ compiler. For the C compiler such wrapping is not required. C++ compilers defines preprocessor variable “__cplusplus” that can be used with function wrappers.
Here is how it looks like:

#ifdef __cplusplus
extern "C" {
#endif
void foo_c_style_function();
void bar_c_style_function();
#ifdef __cplusplus
}
#endif

Great thing here that actual implementation of this function can be in C or C++ file. In a C++ world both functions can use classes and other C++ specific code and still can be called from the C world.

When talking about Linux kernel modules some tricks also required in Makefile. Will be discussed below.

Now all together.
What is required to support and run C++ code in the Linux kernel.

Demo implementation of the kernel module with C++ components

Let’s begin our implementation from the simple logger that can provide printk functionality for the C++ module.

Header and implementation files.

/*
 * Linux kernel logger for C and C++
 */

#ifndef LOGGER_H
#define LOGGER_H

#ifdef __cplusplus
extern "C" {
#endif
void kern_log(const char* fmt, ...);
#ifdef __cplusplus
}
#endif

#endif
/*
 * Linux kernel logger for C and C++
 */

#include <linux/kernel.h>
#include "logger.h"

void kern_log(const char* fmt, ...)
{
    va_list args;
    va_start(args, fmt);
    vprintk(fmt, args);
    va_end(args);
}

Now we can use kern_log function in the C and C++ code.

Declaration of the example C++ module with few classes. Implementation, cpp_module.cpp:

/*
 * C++ component inside the Linux kernel
 */
#include <linux/kernel.h>
#include <linux/module.h>
#include "cpp_module.h"
#include "logger.h"

/* Also needed to define NULL as simple 0. It's Ok by standard. */
#define NULL 0

class foo {
public:
    foo()
        : a(0)
    {
        kern_log("C++ class constructor\n");
    }

    ~foo()
    {
        kern_log("C++ class destructor\n");
    }

    /* "Normal" virtual function */
    virtual void set_data(int data)
    { 
        kern_log("Don't call me!\n"); 
    };  

    /* Pure virtual function */
    virtual int get_data() =0;  

protected:
    int a;
};

/* Class bar is inheritor of the class foo
 * Overloading implementation of the class methods
 */
class bar: foo {
public:
/* Virtual destructor is required */
    virtual ~bar()
    {
    }

    void set_data(int data)
    {
        kern_log(">> set_data %d\n", data);
        a = data;
    }

    int get_data()
    {
        return a;
    }
};

static bar *bar_instance = NULL;

/* This functions can be called from the C code */
void init_cpp_subsystem_example(void)
{
    kern_log("Init C++ subsystem\n");

    bar_instance = new bar;

    if (!bar_instance) {
        kern_log("Failed to allocate bar class\n");
        return;
    }

    bar_instance->set_data(42);

    kern_log("Getting data from bar: %d\n", bar_instance->get_data());
}

void release_cpp_subsystem_example(void)
{
    kern_log("Release C++ subsystem\n");
    
    if (bar_instance) {
        delete bar_instance;
    }
}

And header, cpp_module.h:

/*
 */

#ifndef CPP_MODULE_H
#define CPP_MODULE_H

#ifdef __cplusplus
extern "C" {
#endif
void init_cpp_subsystem_example(void);
void release_cpp_subsystem_example(void);
#ifdef __cplusplus
}
#endif

#endif

Very basic kernel library module to support C++ code.

/*
 * Kernel lib - support basic C++ runtime functions
 */

#ifndef KERN_LIB_H
#define KERN_LIB_H

#ifdef __cplusplus
#include <cstdarg>
extern "C" {
#else
#include <stdarg.h>
#endif

void kmemset(void *dst, int c, unsigned int len);
void *kcmemcpy(void *dst, void *src, unsigned int len);
void *kcmemmove(void *dst, void *src, unsigned int len);
int kcmemcmp(void *p1, void *p2, unsigned int len);
void *kcmalloc(unsigned int size);
void *kcrealloc(void *mem, unsigned int size);
void kcfree(void *mem);

#ifdef __cplusplus
}
#endif

#endif
/*
 * Kernel lib - support basic C++ runtime functions
 */

#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/string.h>

#include "kern_lib.h"

void kcmemset(void *dst, int c, unsigned int len)
{
    memset(dst, c, len);
}

void *kcmemcpy(void *dst, void *src, unsigned int len)
{
    memcpy(dst, src, len);
    return dst;
}

void *kcmemmove(void *dst, void *src, unsigned int len)
{
    memmove(dst, src, len);
    return dst;
}

int kcmemcmp(void *p1, void *p2, unsigned int len)
{
    return memcmp(p1, p2, len);
}

void *kcmalloc(unsigned int size)
{
    return kmalloc(size, GFP_ATOMIC);
}

void *kcrealloc(void *mem, unsigned int size)
{
    return krealloc(mem, size, GFP_ATOMIC);
}

void kcfree(void *mem)
{
    kfree(mem);
}

And the last C++ component is “cpp support” module. There is no header file, this code is only required by the linker and just should be present in the object file.

/*
 * Kernel C++ support
 */

#include <cstddef>
#include "kern_lib.h"
#include "logger.h"

void *operator new(size_t sz) throw ()
{
    kmalloc(sz);
    return kcmalloc(sz);
}

void *operator new[](size_t sz) throw ()
{
    return kcmalloc(sz);
}

void operator delete(void *p)
{
    kcfree(p);
}

void operator delete[](void *p)
{
    kcfree(p);
}

void terminate()
{
    kern_log("terminate requested\n");
}

extern "C" void __cxa_pure_virtual()
{
    kern_log("cxa_pure_virtual error handler\n");
}

As you can see this module is quite simple. Function “terminate()” required to be defined and must terminate execution. But since this code is working inside the kernel there is no process to terminate. Probably you can request here panic of the kernel.

Function with a mangled name “__cxa_pure_virtual()” is also required by the linker. This function is called when occured some error with virtual functions. Just another stub.

Here is module entry point which can use C++ code from above.

/*
 * Linux kernel module 
 */

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

#include "logger.h"
#include "cpp_module.h"

static int __init module_load(void)
{
    kern_log("Loading C++ kernel module\n");

    init_cpp_subsystem_example();

    return 0;
}

static void __exit module_unload(void)
{
    kern_log("Unloading C++ kernel module\n");

    release_cpp_subsystem_example();
}

module_init(module_load);
module_exit(module_unload);

MODULE_SUPPORTED_DEVICE ("cpp_kernel");
MODULE_DESCRIPTION ("Linux kernel module with C++");
MODULE_VERSION ("0.1");
MODULE_AUTHOR ("Oleg Kutkov");
MODULE_LICENSE ("GPL");

Now it’s time for the Makefile with some tricks.
We can declare cpp objects in a common list of the all module objects.

OBJECTS := module.o \
            kern_lib.o \
            logger.o \
            cpp_support.cpp.o \
            cpp_module.cpp.o

But to be able to build this cpp objects we need to define special target for the cpp files:

cxx-prefix := " $(HOSTCXX) [M]  "

%.cpp.o: %.cpp
    @echo $(cxx-prefix)$@
    @$(HOSTCXX) $(cxxflags) -c $< -o $@

This target defines to use g++ compiler for cpp files. Also you can see here some “cosmetics” that changes output of the cpp build to the Kbuild-like.

For the proper compilation it’s important to set some compiler flags.

cxxflags = $(FLAGS) \
            -fno-builtin \
            -nostdlib \
            -fno-rtti \
            -fno-exceptions \
            -std=c++0x

FLAGS is common flags set that can be derived from the CFLAGS.
-fno-builtin disables built-in compiler functions. Not all of the functions can be used inside the kernel.
-nostdlib disables usage of the standard C++ library
-fno-rtti disables RTTI support. This can help to reduce code size.
-fno-exceptions disables exceptions support.
-std=c++0x defines to use 0x C++ standard. You can try to use some other standard if required…

Also it’s very important to provide KBUILD_CFLAGS for the C++ compiler. But not all flags are valid for the C++ compiler. To remove warnings we can use sed magic:

cxx-selected-flags = $(shell echo $(KBUILD_CFLAGS) \
            | sed s/-D\"KBUILD.\"//g \
            | sed s/-Wstrict-prototypes//g \
            | sed s/-Wdeclaration-after-statement//g \
            | sed s/-Wno-pointer-sign//g \
            | sed s/-std=gnu90//g)

Append cxx-selected-flags to the cxxflags

Now all together, Makefile:

#
# Linux kernel C++ module makefile
# Oleg Kutkov, 2019
#

MOD_NAME    := cpp_kernel
KERNEL      := /lib/modules/$(shell uname -r)/build
FLAGS       := -Wall
KMOD_DIR    := $(shell pwd)

OBJECTS := module.o \
            kern_lib.o \
            logger.o \
            cpp_support.cpp.o \
            cpp_module.cpp.o

ccflags-y += $(FLAGS)

# Apply C flags to the cpp compiler and disable cpp features that can't be supported in kernel module

cxx-selected-flags = $(shell echo $(KBUILD_CFLAGS) \
            | sed s/-D\"KBUILD.\"//g \
            | sed s/-Wstrict-prototypes//g \
            | sed s/-Wdeclaration-after-statement//g \
            | sed s/-Wno-pointer-sign//g \
            | sed s/-std=gnu90//g)

cxxflags = $(FLAGS) \
            $(cxx-selected-flags) \
            -fno-builtin \
            -nostdlib \
            -fno-rtti \
            -fno-exceptions \
            -std=c++0x


obj-m += $(MOD_NAME).o

$(MOD_NAME)-y := $(OBJECTS)

.PHONY: $(MOD_NAME).ko
$(MOD_NAME).ko:
    @echo building module
    make -C $(KERNEL) M=$(KMOD_DIR) modules

cxx-prefix := " $(HOSTCXX) [M]  "

%.cpp.o: %.cpp
    @echo $(cxx-prefix)$@
    @$(HOSTCXX) $(cxxflags) -c $< -o $@

.PHONY: clean
clean:
    @echo clean
    make -C $(KERNEL) M=$(KMOD_DIR) clean

Compilation and testing

$ make clean && make
$ sudo insmod cpp_kernel.ko
$ dmesg | tail -n10

[158142.977086] Loading C++ kernel module
[158142.977088] Init C++ subsystem
[158142.977089] C++ class constructor
[158142.977091] >> set_data 42
[158142.977092] Getting data from bar: 42
[158144.422263] Unloading C++ kernel module
[158144.422265] Release C++ subsystem
[158144.422266] C++ class destructor

As you can see everything is working properly. Destructors of the classes are handled as expected, no linking errors or crashes.

Hope this material will be helpful for someone 🙂
Thanks for the reading!

Tagged , , ,

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.