Long time no see, confreres.
This time we will do some crazy (or maybe even mad) things.
As we all know, Linux kernel is a cross-platform software. It supports a very large variety of hardware platforms. Thus it’s written in the most “inter-platform” language in the world — Plain C. The secret of cross-platformness is a compiler used to build the kernel itself and it’s modules. Exceptions are small parts that provide the only hardware specific functionality that can not be written in C — SoCs/CPUs starting and configuring routines. These small parts of kernel are written in assembly languages.
But today I’ll present some sort of skeleton of the whole kernel module written in assembly language — not a single .c-file is used. I had this idea for a long time, I’ve googled a lot of times for some examples or at least a starting point or a discussion. But had no luck — no examples for ARM nor x86 or any other architecture. This fact proves the craziness of my idea (possibly also it explains why you should not do this in your practice). But I’ve managed to do this.
This module’s functionality will be limited to some kind of “Hello World” — it will be able to be loaded and unloaded correctly and do the only thing — print messages on these events. I’ve mentioned that no .c-files are used in this module, but this module looks like a usual kernel module (except the language it is written in) and is built and works like any other module — thus it is a regular code which you can work with like your everyday routine — no any sort of magic nor discomfort. We will talk about ARM64 (or AArch64) but it can be easily rewritten for any other architecture.
I said that there is no magic in this module, but… we know that any object file must have some sections, information and has to be built — compiled and linked according to strict rules. Kernel module is no exception. That was the “magic” I had to reveal to achieve the goal of my idea — regular-looking and developer-friendly kernel module in assembly language.
There is a lot of “magic” in kernel build system. But my idea was to write a template of a kernel module that will look, feel, act and work like a regular one, as regular part of kernel source tree. Thus today we will not talk about kernel’s build system, differences between linking user-space objects and linking kernel-space objects — but about template of a kernel module in assembly language only (and Makefile for it, of course). Building — compiling and linking will be done by standard kernel build system.
Kernel module project consists of two parts — Makefile and source code. Let's start with Makefile. Everything is clear enough here — you just set source file name to your assembly file and set obj-m variable. That's all — the rest of the job will be done by kernel build system. Here is our Makefile:
ifeq ($(KERNEL_SRC),)
$(error Specify KERNEL_SRC directory)
endif
export ARCH := arm64
export CROSS_COMPILE ?= aarch64-linux-gnu-
PWD := $(shell pwd)
PROJECT_NAME := asm_ko_hello
$(PROJECT_NAME)-src := $(PROJECT_NAME).S
obj-m += $(PROJECT_NAME).o
AFLAGS_$(PROJECT_NAME).o := -DPROJECT_NAME=$(PROJECT_NAME)
all:
make -C $(KERNEL_SRC) M=$(PWD) modules
clean:
make -C $(KERNEL_SRC) M=$(PWD) clean
Now, let's have a look at asm_ko_hello.S:
#include "linux/kern_levels.h"
Here you can see something familiar to kernel modules you have worked with and guess that we will have standard levels of output and you are right — we will have all that standard KERN_XXXX output levels in our assembly code.
#if !defined (PROJECT_NAME)
#error You must define project name for this template. Stopping build.
#endif
#define MAKE_FN_NAME(x, y) x##_##y
#define FN_NAME(project, func) MAKE_FN_NAME(project, func)
The above lines serve project's template and are not related to today's topic.
.section .text
FN_NAME(PROJECT_NAME, init):
stp x29, x30, [sp, -16]!
adrp x0, .loaded
mov x29, sp
add x0, x0, :lo12:.loaded
bl _printk
mov w0, 0
ldp x29, x30, [sp], 16
ret
FN_NAME(PROJECT_NAME, exit):
stp x29, x30, [sp, -16]!
adrp x0, .unloaded
mov x29, sp
add x0, x0, :lo12:.unloaded
bl _printk
ldp x29, x30, [sp], 16
ret
This is the code of common "Hello World" kernel module. It is put into standard .text section. FN_NAME macro will produce functions names. And after go two functions bodies in standard/usual ARM64 assembly language. As you can see we preserve registers, load string address and call (in ARM assembly it's called "branch with link") printk() function, restore registers and, in case of _init(), return value (which actually is an abstraction).
The code looks clear and familiar, but this is kernel module and there is a little difference with user-space program. We should let the system know where are the entry and leave points (or load/unload functions) of our module. In C it's done by two macros module_init() and module_exit(). In our case it would look like:
module_init(asm_ko_init);
module_exit(asm_ko_exit);
But how should we specify these functions in assembly language? What's covered under those macros? Actually nothing too complicated. We just need to declare global functions (symbols) init_module and cleanup_module. To give them proper payload (symbol itself is just a symbol in object file) we specify aliases to our _init() and _exit() functions with .set directive. The whole part of this code is in snippet below:
.global init_module
.global cleanup_module
.set init_module, FN_NAME(PROJECT_NAME, init)
.set cleanup_module, FN_NAME(PROJECT_NAME, exit)
We can't omit the .data section. This one is absolutely standard. Here is our section with strings used for our output:
.section .data
.unloaded:
.string KERN_INFO MODULE_NAME": unloaded.\n"
.loaded:
.string KERN_INFO MODULE_NAME": successfully loaded.\n"
There is something that the kernel will not accept our module without. This is something new for developers working in user-space and familiar for kernel-space developers. For Linux kernel we have to specify one necessary parameter that can not be omitted — the license. In C it is done by MODULE_LICENSE() macro and would look like:
MODULE_LICENSE("GPL");
Let's see how it is done in assembly language. Maybe you've expected something serious here — some special codes or sequences. But it's easy too — this is just .modinfo section containing information about module in a very simple (and unexpectedly ridiculous) format. See self-explaining snippet below:
#define MODULE_NAME "Kernel Module in Assembly"
#define MODULE_VER "1.0"
#define MODULE_AUTHOR "Timofey Chernigovskiy, 2024"
.section .modinfo, "a"
.string "author=" MODULE_AUTHOR
.string "version=" MODULE_VER
.string "description=" MODULE_NAME
.string "license=GPL"
That's all about code. The link is here. According to our Makefile, if you cross-compiler is aarch64-linux-gnu-, you build module as follows:
make KERNEL_SRC="PATAH/TO/YOUR/KERNEL/lib/modules/VERSION/build" clean all
Test it:
modinfo ./asm_ko_hello.ko
filename: ./asm_ko_hello.ko
author: Timofey Chernigovskiy, 2024
version: 1.0
description: Kernel Module in Assembly
license: GPL
srcversion: 48259120F9222D4D7B9D8E7
depends:
name: asm_ko_hello
vermagic: 6.1.55 SMP preempt mod_unload modversions aarch64
insmod ./asm_ko_hello.ko
Kernel Module in Assembly: successfully loaded.
lsmod
Module Size Used by
asm_ko_hello 16384 0
rmmod asm_ko_hello
Kernel Module in Assembly: unloaded.
P.S.
High level programming languages do a lot of job for developer and prevent a lot of mistakes. Actually not prevent but don't allow — you don't have enough tools to do most dummy things. When you write in assembly language in user-space it's a risk, but it's a funny walk in comparison to kernel-space. So you have to be extremely cautious working in assembly language in kernel-space because your any typo will be compiled and executed. For example, slight shift of stack pointer may lead to huge troubles — broken file system is a case. You have been warned — it's your decision how hard you wanna play.