Skip to content

Day 7 — Cross Compile Kernel Module

📌 What I did

  • Set up cross-compilation environment on Ubuntu 24.04 (WSL)
  • Downloaded Raspberry Pi kernel source (matching 6.12.47)
  • Applied running kernel config from Raspberry Pi
  • Prepared kernel build tree for external module
  • Built kernel module (hello.ko) on WSL
  • Deployed module to Raspberry Pi via scp
  • Successfully loaded module using insmod

⚠️ Issues Encountered

1. fixdep: Exec format error

/bin/sh: 1: scripts/basic/fixdep: Exec format error

Cause:

  • Kernel build tools were copied from Raspberry Pi (ARM)
  • Attempted to execute on WSL (x86)

Fix:

make prepare
make modules_prepare

👉 Rebuilt host tools for x86


2. kernelrelease mismatch

Initial result:

6.12.47-v8-16k+

Target:

6.12.47+rpt-rpi-2712

Cause:

  • CONFIG_LOCALVERSION mismatch
  • Dirty git tree adds +

Fix:

scripts/config --set-str LOCALVERSION "+rpt-rpi-2712"
scripts/config --disable LOCALVERSION_AUTO
make LOCALVERSION= kernelrelease


3. Module load failure (Invalid parameters)

insmod: ERROR: could not insert module hello.ko: Invalid parameters

Real cause from dmesg:

disagrees about version of symbol _printk
Unknown symbol _printk

Cause:

  • Symbol version mismatch
  • Module.symvers not aligned with target kernel

Fix:

  • Copied Module.symvers from Raspberry Pi
scp pi:/usr/src/linux-headers-6.12.47+rpt-rpi-2712/Module.symvers .

✅ Result

Successfully loaded module:

[ 6663.120808] Hello Kernel Module Loaded

Verified:

lsmod | grep hello

Unloaded:

sudo rmmod hello

🧠 Key Takeaways

  • Cross-compilation includes host tools + target binaries
  • Kernel version string must match exactly (uname -r)
  • LOCALVERSION affects module compatibility
  • + indicates dirty tree and can break matching
  • Module.symvers is required for symbol version alignment
  • vermagic matching is necessary but not sufficient

Module Setup & Cleanup Scripts

setup_mydev.sh

#!/bin/bash
#
# @file setup_mydev.sh
# @brief Load mydev kernel module and create device node
#

# Exit immediately if any command fails (except those explicitly handled)
set -e

MODULE="mydev"
DEVICE="/dev/mydev"

echo "=== Loading module ==="

# Try to remove existing module
# - '2>/dev/null' suppresses error output (e.g., module not loaded)
# - '|| true' ensures script continues even if rmmod fails
#   (important because set -e would otherwise stop execution)
sudo rmmod $MODULE 2>/dev/null || true

# Insert module
sudo insmod ./${MODULE}.ko

echo "=== Getting major number ==="

# Extract major number from /proc/devices
#
# Step-by-step:
# 1. grep $MODULE /proc/devices
#    → find the line containing "mydev"
#      e.g., "509 mydev"
#
# 2. awk '{print $1}'
#    → extract first column (major number)
#
# 3. $( ... )
#    → command substitution: store output into variable
MAJOR=$(grep $MODULE /proc/devices | awk '{print $1}')

# Check if MAJOR is empty
# -z means "string length is zero"
if [ -z "$MAJOR" ]; then
    echo "ERROR: Failed to get major number"
    exit 1
fi

echo "Major number: $MAJOR"

echo "=== Creating device node ==="

# If device node already exists, remove it
# This prevents mismatch if major number changed
if [ -e $DEVICE ]; then
    sudo rm -f $DEVICE
fi

# Create character device node
# Syntax: mknod <path> <type> <major> <minor>
# 'c' = character device
sudo mknod $DEVICE c $MAJOR 0

# Set permissions to rw-rw-rw-
# This allows non-root users to read/write the device (for testing)
sudo chmod 666 $DEVICE

echo "=== Device ready ==="
ls -l $DEVICE

echo "=== dmesg ==="

# Show last 10 kernel log lines
dmesg | tail -n 10

# End of File

cleanup_mydev.sh

#!/bin/bash
#
# @file cleanup_mydev.sh
# @brief Remove mydev kernel module and device node
#

# Exit immediately if any command fails
set -e

MODULE="mydev"
DEVICE="/dev/mydev"

echo "=== Removing module ==="

sudo rmmod $MODULE

echo "=== Removing device node ==="

# Check if device node exists
# -e means "file exists"
if [ -e $DEVICE ]; then
    sudo rm -f $DEVICE
fi

echo "=== Cleanup done ==="

# End of File

Test mydev using Python Script

test_mydev.py

#!/usr/bin/env python3
"""
@file test_mydev.py
@brief Test script for /dev/mydev character device

Usage:
    python3 test_mydev.py basic
    python3 test_mydev.py offset
    python3 test_mydev.py boundary
    python3 test_mydev.py busy
    python3 test_mydev.py stress
"""

import os
import sys
import time
import threading

DEV_PATH = "/dev/mydev"


def check_device():
    """Ensure device exists"""
    if not os.path.exists(DEV_PATH):
        print(f"ERROR: {DEV_PATH} not found")
        sys.exit(1)


# =========================================================
# Basic Test
# =========================================================
def test_basic():
    print("=== Basic Test ===")

    data = b"hello kernel"

    with open(DEV_PATH, "wb") as f:
        f.write(data)

    with open(DEV_PATH, "rb") as f:
        read_data = f.read()

    print(f"Write: {data}")
    print(f"Read : {read_data}")

    print("PASS" if read_data == data else "FAIL")


# =========================================================
# Offset Test
# =========================================================
def test_offset():
    print("=== Offset Test ===")

    data = b"abcdef"

    with open(DEV_PATH, "wb") as f:
        f.write(data)

    f = open(DEV_PATH, "rb")
    r1 = f.read(3)
    r2 = f.read(3)
    r3 = f.read(3)
    f.close()

    print(f"Read1: {r1}")
    print(f"Read2: {r2}")
    print(f"Read3: {r3}")

    if r1 == b"abc" and r2 == b"def" and r3 == b"":
        print("PASS")
    else:
        print("FAIL")


# =========================================================
# Boundary Test
# =========================================================
def test_boundary():
    print("=== Boundary Test ===")

    data = b"A" * 200  # larger than BUFFER_SIZE (128)

    with open(DEV_PATH, "wb") as f:
        f.write(data)

    with open(DEV_PATH, "rb") as f:
        read_data = f.read()

    print(f"Written: {len(data)} bytes")
    print(f"Read   : {len(read_data)} bytes")

    if len(read_data) <= 128:
        print("PASS (correct truncation)")
    else:
        print("FAIL (overflow detected)")


# =========================================================
# Busy Test
# =========================================================
def test_busy():
    print("=== Busy Test ===")

    fd1 = os.open(DEV_PATH, os.O_RDWR)
    print("fd1 opened")

    try:
        fd2 = os.open(DEV_PATH, os.O_RDWR)
        print("FAIL: second open should not succeed")
        os.close(fd2)
    except OSError as e:
        print(f"PASS: second open failed as expected ({e})")

    time.sleep(2)
    os.close(fd1)
    print("fd1 closed")


# =========================================================
# Stress Test
# =========================================================
def writer(stop_event, errors):
    while not stop_event.is_set():
        try:
            with open(DEV_PATH, "wb") as f:
                f.write(b"stress-data")
        except Exception:
            errors.append("write")


def reader(stop_event, errors):
    while not stop_event.is_set():
        try:
            with open(DEV_PATH, "rb") as f:
                f.read()
        except Exception:
            errors.append("read")


def test_stress():
    print("=== Stress Test (5 sec) ===")

    stop_event = threading.Event()
    errors = []

    threads = []

    for _ in range(2):
        threads.append(threading.Thread(target=writer, args=(stop_event, errors)))
        threads.append(threading.Thread(target=reader, args=(stop_event, errors)))

    for t in threads:
        t.start()

    time.sleep(5)
    stop_event.set()

    for t in threads:
        t.join()

    print("Stress test finished")

    if errors:
        print(f"Errors occurred: {len(errors)}")
    else:
        print("PASS (no userspace errors)")

    print("👉 Check dmesg for kernel issues")


# =========================================================
# Main
# =========================================================
def main():
    check_device()

    if len(sys.argv) != 2:
        print("Usage: python3 test_mydev.py [basic|offset|boundary|busy|stress]")
        sys.exit(1)

    cmd = sys.argv[1]

    if cmd == "basic":
        test_basic()
    elif cmd == "offset":
        test_offset()
    elif cmd == "boundary":
        test_boundary()
    elif cmd == "busy":
        test_busy()
    elif cmd == "stress":
        test_stress()
    else:
        print("Unknown command")


if __name__ == "__main__":
    main()

# End of File

test_run.sh

sh setup_mydev.sh

chmod +x test_mydev.py

python3 test_mydev.py basic
python3 test_mydev.py offset
python3 test_mydev.py boundary
python3 test_mydev.py busy
python3 test_mydev.py stress

sh cleanup_mydev.sh

🚀 Next Steps

  • Convert hello module to character device driver
  • Add module parameters
  • Learn modprobe and depmod
  • Use dmesg -w for real-time debugging