<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Daniel Petrovic</title>
    <description>The latest articles on DEV Community by Daniel Petrovic (@danielpetrovic).</description>
    <link>https://dev.to/danielpetrovic</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4056694%2Fa9d7ab7b-2e2f-4d51-9abb-fc7b954b2766.jpg</url>
      <title>DEV Community: Daniel Petrovic</title>
      <link>https://dev.to/danielpetrovic</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/danielpetrovic"/>
    <language>en</language>
    <item>
      <title>Building a Linux Kernel Module in Pure x86</title>
      <dc:creator>Daniel Petrovic</dc:creator>
      <pubDate>Fri, 31 Jul 2026 13:38:31 +0000</pubDate>
      <link>https://dev.to/danielpetrovic/building-a-linux-kernel-module-in-pure-x86-i7l</link>
      <guid>https://dev.to/danielpetrovic/building-a-linux-kernel-module-in-pure-x86-i7l</guid>
      <description>&lt;p&gt;Most Linux kernel modules are written in C.&lt;/p&gt;

&lt;p&gt;The reasons are obvious:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The kernel APIs are designed around C.&lt;/li&gt;
&lt;li&gt;Documentation assumes C.&lt;/li&gt;
&lt;li&gt;The build system naturally integrates with C.&lt;/li&gt;
&lt;li&gt;The compiler handles many low-level details for us.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is another approach: remove the compiler-generated layer and interact with the kernel using only the interfaces visible at the binary level.&lt;/p&gt;

&lt;p&gt;What if we remove the compiler-generated machinery and take responsibility for every detail ourselves?&lt;/p&gt;

&lt;p&gt;What if the only language between your code and the Linux kernel is x86-64 assembly?&lt;/p&gt;

&lt;p&gt;I used GAS (GNU Assembler) for this — the same assembler the kernel build system already uses behind the scenes. The module is built with the kernel's kbuild system, so no external toolchain is needed.&lt;/p&gt;

&lt;p&gt;Although this experiment uses x86-64 as its host platform, the core lessons&lt;br&gt;
apply to embedded Linux on ARM64 (AArch64) and RISC-V as well: ELF section&lt;br&gt;
flags, symbol export tables, and build-time validation remain part of the&lt;br&gt;
module contract. The exact &lt;code&gt;objtool&lt;/code&gt; checks and security-thunk sequences&lt;br&gt;
depend on the target architecture and kernel configuration.&lt;/p&gt;

&lt;p&gt;The objective was simple to describe, but less simple to implement:&lt;/p&gt;

&lt;p&gt;Create a Linux kernel module in pure x86 assembly, load it into a modern Fedora kernel, and make it print messages when it loads and unloads.&lt;/p&gt;

&lt;p&gt;The final assembly implementation is surprisingly small, but the surrounding kernel contracts are where most of the complexity lives.&lt;/p&gt;

&lt;p&gt;There were a few bumps along the way.&lt;/p&gt;

&lt;p&gt;The kernel rejected several iterations of the module, and each failure revealed another hidden rule of kernel development.&lt;/p&gt;

&lt;p&gt;This post documents the investigation process, the failures encountered, and the kernel mechanisms behind them.&lt;/p&gt;

&lt;p&gt;Everything in this post was tested on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt;
&lt;span class="go"&gt;x86_64

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt;
&lt;span class="go"&gt;7.1.4-204.fc44.x86_64

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;lsb_release &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;span class="go"&gt;Distributor ID: Fedora
Description:    Fedora Linux 44 (Workstation Edition)
Release:        44
Codename:       n/a

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;as &lt;span class="nt"&gt;--version&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;
&lt;span class="go"&gt;GNU assembler version 2.46.1-1.fc44

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;gcc &lt;span class="nt"&gt;--version&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;
&lt;span class="go"&gt;gcc (GCC) 16.1.1 20260515 (Red Hat 16.1.1-2)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The First Attempt: A Simple Assembly Module
&lt;/h2&gt;

&lt;p&gt;The initial implementation focused on the minimum required kernel interfaces:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create an assembly file.&lt;/li&gt;
&lt;li&gt;Export entry and exit points.&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;printk&lt;/code&gt; to write messages into the kernel log.&lt;/li&gt;
&lt;li&gt;Build with kbuild.&lt;/li&gt;
&lt;li&gt;Insert with &lt;code&gt;insmod&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a deeper look at what happens under the hood when you run &lt;code&gt;insmod&lt;/code&gt;, see &lt;a href="https://kernel-internals.org/modules/module-loading-internals/" rel="noopener noreferrer"&gt;Module Loading Internals&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A kernel module is organized into ELF sections. The ones that matter for a minimal module:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Section&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.modinfo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Null-separated key=value strings: &lt;code&gt;license&lt;/code&gt;, &lt;code&gt;author&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, &lt;code&gt;vermagic&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.init.text&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Init code — freed after &lt;code&gt;mod-&amp;gt;init()&lt;/code&gt; returns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.exit.text&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cleanup code — kept until unload&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first version looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.intel_syntax noprefix

.extern printk

.section .modinfo               # Module metadata
.asciz "license=GPL"
.asciz "description=Minimal assembly Linux kernel module"
.asciz "author=Daniel Petrovic"

.section .init.text

.globl init_module
.type init_module, @function
init_module:
    lea rdi, [rip + msg_load]  # RIP-relative: kernel modules are position-independent
    xor eax, eax
    call printk
    xor eax, eax
    ret

.globl cleanup_module
.type cleanup_module, @function
cleanup_module:
    lea rdi, [rip + msg_unload]
    xor eax, eax
    call printk
    ret

.section .rodata
msg_load:
    .asciz "asm_module: loaded\n"
msg_unload:
    .asciz "asm_module: unloaded\n"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save the assembly as &lt;code&gt;asm_module.S&lt;/code&gt; and place this &lt;code&gt;Makefile&lt;/code&gt; beside it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nv"&gt;obj-m&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; asm_module.o

&lt;span class="nv"&gt;KDIR&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; /lib/modules/&lt;span class="p"&gt;$(&lt;/span&gt;shell &lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;/build

&lt;span class="nl"&gt;all&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;$(&lt;/span&gt;MAKE&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;KDIR&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;M&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;PWD&lt;span class="p"&gt;)&lt;/span&gt; modules

&lt;span class="nl"&gt;clean&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;$(&lt;/span&gt;MAKE&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;KDIR&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;M&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;PWD&lt;span class="p"&gt;)&lt;/span&gt; clean
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code was simple.&lt;/p&gt;

&lt;p&gt;The kernel was not impressed.&lt;/p&gt;

&lt;p&gt;The build succeeded, but loading failed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;insmod: ERROR: could not insert module asm_module.ko: Invalid module format
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Something was wrong inside the module.&lt;/p&gt;

&lt;p&gt;Time to investigate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 1: How .modinfo Was Duplicated
&lt;/h2&gt;

&lt;p&gt;The first clue appeared in the kernel log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Only one .modinfo section must exist.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was not because assembly modules must never define &lt;code&gt;.modinfo&lt;/code&gt;. A pure&lt;br&gt;
assembly module normally supplies its own metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.section .modinfo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A C module usually produces the same kind of metadata through macros:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;MODULE_LICENSE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"GPL"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;MODULE_DESCRIPTION&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Example module"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;MODULE_AUTHOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Author"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those macros emit &lt;code&gt;.modinfo&lt;/code&gt; entries; kbuild does not automatically create&lt;br&gt;
them for every assembly source file. It can, however, generate additional&lt;br&gt;
module metadata such as &lt;code&gt;vermagic&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In this build, the generated metadata and the assembly-supplied metadata did&lt;br&gt;
not merge. The assembly section had no flags, while the generated section was&lt;br&gt;
allocatable, so the final module contained two separate sections with the same&lt;br&gt;
name:&lt;/p&gt;

&lt;p&gt;The kernel saw:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.modinfo
.modinfo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and refused to load it.&lt;/p&gt;

&lt;p&gt;The fix was to mark the assembly section allocatable so it was compatible with&lt;br&gt;
the generated metadata and the linker combined the entries into one section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.section .modinfo,"a"

.asciz "license=GPL"
.asciz "description=Minimal assembly Linux kernel module"
.asciz "author=Daniel Petrovic"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;"a"&lt;/code&gt; flag marks the section as allocatable, meaning it occupies memory in&lt;br&gt;
the loaded object.&lt;/p&gt;
&lt;h2&gt;
  
  
  Problem 2: The Kernel Wants ELF Information
&lt;/h2&gt;

&lt;p&gt;After fixing &lt;code&gt;.modinfo&lt;/code&gt;, the build continued but &lt;code&gt;objtool&lt;/code&gt; complained:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;init_module() is missing an ELF size annotation
cleanup_module() is missing an ELF size annotation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In normal assembly, this is perfectly valid:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function:
    mov eax, 1
    ret
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CPU understands it.&lt;/p&gt;

&lt;p&gt;But the Linux kernel does more than execute code.&lt;/p&gt;

&lt;p&gt;It analyzes code.&lt;/p&gt;

&lt;p&gt;Tools like &lt;code&gt;objtool&lt;/code&gt; inspect functions for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stack correctness&lt;/li&gt;
&lt;li&gt;security issues&lt;/li&gt;
&lt;li&gt;control flow problems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;objtool&lt;/code&gt; relies on ELF symbol information to understand function boundaries.&lt;br&gt;
Without size annotations, assembly-written functions may not contain enough&lt;br&gt;
metadata for its analysis.&lt;/p&gt;

&lt;p&gt;The missing piece was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.size function_name, .-function_name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a GAS (GNU Assembler) directive that writes the function's size into&lt;br&gt;
the ELF symbol table. The expression &lt;code&gt;.-function_name&lt;/code&gt; subtracts the&lt;br&gt;
function's start address (&lt;code&gt;.&lt;/code&gt; is the current location counter,&lt;br&gt;
&lt;code&gt;function_name&lt;/code&gt; is where the function began). The result is the byte length&lt;br&gt;
of the function, allowing &lt;code&gt;objtool&lt;/code&gt; to determine its boundary.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.globl init_module
.type init_module,@function

init_module:
    xor eax,eax
    ret

.size init_module,.-init_module
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the ELF metadata correctly describes the function.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 3: Wrong Section Attributes
&lt;/h2&gt;

&lt;p&gt;The next warning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;unexpected non-allocatable section
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The kernel organizes memory into sections.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Section&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.init.text&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Code used during initialization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.exit.text&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Code used during removal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.rodata&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Read-only data&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Our assembly contained:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.section .init.text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;but the assembler did not know it was executable memory.&lt;/p&gt;

&lt;p&gt;The correct form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.section .init.text,"ax"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Meaning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;a&lt;/code&gt; = allocatable&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;x&lt;/code&gt; = executable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For exit code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.section .exit.text,"ax"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Problem 4: Fedora's Return Protection
&lt;/h2&gt;

&lt;p&gt;The next enemy was not caused by our code.&lt;/p&gt;

&lt;p&gt;It came from the kernel configuration.&lt;/p&gt;

&lt;p&gt;Fedora enables modern CPU security mitigations, including return thunk protection.&lt;/p&gt;

&lt;p&gt;Modern x86 Linux kernels may enable return-thunk based mitigations for &lt;a href="https://docs.kernel.org/admin-guide/hw-vuln/srso.html" rel="noopener noreferrer"&gt;speculative execution vulnerabilities&lt;/a&gt;. These mitigations change the expected return sequence for kernel code, and &lt;code&gt;objtool&lt;/code&gt; enforces the generated pattern. When they are enabled, kernel-generated code uses return thunks instead of direct &lt;code&gt;ret&lt;/code&gt; instructions. The thunk provides the mitigation sequence required by the kernel configuration. Any kernel module written in assembly must follow the same convention.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;objtool&lt;/code&gt; warned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;'naked' return found in MITIGATION_RETHUNK build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The normal assembly return:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ret
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is not what the kernel expects anymore.&lt;/p&gt;

&lt;p&gt;Instead, protected kernels use a return thunk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;jmp __x86_return_thunk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the module imports it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.extern __x86_return_thunk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and returns through it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;jmp __x86_return_thunk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the module follows the kernel's security model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 5: The printk Mystery
&lt;/h2&gt;

&lt;p&gt;The goal was to print:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;asm_module: loaded
asm_module: unloaded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first attempt assumed that the C-level API name &lt;code&gt;printk&lt;/code&gt; was also the&lt;br&gt;
link-visible symbol name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.extern printk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;call printk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was the natural choice.&lt;/p&gt;

&lt;p&gt;Kernel code uses the &lt;code&gt;printk()&lt;/code&gt; interface.&lt;/p&gt;

&lt;p&gt;But the module build failed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR: "printk" [asm_module.ko] undefined!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was confusing.&lt;/p&gt;

&lt;p&gt;The interface exists.&lt;/p&gt;

&lt;p&gt;The kernel uses it.&lt;/p&gt;

&lt;p&gt;Why can the module not call it?&lt;/p&gt;

&lt;h3&gt;
  
  
  Looking Inside the Running Kernel
&lt;/h3&gt;

&lt;p&gt;The first thing to check was whether the symbol existed.&lt;/p&gt;

&lt;p&gt;Linux exposes kernel symbols through:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /proc/kallsyms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Searching:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep &lt;/span&gt;printk /proc/kallsyms | &lt;span class="nb"&gt;head&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;gave:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0000000000000000 t umip_printk.cold
0000000000000000 t printk_prot.cold
0000000000000000 t __warn_printk.cold
0000000000000000 t printk_store_execution_ctx.cold
0000000000000000 T __pfx__printk
0000000000000000 T _printk
0000000000000000 t __pfx_printk_kthreads_check_locked
0000000000000000 t printk_kthreads_check_locked
0000000000000000 T __pfx__printk_deferred
0000000000000000 T _printk_deferred
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This answered one question:&lt;/p&gt;

&lt;p&gt;On this kernel, the implementation symbol is &lt;code&gt;_printk&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Not:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;printk&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;printk()&lt;/code&gt; is a C function or macro interface, while &lt;code&gt;_printk&lt;/code&gt; is an&lt;br&gt;
implementation symbol. Normal external C modules should use &lt;code&gt;printk()&lt;/code&gt; or&lt;br&gt;
&lt;code&gt;pr_info()&lt;/code&gt;, rather than call &lt;code&gt;_printk&lt;/code&gt; directly. This pure-assembly&lt;br&gt;
experiment has no C macro layer, so the implementation had to investigate the&lt;br&gt;
underlying link-visible symbol. Whether it can be used still depends on kernel&lt;br&gt;
exports.&lt;/p&gt;

&lt;p&gt;On this kernel, &lt;code&gt;include/linux/printk.h&lt;/code&gt; declares &lt;code&gt;_printk()&lt;/code&gt; and defines&lt;br&gt;
&lt;code&gt;printk&lt;/code&gt; as a macro that ultimately calls it. Depending on the kernel&lt;br&gt;
configuration, the macro may call &lt;code&gt;_printk&lt;/code&gt; directly or wrap it with&lt;br&gt;
additional indexing logic. Assembly does not run that C preprocessor mapping,&lt;br&gt;
so it must use the symbol resolved for the target kernel.&lt;/p&gt;

&lt;p&gt;The relevant definition is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cm"&gt;/*
 * See the vsnprintf() documentation for format string extensions over C99.
 */&lt;/span&gt;
&lt;span class="cp"&gt;#define printk(fmt, ...) printk_index_wrap(_printk, fmt, ##__VA_ARGS__)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Existing Symbol vs Exported Symbol
&lt;/h3&gt;

&lt;p&gt;Kernel modules cannot call every function inside the kernel.&lt;/p&gt;

&lt;p&gt;There is a difference between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;function &lt;strong&gt;exists&lt;/strong&gt; in the kernel&lt;/li&gt;
&lt;li&gt;function is &lt;strong&gt;available&lt;/strong&gt; to modules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The kernel source may expose a C-level interface, but modules can only use&lt;br&gt;
symbols exported with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;EXPORT_SYMBOL&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;EXPORT_SYMBOL_GPL&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The module build system checks exported symbols during &lt;code&gt;modpost&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So the chain is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Assembly
    |
    v
referenced symbol
    |
    v
modpost checks exports
    |
    v
Module.symvers
    |
    v
Allowed or rejected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The assembly implementation had to target an exported kernel symbol rather&lt;br&gt;
than the C source-level API. Seeing a symbol in &lt;code&gt;/proc/kallsyms&lt;/code&gt; alone does not&lt;br&gt;
establish that it is available to a module.&lt;/p&gt;
&lt;h3&gt;
  
  
  Discovering Kernel Symbols
&lt;/h3&gt;

&lt;p&gt;The debugging lesson was important.&lt;/p&gt;

&lt;p&gt;When writing kernel assembly, do not guess symbols.&lt;/p&gt;

&lt;p&gt;Investigate.&lt;/p&gt;

&lt;p&gt;Useful commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep &lt;/span&gt;function_name /proc/kallsyms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shows whether the running kernel knows the symbol.&lt;/p&gt;

&lt;p&gt;For module exports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep &lt;/span&gt;function_name /lib/modules/&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/build/Module.symvers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shows whether modules can use it.&lt;/p&gt;

&lt;p&gt;The kernel itself is the source of truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Final Pure Assembly Module
&lt;/h2&gt;

&lt;p&gt;After fixing the metadata, ELF information, sections, and return mechanism, the final structure looked like this:&lt;/p&gt;

&lt;p&gt;Before calling &lt;code&gt;_printk&lt;/code&gt;, &lt;code&gt;xor eax,eax&lt;/code&gt; sets &lt;code&gt;AL&lt;/code&gt; to zero. Under the x86-64&lt;br&gt;
System V ABI, &lt;code&gt;AL&lt;/code&gt; records the number of vector registers used for a variadic&lt;br&gt;
call; this module passes none, so the required value is zero.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.intel_syntax noprefix

.extern _printk                     # Kernel symbol resolved from the running kernel
.extern __x86_return_thunk          # Return thunk selected by the kernel configuration

.section .modinfo,"a"               # "a" = allocatable
.asciz "license=GPL"
.asciz "description=Minimal assembly Linux kernel module"
.asciz "author=Daniel Petrovic"

.section .init.text,"ax"            # "ax" = allocatable + executable; kernel frees this after init
.globl init_module
.type init_module,@function
init_module:
    lea rdi,[rip + msg_load]        # RIP-relative: kernel modules are position-independent
    xor eax,eax                    # Required by x86-64 SysV ABI before variadic calls
    call _printk
    xor eax,eax                    # Return 0
    jmp __x86_return_thunk         # Protected return instead of bare ret
.size init_module,.-init_module

.section .exit.text,"ax"           # Exit code — called on rmmod
.globl cleanup_module
.type cleanup_module,@function
cleanup_module:
    lea rdi,[rip + msg_unload]
    xor eax,eax
    call _printk
    jmp __x86_return_thunk
.size cleanup_module,.-cleanup_module

.section .rodata,"a"
msg_load:
    .asciz "asm_module: loaded\n"
msg_unload:
    .asciz "asm_module: unloaded\n"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Build, Load, and Verify
&lt;/h2&gt;

&lt;p&gt;From the directory containing &lt;code&gt;asm_module.S&lt;/code&gt; and the &lt;code&gt;Makefile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;make
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;insmod asm_module.ko
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dmesg | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 1
&lt;span class="go"&gt;[...] asm_module: loaded

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;rmmod asm_module
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dmesg | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 1
&lt;span class="go"&gt;[...] asm_module: unloaded
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;rmmod&lt;/code&gt; takes the loaded module name, so omit the &lt;code&gt;.ko&lt;/code&gt; suffix.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Writing this module was less about avoiding C and more about exposing everything C normally hides: ELF metadata, section placement, symbol visibility, calling conventions, and kernel security constraints.&lt;/p&gt;

&lt;p&gt;The compiler is not merely translating instructions. It is participating in a contract between your code, the linker, the loader, and the kernel.&lt;/p&gt;

&lt;p&gt;Removing that layer makes those contracts visible.&lt;/p&gt;

&lt;p&gt;Every problem in this post — the duplicate &lt;code&gt;.modinfo&lt;/code&gt;, the missing ELF size annotations, the wrong section attributes, the return thunk, the &lt;code&gt;_printk&lt;/code&gt; symbol — is something conventional C source and the kernel build system handle silently. Writing in assembly means handling all of it yourself.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The complete module from this post is 30 lines of assembly. It was validated through five rounds of kernel rejection and careful reading of kernel log messages.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>kernel</category>
      <category>assembly</category>
      <category>x86</category>
    </item>
  </channel>
</rss>
