Writing a pseudo-device driver on Linux

pseudo-devices are files, usually located in /dev, they're like a device file, but instead of acting as a bridge between the operating system and hardware, it's a device driver without an actual device. they usually serve a practical purpose, such as producing random data, or acting as a virtual sinkhole for unwanted data. examples would be files like /dev/random or /dev/null.

the way a device file is associated with its driver is via a unique number called a major number. in addition to its major number, every device is assigned a minor number too.

the best way to imagine this is by thinking of your disk. if you're running your system with a single disk, most likely its device file is going to be /dev/sda. the name tells us that it's using the sd "storage driver", and that its minor number is 0.

/dev/sdb has the same major-, but not minor number.

$ stat /dev/sda
  File: /dev/sda
    Size: 0               Blocks: 0          IO Block: 4096   block special file
    Device: 6h/6d   Inode: 9635        Links: 1     Device type: 8,0
    Access: (0660/brw-rw----)  Uid: (    0/    root)   Gid: (    6/    disk)
[...]

here we can see that sda's major number is 8, and its minor is 0.

the same applies to other device types, such as tty

$ stat /dev/tty1
[...]
Device: 6h/6d   Inode: 1037        Links: 1     Device type: 4,0
[...]
$ stat /dev/tty4
[...]
Device: 6h/6d   Inode: 1045        Links: 1     Device type: 4,4
[...]

with that out of the way, lets make a pseudo-device driver. the driver we're making is going to be called schrödinger's module. it will act as a binary random generator, with an artistic component.

first you'll need the linux source, getting the source is different per distro, but most likely there will be a package for it.

if you can't find it, take a look in kernel.org/pub/linux/kernel and download linux-`uname -r`.

then we need to prepare the source

# whatever command to fetch your linux source
cd linux-`uname -r`
make oldconfig
make prepare
make scripts

now we'll need set up the source directory

mkdir cat
cd cat
$EDITOR Makefile

now modify it as follows

obj-m += cat.o

all:
    make -C /path/to/your/linux-`uname -r` M=`pwd` modules

clean:
    make -C /path/to/your/linux-`uname -r` M=`pwd` clean
obviously adjust the paths.

then we create the source file cat.c like this;

#include <linux/uaccess.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/random.h>
#include <linux/init.h>
#include <linux/fs.h>

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("quantum cat device");
MODULE_AUTHOR("dcat");
MODULE_VERSION("0.1");
MODULE_SUPPORTED_DEVICE("cat");

static ssize_t dev_read(struct file *, char *, size_t, loff_t *);
static int     dev_open(struct inode *, struct file *);
static int     dev_release(struct inode *, struct file *);

/* callbacks for file operations */
static struct file_operations fops = {
    .read = dev_read,
    .open = dev_open,
    .release = dev_release
};

static int major;

/* state control */
static int busy; /* is the device already opened?   */
static int done; /* has the file already been read? */

static char dcat[] =
    "\033[38;5;238m ,                          ,,\"'\n"
    "  ▚,                      ,\"=|\n"
    "  '▒\"UL  .  -= ▔▔  =+=  J'\"░/,\n"
    "   \E}     ▔               ▙' _\n"
    "   ]                       ▞\n"
    "    '░   < \033[38;5;226mX\033[38;5;238m >"
    "     < \033[38;5;226mX\033[38;5;238m >  E\n"
    "  ───-                    G-───\n"
    "  __─-''        `/       ''-─__\n"
    "    ,-'\" ,▗     ▁▁      K\"'-.\n"
    "            =_   \033[38;5;203mU\033[38;5;238m _ # '\"\n"
    "              ' ' \"\033[0m\n";

static ssize_t
dev_read(struct file *fp, char *buf, size_t n, loff_t *of) {
    ssize_t len = sizeof(dcat)/sizeof(dcat[0]); /* get length of dcat */
    char rand;

    get_random_bytes(&rand, sizeof(rand));
    if (rand > 0) {
        dcat[0xce] = '>';
        dcat[0xee] = '>';
        dcat[0x190] = ' ';
    } else {
        dcat[0xce] = 'X';
        dcat[0xee] = 'X';
        dcat[0x190] = 'U';
    }


    if (done)
        return 0;


    /*
     * copy_to_user() and put_user() should be used
     * when moving memory from kernel- to userspace.
     */
    if (copy_to_user(buf, dcat, len))
        printk(KERN_ALERT "copy_on_user");

    done = 1;
    return len;
}

static int
dev_open(struct inode *ino, struct file *fp) {
    /* if device is in use, reply with busy error */
    if (busy)
        return -EBUSY;

    busy = 1; /* toggle device as busy */
    try_module_get(THIS_MODULE); /* tell the system that we're live */
    return 0;
}

static int
dev_release(struct inode *ino, struct file *fp) {
    done = busy = 0;
    module_put(THIS_MODULE); /* we're finished */
    return 0;
}

static void
kexit(void) {
    unregister_chrdev(major, "dcat");
    return;
}

static int
kinit(void) {
    /* register as a character device */
    major = register_chrdev(0, "dcat", &fops);

    done = busy = 0;

    if (major < 0)
        printk(KERN_ALERT "register_chrdev %d", major);

    return 0;
}

module_init(kinit);
module_exit(kexit);

the final thing we do before compiling is to copy over Module.symvers

cp /lib/modules/`uname -r`/build/Module.symvers /path/to/your/linux-source

now run make

make
make[1]: Entering directory '/path/to/your/linux-source'
  CC [M]  /usr/src/cat/cat.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /usr/src/cat/cat.mod.o
  LD [M]  /usr/src/cat/cat.ko
make[1]: Leaving directory '/usr/src/cat'
you might have to change copy_to_user to raw_copy_to_user.

if cat.ko compiled successfully, we can insert the module into the kernel

insmod cat.ko

we need to create a device node associated with our driver, to create the node we need the major number that our driver got assigned

grep dcat </proc/devices
244 dcat

our major number is 244, now we can use mknod(1) to create the node.

mknod /dev/cat c 244 0

now we can try to read it.

scrot


see also; mknod(1), Schrödinger's cat, copy_to_user.