comodore-iec-emu/_research/pi-kernel-module-install-verify-2026-06-18.md
2026-06-18 22:25:42 +02:00

1091 lines
38 KiB
Markdown

# Installing and Verifying a Precompiled Out-of-Tree Kernel Module on Raspberry Pi OS
> Research tier: deep dive · 2026-06-18
---
## Question
How do you deploy, load, and verify a precompiled `.ko` kernel module on a
Raspberry Pi Zero 2 W running 64-bit Raspberry Pi OS Bookworm (kernel 6.6.x,
`-v8` arm64 flavour)? The module (`iec_listener.ko`) is built by an
emulated-arm64 Docker host, creates `/dev/iec0`, accepts `address=4`, and
exposes an `IEC_IOC_SELFTEST` ioctl. An optional `iec-overlay.dtbo` reserves
GPIO pins.
---
## Summary
A precompiled arm64 module built against `linux-headers-rpi-v8` in an
emulated-arm64 Docker container will load cleanly on the matching Pi with
`sudo insmod ./iec_listener.ko address=4`, provided the vermagic string in the
`.ko` exactly matches the running kernel's vermagic (kernel release string +
`SMP preempt mod_unload modversions aarch64`). Stock Raspberry Pi OS Bookworm
does **not** enforce module signing (`CONFIG_MODULE_SIG_FORCE` is not set);
this is confirmed by checking the kernel config. The character device
`/dev/iec0` is created automatically by udev when the driver calls
`class_create`/`device_create` — no `mknod` is needed. The device-tree overlay
goes in `/boot/firmware/overlays/` (Bookworm path — **not** `/boot/overlays/`)
and is activated via a `dtoverlay=iec` line in `/boot/firmware/config.txt`. To
survive `apt` kernel upgrades, pin the kernel packages with `apt-mark hold`
immediately after first load.
---
## Notes on `docs/kernel-notes.md` — discrepancies and gaps found
| Issue | Location in notes | Correct value for Bookworm |
|-------|------------------|---------------------------|
| `sudo cp iec-overlay.dtbo /boot/overlays/` | §"Build & deploy" | Must be `/boot/firmware/overlays/` on Bookworm |
| `echo "dtoverlay=iec" \| sudo tee -a /boot/config.txt` | (derived from prior research doc §4.1) | File is `/boot/firmware/config.txt` on Bookworm |
| `/boot/cmdline.txt` mentioned for `isolcpus` | §1.4 of prior research | File is `/boot/firmware/cmdline.txt` on Bookworm |
| Module signing stated as "not required" but not verified | §Decisions table | Correct — confirmed below in §6 with verification commands |
| No `depmod -a` step after permanent install | §"Build & deploy" | Required before `modprobe` works |
---
## Findings
---
### 1. Getting the `.ko` onto the Pi
Run **on the build host** (not the Pi):
```bash
# Preferred: rsync preserves timestamps, skips unchanged
rsync -avz --progress kernel/iec_listener.ko pi@raspberrypi.local:/home/pi/iec/
# Alternative: plain scp
scp kernel/iec_listener.ko pi@raspberrypi.local:/home/pi/iec/
# If you also built the overlay:
scp dts/iec-overlay.dtbo pi@raspberrypi.local:/home/pi/iec/
```
Stage under `~/iec/` (or any writable directory). Do **not** copy directly to
`/lib/modules/` yet — test with `insmod` from the staging directory first.
---
### 2. Pre-load verification: does the module match the running kernel?
This step prevents a wasted `insmod` attempt. Run **on the Pi**.
#### 2.1 Read the module's vermagic
```bash
modinfo ~/iec/iec_listener.ko
```
Expected output (values vary with exact kernel point release):
```
filename: /home/pi/iec/iec_listener.ko
description: Commodore IEC bus listener
author: Christian Werner
license: GPL
version: 0.1
srcversion: A3F9C1D8E4B2071F5C6D890
depends:
name: iec_listener
vermagic: 6.6.51+rpt-rpi-v8 SMP preempt mod_unload modversions aarch64
parm: address:IEC device address (int)
```
The **`vermagic`** line is the critical field. [1][2]
#### 2.2 Read the running kernel's vermagic string
```bash
uname -r
# e.g.: 6.6.51+rpt-rpi-v8
# The kernel stores its own vermagic in any already-loaded module.
# The fastest cross-check: read one in-tree module.
modinfo /lib/modules/$(uname -r)/kernel/drivers/char/random.ko | grep vermagic
# OR
modinfo $(find /lib/modules/$(uname -r)/ -name "*.ko.xz" | head -1) | grep vermagic
```
Alternatively, check `Module.symvers` which is produced by the same kernel
build and encodes the same release string:
```bash
head -1 /lib/modules/$(uname -r)/build/Module.symvers
# or from the installed headers:
head -1 /usr/src/linux-headers-$(uname -r)/Module.symvers
```
#### 2.3 Fields that must match exactly
| Field | Example value | Where to find |
|-------|---------------|---------------|
| Kernel release string | `6.6.51+rpt-rpi-v8` | `uname -r` |
| `SMP` | always present on Pi Zero 2 W | vermagic |
| `preempt` | present (Bookworm stock) | vermagic |
| `mod_unload` | present | vermagic |
| `modversions` | present (CRC checking enabled) | vermagic |
| `aarch64` | present for 64-bit arm64 `-v8` | vermagic |
A **match** looks like:
```
# uname -r output:
6.6.51+rpt-rpi-v8
# modinfo vermagic line (must be identical):
vermagic: 6.6.51+rpt-rpi-v8 SMP preempt mod_unload modversions aarch64
```
#### 2.4 `CONFIG_MODVERSIONS` and symbol CRC checking
The `modversions` word in the vermagic string signals that
`CONFIG_MODVERSIONS=y` is set in this kernel. This adds per-symbol CRC
checksums (stored in `Module.symvers`) to every exported kernel symbol.
When insmod loads your module it checks the CRC of every symbol the module
imports against the kernel's own CRC table. If **any** CRC diverges — even if
the release string matches — you get `Unknown symbol in module (err -22)`.
This means: building against the correct *headers package* is not sufficient
if those headers were not produced by the same kernel build (i.e., the same
`Module.symvers`). The Docker build strategy (install `linux-headers-rpi-v8`
via apt inside the container) works **only** when the apt archive serves the
same kernel version that is running on the Pi. That is precisely why
`kernel-notes.md` recommends keeping the Pi on the latest release and building
with the default (latest) headers. [2][3]
#### 2.5 Also check the ELF architecture
```bash
file ~/iec/iec_listener.ko
# Expected:
# iec_listener.ko: ELF 64-bit LSB relocatable, ARM aarch64, version 1 (SYSV), not stripped
```
Any `32-bit` or `ARM, EABI5` in the output means the module was built with the
wrong headers (e.g., `-v7` armhf instead of `-v8` arm64) and will fail with
`Exec format error`. [4]
---
### 3. Loading the module
#### 3.1 `insmod` — use during bring-up
```bash
# On the Pi, from the staging directory:
sudo insmod ~/iec/iec_listener.ko address=4
```
What `insmod` does:
- Takes the **full path** to the `.ko` file.
- Passes it directly to the kernel via `finit_module(2)` or `init_module(2)`.
- Resolves no dependencies — fails hard if any `MODULE_IMPORT` symbol is
missing.
- Does **not** read `/etc/modprobe.d/`; parameters must be on the command line.
- Does **not** require the module to be installed under `/lib/modules/`.
**Use `insmod` during development and initial bring-up.** It is simple, direct,
and gives you the raw kernel error if something is wrong. [5]
#### 3.2 `modprobe` — use for production / permanent install
```bash
# Requires the module to be installed first (see §8):
sudo modprobe iec_listener address=4
```
What `modprobe` does:
- Looks up the module **by name** (not path) in
`/lib/modules/$(uname -r)/modules.dep` (built by `depmod`).
- Automatically loads any modules listed as dependencies in that dep file.
- Reads `/etc/modprobe.d/*.conf` for default options (so `address=4` can be
made permanent there rather than on the command line).
- If the module is not in the dep database → `FATAL: Module iec_listener not found`.
`modprobe` therefore **requires** two setup steps before it works for an
out-of-tree module:
1. Copy the `.ko` to `/lib/modules/$(uname -r)/extra/iec_listener.ko`
2. Run `sudo depmod -a`
**Use `modprobe` only after the permanent install steps in §8.** [5][6]
#### 3.3 Passing `address=4` in each case
```bash
# insmod — on the command line:
sudo insmod ~/iec/iec_listener.ko address=4
# modprobe — on the command line (temporary override):
sudo modprobe iec_listener address=4
# modprobe — permanent (persists across reboots; see §8):
echo "options iec_listener address=4" | sudo tee /etc/modprobe.d/iec_listener.conf
sudo modprobe iec_listener # now uses address=4 from the conf file
```
---
### 4. Verifying a successful load
Run all commands **on the Pi** immediately after `insmod`:
#### 4.1 `lsmod` — is the module listed?
```bash
lsmod | grep iec_listener
```
Expected:
```
iec_listener 24576 0
```
Columns: module name | size in bytes | use count (0 = no open file descriptors
or dependent modules). [7]
#### 4.2 `/sys/module/iec_listener/` — sysfs subtree
```bash
ls /sys/module/iec_listener/
# Expected directories/files: holders/ initstate parameters/ refcnt srcversion
# Confirm the address parameter was accepted:
cat /sys/module/iec_listener/parameters/address
# Expected: 4
# Module use count (should be 0 when no fd is open):
cat /sys/module/iec_listener/refcnt
# Expected: 0
```
The `parameters/` directory exposes every `module_param()` as a file whose
content reflects the runtime value. If `address` reads `4` the kernel accepted
the parameter correctly. [8]
#### 4.3 `dmesg` — read the init message
```bash
# Most recent kernel messages (shows init printk output):
dmesg | tail -20
# Or with timestamps since last boot:
sudo journalctl -k --since "1 minute ago"
# Filter to the module name:
dmesg | grep -i iec
```
Expected healthy output:
```
[ 123.456789] iec_listener: IEC listener v0.1 loaded, address=4
[ 123.456801] iec_listener: registered char device major=240 minor=0
[ 123.456815] iec_listener: /dev/iec0 created
```
Anything with `BUG:`, `WARNING:`, `kernel panic`, or `NULL pointer dereference`
in the module's output is a real problem — unload immediately with `sudo rmmod
iec_listener`. [5]
#### 4.4 `/dev/iec0` — does the device node exist?
```bash
ls -l /dev/iec0
```
Expected:
```
crw------- 1 root root 240, 0 Jun 18 14:35 /dev/iec0
```
Fields: `c` = character device, major 240 (dynamically allocated; your number
may differ), minor 0.
**How the node appears automatically:** When the module calls `class_create()`
followed by `device_create()`, the kernel creates a sysfs entry at
`/sys/class/iec/iec0/dev` containing the `major:minor` pair. The `udev` daemon
(running as a service — `systemctl status udev`) watches sysfs for new entries
and creates the `/dev/iec0` character device node automatically. No manual
`mknod` is needed on any modern Linux with udev. [9][10]
**Verify via sysfs:**
```bash
cat /sys/class/iec/iec0/dev # prints e.g. "240:0"
ls -la /sys/class/iec/iec0/ # shows the full sysfs entry
```
**Permissions:** udev defaults to `root:root` with mode `0600` (owner read/write
only). During bring-up this is fine — run tests as root. For non-root access,
add a udev rule:
```bash
# /etc/udev/rules.d/99-iec.rules
KERNEL=="iec0", GROUP="dialout", MODE="0660"
```
Then reload: `sudo udevadm control --reload-rules && sudo udevadm trigger` [10]
**Check major/minor with `stat`:**
```bash
stat /dev/iec0
# File: /dev/iec0
# Size: 0 Blocks: 0 IO Block: 4096 character special file
# Device: 5h/5d Inode: 1357 Links: 1 Device type: f0,0
# (f0 hex = 240 decimal = major number)
```
---
### 5. The "is it actually working" check short of attaching the C64
#### 5.1 Confirm the device is openable
```bash
sudo python3 -c "
import os
fd = os.open('/dev/iec0', os.O_RDONLY | os.O_NONBLOCK)
print('open OK, fd =', fd)
os.close(fd)
print('close OK')
"
```
Expected: `open OK, fd = 3` then `close OK`. If you get `PermissionError` run as
root (`sudo`) or fix the udev rule in §4.4. If you get `No such device`, the
module's `cdev_add()` failed — check `dmesg`. [9]
#### 5.2 Issue the `IEC_IOC_SELFTEST` ioctl
The ioctl number is defined in the module header as
`_IO(IEC_IOC_MAGIC, IEC_IOC_NR_SELFTEST)`. Substitute the values from
`kernel/iec_listener.h` (e.g. magic `'I'`=0x49, nr 0 → ioctl number 0x4900):
```python
#!/usr/bin/env python3
"""Quick userspace selftest for iec_listener.ko — run on the Pi as root."""
import fcntl, os, struct
# Reconstruct the ioctl number from the kernel header:
# _IO(type, nr) = ((type) << 8) | (nr)
# Example: IEC_IOC_MAGIC = ord('I') = 0x49, IEC_IOC_NR_SELFTEST = 0
IEC_IOC_SELFTEST = (ord('I') << 8) | 0 # = 0x4900 (adjust to match header)
fd = os.open('/dev/iec0', os.O_RDWR)
try:
ret = fcntl.ioctl(fd, IEC_IOC_SELFTEST, 0)
print(f"SELFTEST ioctl returned {ret}{'PASS' if ret == 0 else 'FAIL'}")
except OSError as e:
print(f"ioctl failed: {e}")
finally:
os.close(fd)
```
After the ioctl, check `dmesg` again:
```bash
dmesg | tail -5
```
Healthy output example:
```
[ 145.882011] iec_listener: SELFTEST: driving DATA low (GPIO 18)
[ 145.882025] iec_listener: SELFTEST: reading DATA back → 0 (PASS)
[ 145.882031] iec_listener: SELFTEST: releasing DATA
[ 145.882038] iec_listener: SELFTEST: reading DATA back → 1 (PASS)
[ 145.882041] iec_listener: SELFTEST passed
```
If `SELFTEST: reading DATA back → 0 expected 1 FAIL` appears, the GPIO pin
wiring has an issue — the bus is stuck low. This is the exact test to run
before connecting the C64 (as recommended in `kernel-notes.md` §"Level-shifter
/ DATA-sensing note"). [11]
**C snippet alternative** (if Python is not available on the Pi):
```c
/* selftest.c — compile with: gcc -o selftest selftest.c */
#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#define IEC_IOC_SELFTEST 0x4900 /* adjust to match iec_listener.h */
int main(void) {
int fd = open("/dev/iec0", O_RDWR);
if (fd < 0) { perror("open"); return 1; }
int r = ioctl(fd, IEC_IOC_SELFTEST, 0);
printf("ioctl returned %d\n", r);
close(fd);
return r ? 1 : 0;
}
```
---
### 6. Troubleshooting table
#### Error 1: `Invalid module format` — vermagic mismatch
**insmod output:**
```
insmod: ERROR: could not insert module ./iec_listener.ko: Invalid module format
```
**dmesg (the diagnostic detail):**
```
[ 124.001] iec_listener: version magic '6.6.47+rpt-rpi-v8 SMP preempt mod_unload modversions aarch64'
should be '6.6.51+rpt-rpi-v8 SMP preempt mod_unload modversions aarch64'
```
**What it means:** The module was compiled against headers for kernel
`6.6.47+rpt-rpi-v8` but the Pi is now running `6.6.51+rpt-rpi-v8`. A kernel
upgrade happened after the module was built.
**Fix:**
```bash
# On the Pi — check current kernel:
uname -r
# On the build host — rebuild with matching headers:
KERNEL_VERSION=<exact-version-string> ./build-in-docker.sh
# Or: keep Pi current (apt full-upgrade) then rebuild with default headers.
```
The vermagic comparison is **byte-for-byte** including the `+rpt` suffix, any
`+` git-dirty marker, and the SMP/preempt/modversions flags. Even a missing or
extra `+` character fails the check. [1][2]
---
#### Error 2: `Exec format error` — wrong ELF architecture
**insmod output:**
```
insmod: ERROR: could not insert module ./iec_listener.ko: Exec format error
```
**dmesg:**
```
[ 125.002] iec_listener: Invalid architecture in ELF header (value 40, expected 183)
```
(`40` = `EM_ARM` 32-bit; `183` = `EM_AARCH64` 64-bit)
**What it means:** The `.ko` was compiled for 32-bit ARM (armhf, using
`linux-headers-rpi-v7` or an x86 host without the correct cross-compile
toolchain).
**Diagnosis:**
```bash
file ~/iec/iec_listener.ko
# Wrong: ELF 32-bit LSB relocatable, ARM, EABI5 ...
# Correct: ELF 64-bit LSB relocatable, ARM aarch64 ...
readelf -h ~/iec/iec_listener.ko | grep Machine
# Wrong: Machine: ARM
# Correct: Machine: AArch64
```
**Fix:** Rebuild using `linux-headers-rpi-v8` (the `-v8` suffix is the 64-bit
arm64 flavour) inside the emulated-arm64 container. Confirm `HEADERS_PKG` in
the build script is `linux-headers-rpi-v8`, not `-v7` or `-v7l`. [4][12]
---
#### Error 3: `Unknown symbol in module` — symbol CRC mismatch
**insmod output:**
```
insmod: ERROR: could not insert module ./iec_listener.ko: Unknown symbol in module
```
**dmesg:**
```
[ 126.003] iec_listener: Unknown symbol kfifo_alloc (err -22)
[ 126.003] iec_listener: Unknown symbol class_create (err 0)
```
`err 0` = symbol not found at all; `err -22` = symbol exists but CRC mismatch
(`EINVAL`). [13]
**What it means:** The module's `Module.symvers` (baked in at build time) has
different CRC values than the running kernel. Most common cause: building
against headers from a *different build* of the same kernel version (e.g., the
headers were from a kernel re-compiled with different `CONFIG_*` options than
the official Pi kernel).
**Diagnosis:**
```bash
# Find the offending symbols:
dmesg | grep "Unknown symbol"
# Check if the symbol exists in the running kernel:
grep kfifo_alloc /proc/kallsyms | head -3
# No output → symbol genuinely missing from this kernel
# Output → symbol exists but CRC differs (build mismatch)
```
**Fix:** Ensure the Docker build uses headers installed from the **same apt
pool** that produced the running Pi kernel. If the Pi's kernel came from
`apt full-upgrade`, the container's `linux-headers-rpi-v8` from the same
Bookworm repo will have matching symvers. Avoid using `rpi-source` or manually
downloaded headers unless you also have the `Module.symvers` from that exact
kernel build. [3][13]
---
#### Error 4: `Required key not available` / `Key was rejected by service` — module signing enforced
**insmod output:**
```
insmod: ERROR: could not insert module ./iec_listener.ko: Required key not available
```
or
```
insmod: ERROR: could not insert module ./iec_listener.ko: Key was rejected by service
```
**This should NOT happen on stock Raspberry Pi OS Bookworm.** The RPi kernel
does not set `CONFIG_MODULE_SIG_FORCE=y` and the Raspberry Pi does not use UEFI
Secure Boot by default. Secure Boot on RPi requires explicit OTP key burning —
it is opt-in and off by default.
**Verify that signing is not enforced (run on the Pi):**
```bash
# Method 1: check the running kernel's config (most reliable)
# The stock RPi kernel ships with CONFIG_IKCONFIG=y, so /proc/config.gz exists:
sudo modprobe configs 2>/dev/null; zcat /proc/config.gz | grep -E "CONFIG_MODULE_SIG|CONFIG_SECURITY_LOCKDOWN"
```
Expected output on stock Bookworm:
```
CONFIG_MODULE_SIG=y
# CONFIG_MODULE_SIG_FORCE is not set
# CONFIG_MODULE_SIG_ALL is not set
# CONFIG_SECURITY_LOCKDOWN_LSM is not set
```
`CONFIG_MODULE_SIG=y` alone means the signing *infrastructure* is compiled in
(the kernel *can* check signatures) but `CONFIG_MODULE_SIG_FORCE` being absent
means unsigned modules are **permitted** (kernel taints itself with `E` but
loads the module). [14][15]
```bash
# Method 2: check the lockdown sysfs node (only present if LSM compiled in)
cat /sys/kernel/security/lockdown 2>/dev/null
# If the file does not exist → lockdown LSM is not compiled in → safe.
# If it exists: "none" = off; "integrity" or "confidentiality" = modules blocked.
# Method 3: check dmesg for lockdown messages
dmesg | grep -i lockdown
# On stock RPi: no output
# Method 4: check kernel cmdline for enforcement override
cat /proc/cmdline | grep -o "module.sig_enforce=[01]"
# No output → not set → default is permissive
```
**If you somehow see this error** (e.g., on a custom Pi OS build with
`CONFIG_MODULE_SIG_FORCE=y`), you must sign the module with a MOK key and
enroll it via `mokutil` — this is out of scope for the standard Pi setup. [14]
---
#### Error 5: Module taint flags — what is expected vs. a problem
After loading an out-of-tree unsigned module, the kernel taints itself:
```bash
cat /proc/sys/kernel/tainted
# Typical value: 12288 = 4096 (O) + 8192 (E) = out-of-tree + unsigned
```
**Taint bit decoding:**
| Bit | Value | Flag | Meaning | Expected for our module? |
|-----|-------|------|---------|--------------------------|
| 12 | 4096 | O | Out-of-tree module loaded | **YES — normal** |
| 13 | 8192 | E | Unsigned module | **YES — normal on stock RPi OS** |
| 0 | 1 | P | Proprietary (non-GPL) module | No — we use `MODULE_LICENSE("GPL")` |
| 1 | 2 | F | Module force-loaded | No — bad, means vermagic was overridden |
A `tainted` value of `12288` is **entirely expected and benign** for our
use case. [16]
```bash
# Read taint in human-readable form from dmesg:
dmesg | grep -i taint
# Expected: "iec_listener: loading out-of-tree module taints kernel."
# Or: "iec_listener: module license 'GPL' taints kernel."
# (The second message appears when CONFIG_MODULE_SIG_FORCE is set, not our case.)
```
**A taint value of 2 (F flag) means `insmod --force` or `modprobe --force`
was used** — this overrides the vermagic check and can cause silent memory
corruption. Never use `--force` except as a last-resort diagnostic step. [16]
---
#### Error 6: `Operation not permitted`
**insmod output:**
```
insmod: ERROR: could not insert module ./iec_listener.ko: Operation not permitted
```
**What it means (several sub-cases):**
a) You forgot `sudo`. Always run `insmod` / `rmmod` as root.
b) A secureboot-related lockdown is active (see Error 4).
c) The module tries to map a physical address (our `ioremap(0x3F200000, ...)`)
and the kernel has `CONFIG_STRICT_DEVMEM=y` plus the request overlaps a
restricted region. This is unlikely on stock RPi but worth checking:
```bash
dmesg | grep -E "ioremap|DEVMEM|mem_encrypt"
```
---
#### Error 7: `Device or resource busy`
**insmod output:**
```
insmod: ERROR: could not insert module ./iec_listener.ko: Device or resource busy
```
**dmesg:**
```
[ 128.007] iec_listener: GPIO 17 request failed: -16 (EBUSY)
```
**What it means:** One of the GPIOs the module tries to claim with
`gpio_request()` or `devm_gpiod_get()` is already owned by another driver
(e.g., a previous unclean unload left the GPIO locked, or a DT overlay already
claimed it through pinctrl).
**Fix:**
```bash
# See which GPIOs are claimed:
cat /sys/kernel/debug/gpio # requires root / debugfs mounted
# or:
sudo ls /sys/class/gpio/
# If a previous load left GPIO locked, reboot to clear the state.
# If the DT overlay is claiming the pins, ensure the overlay's GPIO
# reservation matches exactly what the module requests.
```
---
#### Error 8: `rmmod: ERROR: Module iec_listener is in use`
**rmmod output:**
```
rmmod: ERROR: Module iec_listener is in use.
```
**What it means:** `refcnt` is non-zero. Either a process has `/dev/iec0` open
(e.g., your Python test script didn't close the fd) or another module depends
on `iec_listener`.
**Fix:**
```bash
# See who holds references:
lsmod | grep iec_listener
# Third column > 0 means it has dependents or open file descriptors.
# Find open file descriptors:
sudo lsof /dev/iec0
# Shows the process(es) with the device open.
# Kill or close the process, then retry rmmod.
sudo kill <PID>
sudo rmmod iec_listener
```
---
### 7. Installing the optional device-tree overlay
The `iec-overlay.dtbo` reserves GPIO pins (ATN, CLK, DATA, RESET) via the
kernel pinctrl subsystem, preventing other drivers from claiming them.
#### 7.1 Where the file goes on Bookworm
```bash
# ON THE PI — copy to the Bookworm overlay directory:
sudo cp ~/iec/iec-overlay.dtbo /boot/firmware/overlays/iec.dtbo
```
**Bookworm path: `/boot/firmware/overlays/`** — not `/boot/overlays/`.
In Bullseye and earlier the path was `/boot/overlays/`. On fresh Bookworm
installs `/boot/overlays/` may be a symlink or may not exist at all. Always
use `/boot/firmware/overlays/` on Bookworm. [17][18]
#### 7.2 Activate in `config.txt`
```bash
# Edit the Bookworm config file:
echo "dtoverlay=iec" | sudo tee -a /boot/firmware/config.txt
# Verify it was added:
grep dtoverlay /boot/firmware/config.txt
```
The firmware strips the `.dtbo` extension automatically — `dtoverlay=iec` loads
`/boot/firmware/overlays/iec.dtbo`. [17]
#### 7.3 Reboot required
```bash
sudo reboot
```
Device-tree overlays are applied by the VideoCore firmware during boot, before
the kernel starts. They **cannot** be applied without a reboot via config.txt.
(The `dtoverlay` runtime command works for some overlays but is unreliable for
GPIO pinctrl fragments.) [18]
#### 7.4 Verify the overlay loaded after reboot
```bash
# Method 1: dtoverlay list (shows overlays loaded by the firmware at boot)
sudo dtoverlay -l
# Expected:
# Overlays (in load order):
# 0: iec
# NOTE: dtoverlay -l only shows overlays loaded via config.txt at boot.
# It does NOT show overlays applied at runtime via the dtoverlay command.
# Method 2: inspect the live device tree for our GPIO reservation node
ls /proc/device-tree/
# Look for a node matching the overlay name or the GPIO label:
find /proc/device-tree/ -name "*iec*" 2>/dev/null
# Method 3: check GPIO allocation after module load
sudo cat /sys/kernel/debug/gpio | grep -A 3 "iec"
# Expected lines like:
# gpio-2 (iec_atn ) in lo
# Method 4: firmware log (available on older RPi firmware builds)
sudo vcdbg log msg 2>/dev/null | grep -i overlay
# On Bookworm with recent firmware this command may not be available.
# Method 5: dmesg for pinctrl messages on module load
dmesg | grep -E "pinctrl|iec_pins"
```
If `dtoverlay -l` shows no overlays and `dmesg` shows GPIO claim errors, the
overlay did not load. Check that the `.dtbo` file is present in
`/boot/firmware/overlays/` and that the `dtoverlay=iec` line is in
`/boot/firmware/config.txt` (not `/boot/config.txt`). [17][19]
---
### 8. Persistence across reboot — permanent install
Use this after the module is confirmed working via `insmod`.
#### 8.1 Install the `.ko` into the modules tree
```bash
# Standard out-of-tree location:
KVER=$(uname -r)
sudo mkdir -p /lib/modules/${KVER}/extra/
sudo cp ~/iec/iec_listener.ko /lib/modules/${KVER}/extra/
# Regenerate the module dependency database:
sudo depmod -a
```
`depmod -a` scans all `.ko` files under `/lib/modules/$(uname -r)/` and writes
`modules.dep`, `modules.alias`, and related files. Without this step, `modprobe
iec_listener` fails with `FATAL: Module iec_listener not found`. [5][6]
#### 8.2 Verify `modprobe` can now find it
```bash
modinfo iec_listener
# Should print the same output as modinfo on the .ko file path.
# If it says "modinfo: ERROR: Module iec_listener not found" → depmod -a was not run
# or the file wasn't copied to the right directory.
```
#### 8.3 Auto-load at boot via `modules-load.d`
```bash
echo "iec_listener" | sudo tee /etc/modules-load.d/iec_listener.conf
```
`systemd-modules-load.service` reads files under `/etc/modules-load.d/` at
boot and calls `modprobe` for each listed module name. [5]
#### 8.4 Supply `address=4` permanently via `modprobe.d`
```bash
echo "options iec_listener address=4" | sudo tee /etc/modprobe.d/iec_listener.conf
```
This file is read by `modprobe` (and by `systemd-modules-load`) whenever the
module is loaded, eliminating the need to pass `address=4` on the command line. [5][6]
#### 8.5 Verify after a reboot
```bash
sudo reboot
# ... after reboot:
lsmod | grep iec_listener
# Expected: iec_listener 24576 0
cat /sys/module/iec_listener/parameters/address
# Expected: 4
ls -l /dev/iec0
# Expected: crw------- 1 root root 240, 0 ...
dmesg | grep iec_listener
# Expected: init message printed during boot
```
---
### 9. Surviving `apt` kernel upgrades
#### 9.1 Why a kernel upgrade breaks a manually-installed module
When `sudo apt full-upgrade` installs a new `raspberrypi-kernel` package:
- A new kernel image (e.g., `6.6.62+rpt-rpi-v8`) replaces the old one in
`/boot/firmware/`.
- A new `/lib/modules/6.6.62+rpt-rpi-v8/` tree is created.
- Your module file lives in `/lib/modules/6.6.51+rpt-rpi-v8/extra/` — it is
**not moved or rebuilt**.
- On next boot the new kernel runs, `systemd-modules-load` calls `modprobe
iec_listener`, modprobe looks in `/lib/modules/6.6.62+rpt-rpi-v8/` — finds
nothing — fails silently (or with a log message).
- Even if you copy the `.ko` manually, the vermagic mismatch causes `Invalid
module format`.
#### 9.2 Detecting that the kernel moved out from under the module
```bash
# After a reboot where the module failed to load:
uname -r
# e.g.: 6.6.62+rpt-rpi-v8
ls /lib/modules/$(uname -r)/extra/
# "No such file or directory" → module was installed for a different kernel version.
dmesg | grep "iec_listener"
# "FATAL: Module iec_listener not found" or "Invalid module format" in systemd log.
sudo journalctl -b | grep "Failed to insert module"
```
#### 9.3 Pinning the kernel with `apt-mark hold`
```bash
sudo apt-mark hold raspberrypi-kernel raspberrypi-kernel-headers raspberrypi-bootloader
```
**Record the pinned version in `kernel-notes.md`:**
```bash
dpkg -l raspberrypi-kernel | awk 'NR==5{print $2, $3}'
# e.g.: raspberrypi-kernel 1:6.6.51-1+rpt3
```
To unhold (when you deliberately want to upgrade and rebuild):
```bash
sudo apt-mark unhold raspberrypi-kernel raspberrypi-kernel-headers raspberrypi-bootloader
sudo apt full-upgrade
# → rebuild the module against the new headers
# → re-run §8 (copy + depmod)
sudo apt-mark hold raspberrypi-kernel raspberrypi-kernel-headers raspberrypi-bootloader
```
**Check current hold status:**
```bash
apt-mark showhold
```
#### 9.4 DKMS (for completeness — out of scope for our project)
DKMS (Dynamic Kernel Module Support) automates the rebuild-on-upgrade cycle.
It requires the *source code* and a `dkms.conf` on the Pi itself. Since our
build strategy uses Docker on an x86 host, DKMS is not applicable. Mention it
only as a pointer if the project ever shifts to building on the Pi directly. [20]
---
### 10. Clean unload / reload during iteration
**The reload cycle during development:**
```bash
# Step 1: close any open file descriptors (kill test scripts, close /dev/iec0)
sudo lsof /dev/iec0
# Step 2: unload
sudo rmmod iec_listener
# Step 3: confirm GPIO/device cleanup in dmesg
dmesg | tail -10
# Expected:
# [ 200.112] iec_listener: freeing IRQs
# [ 200.113] iec_listener: DATA line released (GPIO 18 low)
# [ 200.114] iec_listener: /dev/iec0 destroyed
# [ 200.115] iec_listener: unloaded
# Step 4: confirm /dev/iec0 is gone
ls /dev/iec0
# ls: cannot access '/dev/iec0': No such file or directory
# Step 5: reload with new parameters or updated binary
sudo insmod ~/iec/iec_listener.ko address=4
# Step 6: verify
lsmod | grep iec_listener && ls -l /dev/iec0 && dmesg | tail -5
```
**If Step 2 hangs ("Module iec_listener is in use"):**
```bash
# Find the holder:
sudo lsof /dev/iec0
# Kill it:
sudo kill -9 <PID>
# Retry:
sudo rmmod iec_listener
```
**CRITICAL — what the module's `exit` function must do** (already correct in
`iec_exit` per `kernel-notes.md`, verifying here):
1. `free_irq(irq_atn, NULL)` — disarm ATN interrupt first
2. `free_irq(irq_clk, NULL)` — disarm CLK interrupt
3. **Release DATA line** — `gpiod_set_value(gd_data_out, 0)` — **must happen or
the IEC bus is left stuck LOW**. A stuck DATA line will prevent the C64 from
communicating with any device.
4. `iounmap(gpio_regs)` — unmap BCM register block
5. `device_destroy()` + `class_destroy()` + `cdev_del()` — tear down the
character device (causes udev to remove `/dev/iec0` automatically)
6. `unregister_chrdev_region()` — free the major/minor allocation [11]
---
## Open questions / gaps
1. **`/proc/config.gz` availability on stock Bookworm**: The `CONFIG_IKCONFIG_PROC`
option makes `/proc/config.gz` available after `modprobe configs`. Whether
the stock RPi OS 6.6.x kernel has `CONFIG_IKCONFIG=y` is not confirmed by
a live system check — only by inference from forum posts. The fallback is
`cat /boot/config-$(uname -r)` which may also not exist on RPi. Confirm on
first boot by running both commands.
2. **`dtoverlay -l` behaviour on Bookworm**: As noted in §7.4, boot-time overlays
from `config.txt` may or may not appear in `dtoverlay -l` output depending
on the firmware version. The reliable check is `/proc/device-tree/` inspection
or `dmesg` GPIO messages after module load.
3. **udev permissions for `/dev/iec0`**: Default `0600 root:root` permissions
require `sudo` for all userspace access. The udev rule in §4.4 broadens this
to `0660 root:dialout`. Whether the `pi` user is in the `dialout` group on
a fresh Bookworm install needs to be confirmed: `groups pi | grep dialout`.
4. **Module signing config on the exact running Pi**: The analysis is based on
forum reports and the known default RPi kernel defconfig. The commands in
§6 Error 4 (`zcat /proc/config.gz | grep CONFIG_MODULE_SIG`) should be run
on first boot to confirm. If `CONFIG_IKCONFIG` is not set, check
`/boot/config-$(uname -r)` — this path is used on Debian-family kernels
(the RPi may or may not ship it; community evidence suggests it does not
on stock installs, but `/proc/config.gz` via `modprobe configs` is available).
5. **`apt full-upgrade` and the `linux-headers-rpi-v8` meta-package**: The
`linux-headers-rpi-v8` package is a meta-package that always pulls the latest
headers. Pinning the kernel without also pinning the headers meta-package
could result in headers that don't match the pinned kernel. The `apt-mark
hold` command in §9.3 covers both.
---
## Sources
[1] Gateworks — Linux Kernel Modules (vermagic, modinfo, loading) — https://trac.gateworks.com/wiki/linux/kernel/modules (accessed 2026-06-18)
[2] Raspberry Pi Forums — "Invalid module format when running a cross-compiled linux kernel module" — https://forums.raspberrypi.com/viewtopic.php?t=369617 (accessed 2026-06-18)
[3] Linux Kernel Docs — Building External Modules (Module.symvers, MODVERSIONS) — https://docs.kernel.org/kbuild/modules.html (accessed 2026-06-18)
[4] Raspberry Pi Forums — "After apt upgrade my 32-bit OS runs on a 64-bit kernel and now I can't compile an external module" — https://forums.raspberrypi.com/viewtopic.php?t=349070 (accessed 2026-06-18)
[5] ArchWiki — Kernel module (insmod, modprobe, /etc/modules-load.d/, /etc/modprobe.d/) — https://wiki.archlinux.org/title/Kernel_module (accessed 2026-06-18)
[6] Red Hat Docs — Setting Module Parameters (/etc/modprobe.d/, depmod) — https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/6/html/deployment_guide/sec-setting_module_parameters (accessed 2026-06-18)
[7] Linuxize — lsmod command — https://linuxize.com/post/lsmod-command-in-linux/ (accessed 2026-06-18)
[8] Linux Kernel ABI — /sys/module structure — https://www.kernel.org/doc/Documentation/ABI/stable/sysfs-module (accessed 2026-06-18)
[9] embetronicx — Device File Creation for Character Drivers (class_create / device_create / udev) — https://embetronicx.com/tutorials/linux/device-drivers/device-file-creation-for-character-drivers/ (accessed 2026-06-18)
[10] GitHub Gist strezh — Automatic /dev file creation when driver module is loaded — https://gist.github.com/strezh/b01fcd50875c214e510a81c6aa6d2a2a (accessed 2026-06-18)
[11] This project — docs/kernel-notes.md and _plans/poc-listener-printer-PLAN.md (accessed 2026-06-18)
[12] Zak's Electronics Blog — RPi compiling a module for the 64-bit kernel — https://blog.zakkemble.net/rpi-compiling-a-module-for-the-64-bit-kernel/ (accessed 2026-06-18)
[13] linuxvox.com — How to Fix 'Unknown Symbol in Module' Error — https://linuxvox.com/blog/unknown-symbol-in-while-loading-a-kernel-module/ (accessed 2026-06-18)
[14] Linux Kernel Docs — Kernel module signing facility — https://www.kernel.org/doc/html/v4.15/admin-guide/module-signing.html (accessed 2026-06-18)
[15] Raspberry Pi Forums — "How to lockdown kernel / Disable kernel rewrite on RPi 4" — https://forums.raspberrypi.com/viewtopic.php?t=360877 (accessed 2026-06-18)
[16] Linux Kernel Docs — Tainted Kernels (taint flag table) — https://docs.kernel.org/admin-guide/tainted-kernels.html (accessed 2026-06-18)
[17] Raspberry Pi Documentation — config.txt: dtoverlay directive, overlay path on Bookworm — https://www.raspberrypi.com/documentation/computers/config_txt.html (accessed 2026-06-18)
[18] Bootlin Blog — Enabling new hardware on Raspberry Pi with Device Tree Overlays — https://bootlin.com/blog/enabling-new-hardware-on-raspberry-pi-with-device-tree-overlays/ (accessed 2026-06-18)
[19] Raspberry Pi Forums — "Bookworm - Device tree overlays not loading" — https://forums.raspberrypi.com/viewtopic.php?t=367942 (accessed 2026-06-18)
[20] Flogistoni/raspbiec — instdrv.sh installation script (insmod + chmod pattern) — https://github.com/Flogistoni/raspbiec/blob/development/instdrv.sh (accessed 2026-06-18)
[21] yeri.be — RPi kernels in Bookworm (package names: linux-headers-rpi-v8 etc.) — https://yeri.be/rpi-kernels-in-bookworm/ (accessed 2026-06-18)
[22] Raspberry Pi Forums — "Rpi OS Bookworm 64 bit: cannot install linux-headers-rpi-v8" — https://forums.raspberrypi.com/viewtopic.php?t=360082 (accessed 2026-06-18)
---
## Recommended canonical install+verify sequence (< 10 lines)
```bash
# On the build host — transfer:
rsync -avz kernel/iec_listener.ko pi@raspberrypi.local:/home/pi/iec/
# On the Pi — pre-check:
modinfo ~/iec/iec_listener.ko | grep vermagic # must match uname -r exactly
# Load and verify:
sudo insmod ~/iec/iec_listener.ko address=4
lsmod | grep iec_listener # module listed → OK
cat /sys/module/iec_listener/parameters/address # prints 4 → OK
ls -l /dev/iec0 # crw------- → udev node created
dmesg | tail -5 # init messages → no BUG/WARNING
# Selftest before connecting C64:
sudo python3 ~/iec/selftest.py # IEC_IOC_SELFTEST → PASS
# Unload cleanly:
sudo rmmod iec_listener
dmesg | tail -5 # DATA released message → safe
```
**Bookworm / Pi Zero 2 W gotchas:**
- All boot config paths have moved: `/boot/firmware/config.txt`, `/boot/firmware/cmdline.txt`, `/boot/firmware/overlays/`. Using the old `/boot/` paths silently does nothing.
- The vermagic for arm64 Bookworm ends in `modversions aarch64` — the `modversions` token means symbol CRC checking is active. Headers and running kernel must come from the same apt transaction.
- `apt full-upgrade` will break the module. Pin the kernel immediately: `sudo apt-mark hold raspberrypi-kernel raspberrypi-kernel-headers raspberrypi-bootloader`.
- `/dev/iec0` is created automatically by udev — no `mknod`. If it doesn't appear within ~1 second of insmod, check `dmesg` for `device_create` errors and confirm `udev` is running (`systemctl status udev`).
- Module signing is NOT enforced on stock RPi OS. A taint value of 12288 (flags O+E) after load is normal and harmless.