Cross-Compilation of Linux Kernel Modules for ARM

Cross-Compiling Linux Kernel Modules for ARM – Free Linux Kernel Programming Course

Cross-Compiling Linux Kernel Modules for ARM
Free Linux Kernel Programming Course — EmbeddedPathashala
Level
Intermediate
Kernel
Linux 6.x
Target
ARM 32/64-bit
Free
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 vermagic and 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.

Cross-Compilation: Host vs Target
HOST (x86-64 PC)

▶ Runs the cross-compiler
▶ Has the ARM kernel source
▶ Has the ARM sysroot / headers
▶ Produces .ko file

Example: 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
Tip: Always match the toolchain version to the one used to build your target kernel. A kernel built with GCC 12 and a module compiled with GCC 9 can produce subtle ABI mismatches even if the version strings look compatible. On Linux 6.x targets you generally want GCC 11 or later.

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
Note: In Linux 6.x, 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.
Build Output: What Each File Is
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.)
The vermagic String: What It Contains
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.

Diagnosing insmod Failure — Decision Flow
insmod fails
Invalid module format
Run
sudo dmesg | tail -5
Read the version magic lines
Two causes:
1. Wrong KDIR in Makefile
2. Target booted a different kernel than expected
Fix 1: Point KDIR to the kernel source that matches the target’s running kernel, then rebuild the module. Fix 2: Boot the target with the kernel that matches the vermagic already embedded in the module.

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.

Source vs Binary Compatibility
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 apt or dnf)
How DKMS Works
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
Note for Embedded Systems: DKMS is primarily a desktop/server tool. In typical embedded Linux products you control the kernel version precisely, so DKMS is not commonly used. Instead, your build system (Yocto, Buildroot, etc.) handles kernel module compilation as part of the overall image build. DKMS is more relevant if you are shipping a driver for general-purpose Linux desktops or servers.

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

✅ Always use a versioned kernel source tree

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-

✅ Run file on your .ko before deploying

It takes one second and catches the wrong-architecture mistake before you waste time copying files to a board.

✅ Keep your cross-toolchain and kernel source in version control or a documented location

In a team, everyone must use the same toolchain version. Document the exact package version in your project’s README.

✅ Use modinfo to inspect any unfamiliar .ko file

modinfo tells you the kernel version, author, license, parameters, and dependencies without loading the module. Always inspect before loading on a live system.

✅ Keep modules source-compatible, not binary-compatible

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

Cross-compilation = host builds, target runs
ARCH + CROSS_COMPILE are mandatory
KDIR must point to target kernel source
vermagic must match uname -r exactly
Source-compatible, not binary-compatible
dmesg is your best diagnostic tool
Linux 6.x needs GCC 11+ and possibly pahole

Frequently Asked Questions

Q1. Do I need to compile the entire ARM kernel before I can cross-compile a module?

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.

Q2. Can I cross-compile for ARM64 on an ARM32 host?

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.

Q3. My board vendor provides a BSP. Should I use that kernel source?

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.

Q4. Why does 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.

Q5. Can I use Clang instead of GCC for cross-compiling kernel modules in Linux 6.x?

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).

Q6. What is the Module.symvers warning about?

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.

Q7. Is cross-compilation needed for native ARM development boards?

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.

Q8. What is the difference between 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

Leave a Reply

Your email address will not be published. Required fields are marked *