From User Space to Kernel Space

Kernel Module

The kernel module does the below tasks:

  1. Makefile.
obj-m += simplechar.o
EXTRA_CFLAGS += -g

all:
    make -C /root/kernel/linux-5.15.186 M=$(PWD) modules

clean:
    make -C /root/kernel/linux-5.15.186 M=$(PWD) clean
  1. Build.
$ make
  1. Copy module to filesystem of VM.
$ mkdir /mnt/rootfs
$ mount /root/kernel/buildroot-2025.05/output/images/rootfs.ext2 /mnt/rootfs/

$ cp simplechar.ko /mnt/rootfs/opt/
  1. Start VM.
$ qemu-system-x86_64 -boot order=c -m 2048M \
    -kernel /root/kernel/linux-5.15.186/arch/x86/boot/bzImage \
    -hda /root/kernel/buildroot-2025.05/output/images/rootfs.ext2 \
    -append "root=/dev/sda rw console=ttyS0,115200 nokaslr" \
    -nographic
  1. On VM, insert the module.
$ insmod /opt/simplechar.ko
[   28.060330] simplechar: loading out-of-tree module taints kernel.
[   28.075624] simplechar: registered major 248
  1. Create device on VM.
$ mknod /dev/simplechar c 248 0
$ chmod 666 /dev/simplechar
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>

#define DEVICE_NAME "simplechar"
#define MESSAGE "Hello from kernel module!\n"

static int major;
static int msg_len = sizeof(MESSAGE) - 1;

static int dev_open(struct inode *inode, struct file *file) {
    printk(KERN_INFO "simplechar: opened\n");
    return 0;
}

static ssize_t dev_read(struct file *file, char __user *buf, size_t len, loff_t *offset) {
    if (len < msg_len)
        return -EINVAL; // user buffer too small

    if (copy_to_user(buf, MESSAGE, msg_len))
        return -EFAULT;

    return msg_len;
}

static int dev_release(struct inode *inode, struct file *file) {
    printk(KERN_INFO "simplechar: closed\n");
    return 0;
}

static struct file_operations fops = {
    .open = dev_open,
    .read = dev_read,
    .release = dev_release,
};

static int __init simplechar_init(void) {
    major = register_chrdev(0, DEVICE_NAME, &fops);
    if (major < 0)
        return major;
    printk(KERN_INFO "simplechar: registered major %d\n", major);
    return 0;
}

static void __exit simplechar_exit(void) {
    unregister_chrdev(major, DEVICE_NAME);
    printk(KERN_INFO "simplechar: unregistered\n");
}

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Simple char device module");

module_init(simplechar_init);
module_exit(simplechar_exit);

User-Space Program

  1. Build
$ gcc -static -o demo demo.c
  1. Copy the program to the filesystem of VM.
$ cp demo /mnt/rootfs/opt/
  1. Restart QEMU VM.
$ qemu-system-x86_64 -boot order=c -m 2048M \
    -kernel /root/kernel/linux-5.15.186/arch/x86/boot/bzImage \
    -hda /root/kernel/buildroot-2025.05/output/images/rootfs.ext2 \
    -append "root=/dev/sda rw console=ttyS0,115200 nokaslr" \
    -nographic
  1. Setup module, device.
$ insmod /opt/simplechar.ko
$ mknod /dev/simplechar c 248 0
$ chmod 666 /dev/simplechar
  1. Run program.
$ /opt/demo
[  232.892346] simplechar: opened
Read from kernel module: Hello from kernel module!
[  232.895639] simplechar: closed

The user-space program does the below tasks:

File: demo.c

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

#define DEVICE "/dev/simplechar"

int main() {
    int fd = open(DEVICE, O_RDONLY);
    if (fd < 0) {
        perror("Failed to open device");
        return 1;
    }

    char buf[128];
    ssize_t ret = read(fd, buf, sizeof(buf) - 1);
    if (ret < 0) {
        perror("Failed to read");
        close(fd);
        return 1;
    }

    buf[ret] = '\0';
    printf("Read from kernel module: %s", buf);

    close(fd);
    return 0;
} 

Debug with GDB

  1. Start QEMU VM with gdb server.
$ qemu-system-x86_64 -boot order=c -m 2048M \
    -kernel /root/kernel/linux-5.15.186/arch/x86/boot/bzImage \
    -hda /root/kernel/buildroot-2025.05/output/images/rootfs.ext2 \
    -append "root=/dev/sda rw console=ttyS0,115200 nokaslr" \
    -nographic \
    -s -S
  1. From host, remote debug to VM.
$ gdb /root/kernel/linux-5.15.186/vmlinux
(gdb) target remote :1234
(gdb) lx-symbols
loading vmlinux
scanning for modules in /root/kernel/module
loading @0xffffffffc0000000: /root/kernel/module/simplechar.ko
(gdb) break dev_read
Breakpoint 1 at 0xffffffffc0000000: file /root/kernel/module/simplechar.c, line 17.
(gdb) continue
  1. On VM, setup module, device.
$ insmod /opt/simplechar.ko
[  111.235440] simplechar: loading out-of-tree module taints kernel.
[  111.250356] simplechar: registered major 248

$ mknod /dev/simplechar c 248 0
$ chmod 666 /dev/simplechar
  1. Run the program.
$ /opt/demo
Breakpoint 1, dev_read (file=0xffff8880049d5800, buf=0x7ffeb62dcda0 "\340WL", len=127, 
    offset=0xffffc900001d7f08) at /root/kernel/module/simplechar.c:17
17          if (len < msg_len)