Intermediate
Linux 6.x
ARM 32/64-bit
Always
Cross-Compiling Linux Kernel Modules for ARM Targets (Linux 6.x)
Welcome to this free Linux kernel programming course lecture on cross-compilation of kernel modules. If you are building embedded Linux products — whether it is an industrial controller, a set-top box, a network appliance, or any ARM-based board — you will almost certainly write kernel modules on an x86 development machine and deploy them on an ARM target. That process is called cross-compilation, and it is one of the most essential skills in free Linux device drivers development.
In this lecture you will learn exactly how cross-compilation works, why kernel modules are tied to a specific kernel version, and how to diagnose and fix the most common failure people encounter when they load a cross-compiled module onto a real device. Everything here targets Linux kernel 6.x and modern toolchains.
What You Will Learn
- What cross-compilation means for Linux kernel modules
- How to set up an ARM cross-toolchain on an x86 host (Linux 6.x)
- How to write a Makefile that supports both native and cross builds
- How to cross-compile a kernel module step by step
- What the kernel ABI is and why it enforces version-exact module loading
- How to read
vermagicand diagnose “Invalid module format” errors - What DKMS is and when you should consider using it
- Best practices for embedded Linux kernel module development
Prerequisites
Before going through this lecture, you should be comfortable with:
- Writing a basic “hello world” Linux kernel module in C
- Understanding of Makefiles and the Linux kernel build system (Kbuild)
- Basic command-line usage on Linux
- Some familiarity with ARM architecture (does not need to be deep)
If you have not yet written a kernel module, check the earlier lectures of this free Linux kernel programming course before continuing here.
Part 1 — What Is Cross-Compilation?
When you compile a program normally, the machine that runs the compiler is also the machine that will run the resulting binary. That is called a native build. Cross-compilation is different: the machine that runs the compiler (the host) has a different CPU architecture from the machine that will run the binary (the target).
In embedded Linux development your host is almost always an x86-64 desktop or laptop running Ubuntu or another standard Linux distribution. Your target is an ARM board — 32-bit ARMv7 or 64-bit ARMv8/AArch64. You cannot simply run your x86 compiler and expect to get ARM machine code. You need a cross-toolchain: a set of tools (compiler, linker, assembler, C library headers) that run on x86 but produce ARM binaries.
|
HOST (x86-64 PC) ▶ Runs the cross-compiler ▶ Has the ARM kernel source ▶ Has the ARM sysroot / headers ▶ Produces .ko fileExample: Ubuntu 22.04 / 24.04 x86_64 |
→ |
TARGET (ARM Board) ▶ Runs the kernel module ▶ ARM CPU (32 or 64-bit) ▶ Runs the exact kernel the module was built against Example: ARM Cortex-A board, Linux 6.x |
The .ko file built on the host is copied to the target via scp or NFS,
then loaded with insmod or modprobe
|
||
Why Can’t You Just Copy an x86 Module to ARM?
A .ko file is an ELF object file containing machine code. x86-64 machine code uses
completely different instructions from ARM machine code. The CPU simply cannot execute the wrong
instruction set. Beyond that, even if the architecture matches, the kernel enforces additional
compatibility rules through a mechanism called the kernel ABI. We will cover that
in detail below.
Part 2 — Setting Up the ARM Cross-Toolchain on Linux 6.x Hosts
Modern Ubuntu (22.04 and 24.04) ships cross-toolchains in its package repository. You do not need to build one from scratch. Install the toolchain for your target:
For 32-bit ARMv7 (hard-float)
# Install the ARM 32-bit cross-compiler
sudo apt update
sudo apt install gcc-arm-linux-gnueabihf binutils-arm-linux-gnueabihf
# Verify installation
arm-linux-gnueabihf-gcc --version
For 64-bit AArch64 (ARMv8)
# Install the AArch64 cross-compiler
sudo apt install gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu
# Verify
aarch64-linux-gnu-gcc --version
You Also Need the Target Kernel Source
Cross-compiling a kernel module requires access to the configured kernel source tree of the exact kernel running on your target. The module build system (Kbuild) uses this source tree to pull in the correct headers, symbol tables, and configuration.
You cannot use the kernel headers from your host’s Ubuntu package because those are for the x86 host,
not for your ARM target. You need the ARM target’s kernel source, configured and partially built
(at minimum, a make modules_prepare pass).
Part 3 — Writing a Cross-Compilation-Aware Makefile
The Makefile for a kernel module needs to accept ARCH and CROSS_COMPILE
on the command line and pass them through to the kernel build system. Here is a clean, modern
Makefile structure for Linux 6.x:
# Kernel module Makefile — Linux 6.x compatible
# Supports both native build and cross-compilation for ARM
obj-m := my_driver.o
# Default: use the running host kernel
KDIR ?= /lib/modules/$(shell uname -r)/build
# Allow overriding from command line:
# make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KDIR=/path/to/arm/kernel
ARCH ?= $(shell uname -m)
CROSS_COMPILE ?=
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
Native Build (x86 Host for x86 Target)
make
Cross-Build for 32-bit ARMv7
make ARCH=arm \
CROSS_COMPILE=arm-linux-gnueabihf- \
KDIR=/home/dev/work/arm-kernel/linux
Cross-Build for 64-bit AArch64
make ARCH=arm64 \
CROSS_COMPILE=aarch64-linux-gnu- \
KDIR=/home/dev/work/arm64-kernel/linux
ARCH=arm covers all 32-bit ARM variants.
For 64-bit ARM, always use ARCH=arm64 — not ARCH=aarch64.
The build system does not recognise the latter.
| File | What It Is | Do You Deploy It? |
|---|---|---|
my_driver.o |
Compiled object file (intermediate) | No |
my_driver.mod.o |
Module metadata object (intermediate) | No |
my_driver.ko |
Final loadable kernel module (ELF) | Yes — this is what you copy to the target |
Module.symvers |
Exported symbol versions for this module | Only if another module depends on yours |
Verifying the Architecture of the Built Module
After a successful build, always verify the architecture of the .ko file before
deploying it. Use the file utility:
file ./my_driver.ko
# Expected output for 32-bit ARM:
# my_driver.ko: ELF 32-bit LSB relocatable, ARM, EABI5 version 1 (SYSV), not stripped
# Expected output for 64-bit AArch64:
# my_driver.ko: ELF 64-bit LSB relocatable, ARM aarch64, version 1 (SYSV), not stripped
If the output says x86-64 instead of ARM, you did not pass
ARCH and CROSS_COMPILE correctly — or the Makefile used the wrong
KDIR.
Part 4 — The Linux Kernel ABI and Version Locking
This is the concept that trips up most beginners in free Linux device drivers development, so read this section carefully.
The Linux kernel enforces a strict rule: a kernel module can only be loaded into the exact kernel it was compiled against. This is enforced through the kernel ABI. “Exact” here means matching on all of the following:
- Kernel version string (e.g.
6.6.30-v8+) - SMP support (yes or no)
- Module unloading support
- Symbol versioning (modversions) settings
- CPU architecture and sub-variant (e.g. ARMv7 p2v8 vs ARMv8)
- Key configuration options (preemption model, etc.)
| vermagic field | Example value | What it means |
| Kernel version | 6.6.30-v8+ |
Exact kernel version this module targets |
| SMP | SMP |
Symmetric multi-processing support compiled in |
| mod_unload | mod_unload |
Module can be unloaded at runtime |
| modversions | modversions |
Symbol CRC checksums are enabled |
| Architecture | ARMv7 p2v8 |
CPU sub-architecture and virtual-to-physical patch variant |
Reading the vermagic of a Module
modinfo ./my_driver.ko
# Look for the vermagic line:
# vermagic: 6.6.30-v8+ SMP mod_unload modversions ARMv8
Checking the Running Kernel on the Target
# On the ARM target device:
uname -r
# 6.6.30-v8+
cat /proc/version
# Linux version 6.6.30-v8+ (gcc version 12.2.0) #1 SMP ...
The vermagic string from modinfo must match the output of uname -r
on the target. If they differ, insmod will refuse the module with an
Invalid module format error.
Part 5 — Diagnosing “Invalid module format” Errors
The single most common error beginners hit in this free Linux kernel programming course exercise is:
$ sudo insmod ./my_driver.ko
insmod: ERROR: could not insert module ./my_driver.ko: Invalid module format
The error message is not very helpful by itself. Here is how to diagnose it properly.
Step 1 — Check the Kernel Ring Buffer
sudo dmesg | tail -5
You will see something like:
[ 45.321089] my_driver: no symbol version for module_layout
[ 45.321103] my_driver: version magic '6.1.21-v7+ mod_unload modversions ARMv7 p2v8'
should be '6.6.30-v7+ SMP mod_unload modversions ARMv7 p2v8'
The kernel is telling you exactly what vermagic the module has versus what it expects.
This is the primary diagnostic tool. In Linux 6.x the message format is essentially the same.
insmod failsInvalid module format
|
→ |
Runsudo dmesg | tail -5Read the version magic lines |
→ |
Two causes: 1. Wrong KDIR in Makefile2. Target booted a different kernel than expected |
|||
|
|||||||
Step 2 — Compare vermagic vs uname -r Side by Side
# On host: check what kernel the module was built against
modinfo ./my_driver.ko | grep vermagic
# On target: check what kernel is actually running
uname -r
These two must produce identical version strings for insmod to succeed.
Part 6 — Source Compatibility vs Binary Compatibility
A very important distinction for anyone studying this free Linux device drivers course: kernel modules are source-compatible across different platforms but not binary-compatible.
| Property | Source Compatible? | Binary Compatible? |
|---|---|---|
| Same kernel version, same arch | ✓ Yes | ✓ Yes |
| Same kernel version, different arch (x86 vs ARM) | ✓ Yes (if written portably) | ✗ No |
| Different kernel version, same arch | ⚠ Usually (API changes) | ✗ No |
| Different distro, same kernel version | ✓ Usually | ✗ No (config differs) |
The practical takeaway: if you have the source code of a kernel module, you can always build it
from source on any system with the correct kernel headers, and the resulting binary will work on
that system. The compiled .ko file itself is not portable.
Part 7 — Deploying a Cross-Compiled Module to the Target
Once you have built the .ko file and confirmed it targets the correct architecture,
copy it to the target device. The most common method in development is scp:
# Copy module from host to ARM target
scp ./my_driver.ko user@192.168.1.100:/home/user/
# SSH into the target and load it
ssh user@192.168.1.100
sudo insmod /home/user/my_driver.ko
# Verify it loaded
lsmod | grep my_driver
dmesg | tail -10
Installing into the Proper Kernel Module Path
For production use, install the module into the kernel’s module directory so that
modprobe can find it by name and handle dependencies automatically:
# On the target, install to the correct path
sudo cp my_driver.ko /lib/modules/$(uname -r)/extra/
# Rebuild the module dependency database
sudo depmod -a
# Now you can load it by name
sudo modprobe my_driver
Part 8 — What Is DKMS and When to Use It
DKMS (Dynamic Kernel Module Support) is a framework that automatically recompiles out-of-tree kernel modules whenever a new kernel is installed on a system. It is most useful in scenarios where:
- You distribute a driver to end users who may upgrade their kernel independently
- You maintain a module that must work across multiple kernel versions on the same machine
- You want automatic rebuilds on package update (e.g. via
aptordnf)
|
1. Register Add module source to DKMS tree at /usr/src/mydriver-1.0/
|
→ |
2. Build DKMS compiles the module against the current kernel headers |
→ |
3. Install DKMS installs .ko into/lib/modules/<ver>/updates/dkms/
|
→ |
4. Auto-rebuild On next kernel upgrade, DKMS rebuilds automatically |
Part 9 — Linux 6.x Cross-Compilation Changes vs Older Kernels
If you have seen older cross-compilation tutorials targeting kernel 4.x or 5.x, here are the key differences you need to know for Linux 6.x:
| Topic | Linux 4.x / 5.x | Linux 6.x |
|---|---|---|
| Minimum GCC version | GCC 4.9+ | GCC 11+ recommended (GCC 5.1 minimum) |
| LLVM/Clang support | Experimental | Fully supported — LLVM=1 make |
| Module signing | Optional | Required on many distro kernels; use CONFIG_MODULE_SIG=n for dev builds |
-Wmissing-attributes warnings |
Common with GCC 9+ | Fixed in kernel 6.x — no longer appears |
| BTF (BPF Type Format) | Not required | Required by some subsystems; needs pahole installed |
ARCH=arm64 for 64-bit ARM |
Same | Same — arm64 not aarch64 |
Installing pahole for Linux 6.x Module Builds
# Some Linux 6.x kernel configs need pahole for BTF generation
sudo apt install dwarves
# Verify
pahole --version
Best Practices for Cross-Compiling Kernel Modules
Never point KDIR at a host’s /lib/modules when cross-compiling.
Always use the actual kernel source tree for your target, configured and prepared with
make modules_prepare ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
file on your .ko before deployingIt takes one second and catches the wrong-architecture mistake before you waste time copying files to a board.
In a team, everyone must use the same toolchain version. Document the exact package version in your project’s README.
modinfo to inspect any unfamiliar .ko filemodinfo tells you the kernel version, author, license, parameters, and dependencies
without loading the module. Always inspect before loading on a live system.
Write your driver code in standard portable C, using only public kernel APIs. Never rely on internal kernel structures whose layout can change between versions.
Common Mistakes and Troubleshooting
| Mistake | Symptom | Fix |
|---|---|---|
Forgot ARCH=arm |
file .ko shows x86-64 |
Always pass both ARCH and CROSS_COMPILE |
Wrong KDIR |
vermagic mismatch on target |
Use the target’s exact kernel source tree |
| Kernel source not prepared | Build fails with missing header errors | Run make modules_prepare in the kernel source first |
Missing pahole on Linux 6.x |
Build fails mentioning BTF | sudo apt install dwarves |
| Module signing mismatch | insmod fails with “Required key not available” |
Disable CONFIG_MODULE_SIG_FORCE in dev kernel or sign the module |
Using ARCH=aarch64 |
Build system error: unknown arch | Use ARCH=arm64 for 64-bit ARM |
Key Takeaways
Frequently Asked Questions
No. You need to run at least make modules_prepare ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
in the kernel source tree. This generates the necessary headers and scripts without building the
full kernel image. If your board vendor provides a pre-configured kernel source, that is enough.
Yes. Cross-compilation depends only on having the right toolchain, not on the host architecture. You can even cross-compile on macOS with the right toolchain, though Linux hosts are far more common in embedded development.
Absolutely yes. Always use the vendor’s BSP kernel source when cross-compiling modules for that vendor’s board. The vendor’s kernel may have out-of-tree patches that change internal symbol layouts. Using mainline kernel source against a patched vendor kernel almost always produces a vermagic mismatch or subtle runtime bugs.
modinfo show vermagic but uname -r show a slightly different string?Extra suffixes like +, -dirty, or custom local version strings
(set via CONFIG_LOCALVERSION) are appended to the kernel version string. If
the build you are targeting was configured with CONFIG_LOCALVERSION="-myboard",
your module’s vermagic must include that suffix too. Make sure you are building
your module against the exact kernel source, not a clean upstream source.
Yes. Linux 6.x has excellent LLVM/Clang support. Use: make LLVM=1 ARCH=arm64 M=$(PWD) modules.
You still need the KDIR set to the target kernel source. The LLVM toolchain must include
the correct cross-compilation support (e.g. --target=aarch64-linux-gnu is
handled automatically when LLVM=1 is set in the kernel Makefile).
When you build a kernel module against a kernel source where make has not been
run to completion, Module.symvers may be empty or absent. This file contains
symbol version checksums for the kernel. Without it, the module will have no versioning
information for the symbols it imports. The module will still load if CONFIG_MODVERSIONS
is not set in your kernel config, but this warning is a signal to run at least
make modules_prepare properly.
No. If you are developing directly on an ARM board that has enough RAM, storage, and a full compiler installed (like a Raspberry Pi 5 or a Jetson), you can do native compilation directly on the board. Cross-compilation is for scenarios where the target is resource-constrained or does not have a development toolchain available.
insmod and modprobe?insmod loads a module by its full file path and does not resolve dependencies.
modprobe loads a module by name, automatically loads all its dependencies, and
requires the module to be installed in the proper /lib/modules/$(uname -r)/
hierarchy with depmod having been run. Use insmod for quick development
testing, modprobe for production.
Conclusion
Cross-compilation is a foundational skill in the free Linux kernel programming and free Linux device
drivers development journey. The key insight is deceptively simple: a kernel module is not just
C code — it is binary code that is tightly coupled to one specific kernel build, down to the
configuration options and local version suffix. The kernel enforces this through the
vermagic string and will silently reject any module that does not match perfectly.
Once you understand that contract, the rest follows naturally: always build your module against
the target’s kernel source, always verify the architecture with file, and always
use dmesg as your first diagnostic step when a load fails. In Linux 6.x the tooling
is mature and the error messages are clear — the kernel will tell you exactly what it expected
and what it got.
In the next lecture we will take this further and look at how to manage multiple kernel module builds in a Yocto-based embedded Linux project.
Free Linux Kernel Programming Course
All lectures are free, forever. No signup required.
Back to Course Index