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.
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
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'
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.
see also; mknod(1), Schrödinger's cat, copy_to_user.