musteresel's blog


Using CMake for a Linux kernel module (a template project)

2020-02-09

tagged: cmake, linux, c

Building a Linux kernel module (outside of the Linux kernel source tree) is a fairly straightforward task. In principle there is only one file required, named either Kbuild or Makefile, with content like:

obj-m := mymodule.o
# Additionally, if spilt in multiple source files:
# mymodule-objs := source1.o source2.o

Then one runs the following to build the module, from the directory which contains above file:

make -C /path/to/kernel/build M=$(pwd) modules

But what if I want tests, coverage reports, configuration, packaging and other nice stuff? CMake can do this, so I’ve setup CMake to build a Linux kernel module:

Configuration: Where’s the kernel build directory?

CMake can ask (or even search!) for the kernel build directory; i.e. the directory which contains the Makefile which is actually called in the above make command.

For me a pretty simple approach works fine: Let the user provide the path (via -Dkerneldir=/the/path when calling CMake) or use a sensible default (works on Ubuntu, for example):

set(kerneldir "" CACHE STRING "Path to the kernel build directory")
if("${kerneldir}" STREQUAL "")
  execute_process(COMMAND uname -r OUTPUT_VARIABLE uname_r
                  OUTPUT_STRIP_TRAILING_WHITESPACE)
  set(kerneldir "/lib/modules/${uname_r}/build")
endif()
find_file(kernel_makefile NAMES Makefile
                          PATHS ${kerneldir} NO_DEFAULT_PATH)
if(NOT kernel_makefile)
  message(FATAL_ERROR "There is no Makefile in kerneldir!")
endif()

Of course it’s possible to go crazy here and extends this to try multiple paths or whatever.

Gather the source files

For testing, coverage and so on I often prefer to having a non-kernel build of the most important functionality; therefore I split the module into a static library and a module.c file, where only the later contains acutal “really” Linux kernel specific code. However, I don’t use the static library for linking in the kernel module as there might be preprocessor defines or similar which need to be set differently for a kernel module build. Therefore I use the source files of the static library directly for the kernel module:

add_library(mymodule-lib STATIC source1.c source2.c)
get_target_property(module_sources mymodule-lib SOURCES)
list(APPEND module_sources module.c)

Generate a Kbuild file

Next step is to generate the Kbuild file in the binary directory. For that I take the list of source files, which in CMake is represented by a semicolon separated string, and turn that into a space separated string (because that’s what Make expects a “list” to be) …

string(REPLACE ";" " " module_sources_string "${module_sources}")
configure_file(Kbuild.in Kbuild @ONLY)

… which, in the Kbuild.in file (below), is filtered for actual source code files (as opposed to header files, for example):

obj-m := mymodule.o
mymodule-objs := $(pathsubst %.c,%.o, $(filter %.c, @module_sources_string@))

Make the source files accessible to the kernel Makefile

Of course the source files are in the source directory. But the kernel Makefile is working in the binary directory; therefore I just copy the source files over:

foreach(src ${module_sources})
  configure_file(${src} ${src} COPYONLY)
endforeach()

This means that an “in-tree-build” is not advised, but I wasn’t planning on that either way. Perhaps one should add a warning if an in-tree-build was attempted, though …

Add a custom target to build the module

Last but not least, a custom command to invoke the kernel Makefile. Note that I use a special target module-clean to call the kernel Makefile clean target; another alternative would be to try and add files generated by a build as BYPRODUCTS of the custom command.

set(module_cmd ${CMAKE_MAKE_PROGRAM} -C ${kerneldir} M=${CMAKE_CURRENT_BINARY_DIR})
add_custom_command(OUTPUT mymodule.ko
  COMMAND ${module_cmd} modules
  WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
  DEPENDS ${module_sources} ${CMAKE_CURRENT_BINARY_DIR}/Kbuild
  VERBATIM)
add_custom_target(module DEPENDS mymodule.ko)
add_custom_target(module-clean COMMAND ${module_cmd} clean)