What You Will Learn
This lecture teaches you how to cross-compile Linux kernel modules for ARM targets — the exact workflow embedded Linux engineers use every day in industry. By the end, you will understand:
- Why cross-compilation is needed for embedded targets running Linux 6.x
- How to set up a cross-compiler toolchain for ARM and ARM64
- How a Loadable Kernel Module (LKM) Makefile controls the cross-build
- How the kernel vermagic field enforces version matching
- How to transfer and insert a cross-compiled module on a real ARM device
- Common errors and how to fix them
This is part of our free Linux kernel development course and free Linux device drivers course at EmbeddedPathashala — completely free, no sign-up needed.
Before going through this lecture make sure you are comfortable with:
Why Cross-Compilation Matters for Linux Kernel Modules
When you build a kernel module for your own x86 laptop, you compile it right there on the machine — the same machine that runs it. This is called native compilation. Simple and straightforward.
But in embedded Linux development, your target device (a small ARM board, for example) has very little CPU power, limited RAM, and sometimes no compiler installed at all. Building the kernel module on the target itself would take forever — or simply not be possible.
The solution is cross-compilation: you compile the kernel module on your powerful x86 development machine (called the host), but you produce an output binary that runs on the ARM device (called the target). A special cross-compiler tool understands both worlds and produces the correct ARM binary from your x86 host.
| Aspect | Host Machine (x86_64) | Target Device (ARM) |
|---|---|---|
| Role | Compilation happens here | Module runs here |
| CPU Architecture | x86_64 (Intel / AMD) | ARM / ARM64 (Cortex-A series) |
| Compiler used | arm-linux-gnueabihf-gcc (cross) | gcc (native, if present) |
| Kernel headers needed | ARM kernel source / headers | Not needed for cross build |
| Output file | .ko file (ARM ELF binary) | Loaded with insmod |
Setting Up Your Cross-Compilation Toolchain on Linux 6.x
On a modern Ubuntu host, installing a cross-compiler for ARM is straightforward. For 32-bit ARM targets (like many older Raspberry Pi, BeagleBone, or custom boards):
sudo apt update
sudo apt install gcc-arm-linux-gnueabihf binutils-arm-linux-gnueabihf
For 64-bit ARM (ARM64 / AArch64) targets, which is the standard for modern SBCs and SoCs running Linux 6.x:
sudo apt install gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu
Verify your cross-compiler is installed correctly:
# For ARM32
arm-linux-gnueabihf-gcc --version
# For ARM64
aarch64-linux-gnu-gcc --version
You also need the kernel source or headers for the exact kernel version running on your target. This is critical. The host must compile the module against the same kernel version that runs on the device — otherwise the kernel will refuse to load the module.
For a custom embedded board, you typically clone your board vendor’s Linux 6.x kernel tree and build it first. For Raspberry Pi 5 (which runs Linux 6.x), you use the official Raspberry Pi kernel source.
# Example: clone the Raspberry Pi Linux 6.x kernel tree
git clone --depth=1 https://github.com/raspberrypi/linux --branch rpi-6.6.y rpi-linux
# Set up the default ARM64 configuration for RPi 5
cd rpi-linux
ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- make bcm2711_defconfig
Every kernel module compiled against a particular kernel version carries a vermagic string inside it. When you run insmod, the kernel checks this string against its own version. If they do not match exactly — even a minor difference in build configuration — the kernel refuses to load the module with an error like “version magic does not match”. This is not a bug. It is a deliberate safety check.
Writing a Cross-Compilation Makefile for Your Kernel Module
The kernel build system (kbuild) is what actually compiles your module. Your job is to write a Makefile that tells kbuild two things: what architecture to target, and which cross-compiler to use. Here is a proper Makefile for a module targeting ARM64 running Linux 6.x:
# Makefile for cross-compiling an LKM for ARM64 (Linux 6.x)
# Name of your module (change this to your module name)
obj-m += my_module.o
# Path to the kernel source tree built for your ARM target
KDIR ?= /path/to/your/rpi-linux
# Target architecture
ARCH := arm64
# Cross-compiler prefix
CROSS_COMPILE := aarch64-linux-gnu-
all:
$(MAKE) -C $(KDIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) clean
For a 32-bit ARM target, change the two lines to:
ARCH := arm
CROSS_COMPILE := arm-linux-gnueabihf-
| Your .c source file | → | make (host) | → | ARM cross-compiler | → | .ko (ARM ELF) | → | ARM device (insmod) |
| kbuild uses ARM kernel headers to produce a .ko with correct vermagic | ||||||||
Building and Transferring the Cross-Compiled Kernel Module
Once your Makefile is ready, run make from your module directory on the host:
cd my_module/
make
If everything is set up correctly, you will see kbuild output and end up with a my_module.ko file. This is an ARM ELF binary — you cannot run it on your x86 host directly, but it is exactly what your ARM target needs.
Check what the module was compiled for using file:
file my_module.ko
# Expected output for ARM64:
# my_module.ko: ELF 64-bit LSB relocatable, ARM aarch64, version 1 (SYSV), not stripped
Now transfer the module to your ARM device. The usual tool is scp:
# Replace 192.168.1.100 with your device's IP address
scp my_module.ko user@192.168.1.100:/home/user/
On the ARM device, insert and test the module:
sudo insmod ./my_module.ko
dmesg | tail -20
sudo rmmod my_module
dmesg | tail -5
Understanding vermagic and Kernel Version Matching in Linux 6.x
The vermagic string is one of the most important concepts to understand when cross-compiling kernel modules. Every .ko file embeds a vermagic string at compile time. The kernel checks this on every insmod call.
You can inspect the vermagic of your compiled module with modinfo:
modinfo ./my_module.ko
You will see output like this on a Linux 6.x ARM64 system:
filename: /home/user/my_module.ko
license: GPL
description: My first cross-compiled LKM
author: Your Name
srcversion: A1B2C3D4E5F6
depends:
name: my_module
vermagic: 6.6.20-v8+ SMP preempt mod_unload aarch64
The vermagic line tells you everything: kernel version (6.6.20-v8+), SMP support, preempt config, and architecture (aarch64). This must match the running kernel on the device exactly.
| Step | What the Kernel Does | Result |
|---|---|---|
| 1. insmod called | Kernel reads the .ko ELF file | Module loaded into memory |
| 2. vermagic extracted | Reads vermagic string from .ko | Gets version + config string |
| 3. Version compared | Compares with running kernel’s version | Match = proceed / Mismatch = ERROR |
| 4. init function called | Calls module_init() hook | Module is active |
Common Cross-Compilation Errors and How to Fix Them
Cause: The .ko was compiled against a different kernel version than the one running on the device.
Fix: Make sure KDIR in your Makefile points to the exact same kernel tree that produced the kernel running on the target device. Rebuild the module after getting the right kernel tree.
Cause: You are trying to insmod an x86 .ko on an ARM device, or vice versa. The ELF architecture does not match.
Fix: Confirm you used the correct ARCH and CROSS_COMPILE in your Makefile. Run file my_module.ko to check the ELF target architecture before transferring.
Cause: Cross-compiler is not installed or not in PATH.
Fix: Run which aarch64-linux-gnu-gcc to verify. Install with sudo apt install gcc-aarch64-linux-gnu.
Cause: Module dependencies under /lib/modules/<kernel-ver>/ are not fully populated on the device, and depmod has not been run.
Fix: Copy all built kernel modules to /lib/modules/<kernel-ver>/ on the device, then run sudo depmod -a. The module itself still loads and unloads correctly even before this step — the error is about module dependency metadata, not the module itself.
Best Practices for Cross-Compiling Linux Kernel Modules
- Always match kernel versions exactly. Use the same kernel tree to build both the kernel image and your modules. Even a kernel config difference can cause a vermagic mismatch.
- Use environment variables in your Makefile. Make ARCH and CROSS_COMPILE overridable from the command line so the same Makefile works for both native and cross builds.
- Keep a separate build directory. Do not mix your host-native module builds with cross-compiled builds. Use separate output directories.
- Test with modinfo before deploying. Always run
modinfoon the .ko before transferring to the device to confirm the vermagic and architecture. - Use sparse and checkpatch.pl. Run kernel code static analysis tools from the kernel source tree before building. Catch bugs before they get to the device.
- In Linux 6.x, prefer GPL-licensed modules. Many kernel symbols are only exported to GPL-licensed modules. Declare
MODULE_LICENSE("GPL")in your module to avoid symbol access errors.
🌟 Key Takeaways
Frequently Asked Questions
Yes, if your ARM device has enough resources, a compiler, and the kernel headers installed, you can compile natively on the device. Raspberry Pi for example can do this with the Raspberry Pi OS. However, in professional embedded development, cross-compilation on a host machine is the standard approach because it is much faster and the target may not have a compiler at all.
ARCH=arm targets 32-bit ARM processors (ARMv7 and earlier). ARCH=arm64 targets 64-bit ARM processors (ARMv8 and newer, also called AArch64). Modern SBCs like Raspberry Pi 4 and Pi 5, Rockchip RK3588 boards, and most new embedded Linux SoCs run a 64-bit kernel, so use ARCH=arm64. Always match ARCH with the kernel running on your target.
In Linux 6.x kernel code, implicit function declarations are treated as errors by the kernel build system. You must include the correct kernel headers in your module source. For example, use #include <linux/init.h>, #include <linux/module.h>, and whatever other headers the functions you call are declared in.
The most common reason is a vermagic mismatch — the module was compiled against a different kernel version or configuration than the one running on the ARM device. Use modinfo to check the vermagic in your .ko, and compare it with uname -r output on the target device. They must match exactly.
depmod scans the kernel modules installed under /lib/modules/<kernel-version>/ and builds a dependency database (modules.dep). This database is used by modprobe to automatically load module dependencies. Without running depmod -a after copying your modules, modprobe cannot find your module and some tools may report errors. Direct insmod still works without depmod.
The fundamental approach — setting ARCH and CROSS_COMPILE in the Makefile — is the same. However, Linux 6.x enforces stricter coding standards. Some older APIs are removed or changed. Implicit function declarations now cause build errors. You should always write module code against the kernel headers of the target Linux 6.x version you are building for, rather than adapting old code without checking the API.
Not necessarily. If multiple boards share the same ARM architecture (e.g., all ARM64 with the same ABI), one cross-compiler like aarch64-linux-gnu-gcc can build modules for all of them. What changes per board is the kernel source tree — each board typically has its own defconfig and sometimes custom kernel patches. The module .ko must be compiled against the kernel tree for the specific board it will run on.
No. Kernel modules must use the kernel’s kbuild build system, which is driven by Makefiles in the specific format the kernel expects. You cannot use CMake, Meson, or other build systems for kernel modules. You can use CMake for your user-space companion application, but the kernel module itself always uses a kbuild-compatible Makefile.
Conclusion
Cross-compiling Linux kernel modules for ARM targets is a fundamental skill for any embedded Linux developer. The workflow is clear once you understand the three key elements: the cross-compiler toolchain, the ARM kernel source tree, and the Makefile with ARCH and CROSS_COMPILE set correctly.
The most important rule: the kernel version used to compile the module must exactly match the kernel running on the target. The vermagic string enforces this, and understanding it saves you hours of debugging.
With Linux 6.x, the cross-compilation workflow is more streamlined than ever, with better toolchain support and cleaner kernel APIs. In the next lecture, we will look at how to gather system information from within a running kernel module — understanding the hardware it runs on at runtime.
Continue Learning — Free Linux Kernel Development Course
EmbeddedPathashala offers completely free courses on Linux kernel programming, device drivers, and embedded systems. No fees. No sign-up walls.
🏠 Visit EmbeddedPathashala