Operating Systems 2022F: Tutorial 8
In this tutorial you'll be learning about special files and how to create one kind of special file, a character device, using Linux kernel modules.
Special Files
In this part your goal is to learn how special files are similar and different from regular files.
Learning Objectives
- What does a special file represent?
- Can you have multiple special files that are somehow "the same"? What does it mean to "copy" a special file?
- What are character devices used for? How can you interact with them?
- What are named pipes? How can they be used?
- What kind of device is a tty? How are tty's different from regular files? How are they similar?
Important Tips
- There is a chance your VM will crash or your ssh session will become unresponsive when messing with the kernel. To recover, you can reboot your VM from the OpenStack web console.
- Definitely do not under any circumstances attempt this on your own system. This is your last warning.
- If you run out of disk space, your kernel logs are probably too big or you have other large files. Be sure to free disk space using these instructions.
- Remember to recompile and insert your module after making changes.
- Work incrementally and be prepared for your system to crash.
- It might be wise to code on your own computer and use scp or sshfs to transfer the files to your VM.
- https://elixir.bootlin.com/ is a great resource for learning about kernel functions and data structures.
A: Understanding special files
- Try the following commands as a non-privileged user. What does each do? How do the files f1-f5 compare? How do they compare to /dev/urandom? Remember you can get output from /dev/urandom using cat or dd. If you use cat, make sure to pipe it to less!
- cp /dev/urandom f1
- cp -a /dev/urandom f2
- sudo cp -a /dev/urandom f3
- mknod f4 c 1 9
- sudo mknod f5 c 1 9
Note that some commands may run for a long time and may create large files. If they do, you'll want to terminate them and delete any files they create (after examining them).
- Make named pipes using mknod and mkfifo. Use them to simulate ls | wc using just the > and < operators.
- Make a block device myroot representing your machine's root filesystem. Get info on its filesystem using dumpe2fs myroot. Do you need to be root to do these operations? (Hint: if you use df to find the device, you may have to follow a symlink to get to the real block device.)
- Use mknod to make a copy of your current terminal's tty. Examine its characteristics using stty --file=mytty. Do the same for the original tty. How can you figure out what your current tty is? (NOTE: On modern Linux systems the copy often doesn't behave like the original. Why do you think this is the case?)
- Run stty --help to see what you can do with stty. Try disabling local echo. How does the shell behave with echo disabled? How can you restore echo without logging out and back in?
Linux kernel modules
In this part of the tutorial you will be building and installing kernel modules. You will need root access to install kernel modules.
It is highly recommended that you use a comp3000 openstack instance for the exercises below for two reasons. First, you may have difficulties compiling kernel modules on other systems. Second, these operations are potentially dangerous and mistakes could destroy all data on the Linux system. Consider yourself warned!
This is the first tutorial where you are seeing kernel code. By the end of tutorial you should be able to run all of the code here and be able to make trivial modifications. Lectures later this week will go through this code in detail. Learning as much as you can about the code now will help you prepare for the upcoming lectures. In particular, try to figure out what is different about this code.
If you cannot build a module you may have installed a version of Ubuntu that is too minimal. But you can fix it by installing the right packages. Do the following:
sudo apt update sudo apt dist-upgrade sudo apt install build-essential sudo apt clean
When upgrading you may get a message when upgrading grub about which boot device to use. Select /dev/vda or /dev/sda - don’t select the devices that end with a number.
If your build failed before doing this and again after, delete the downloaded code and unpack the zip file again. (The build process generates hidden files which can mess up later builds.)
Learning Objectives
- How do you compile and install kernel modules? How does this process differ from userspace C programs?
- How does the source and build process of kernel modules differ from C programs?
- Understand when module code is and isn't run.
- Understand how kernel modules can access and manipulate kernel data structures and call kernel functions.
- Understand how making system calls from userspace can cause module functions to be called.
- Understand how character devices can be implemented in a module and thus better understand what a character device is.
- Understand how processes are represented by task_struct's in the Linux kernel, what kinds of information is stored in a task struct, and how to access the task_struct of a process that made a system call.
B: A simple kernel module
- Download the source for this simple module, unpack, and build it by typing "make". Use
wget
to download the zip file. - Install the module using "sudo insmod simple.ko". The hello message is recorded in the kernel logs. How do you view the kernel logs?
- Check to see that the module has been loaded. How do you do this?
- Remove the module from the kernel. What did you do?
- Open a second terminal and run trace 'p::printk printf "%s" arg1'. Repeat the above three questions and note the output in the terminal running trace. What does this trace command do?
C: A character device kernel module
- Download the source for ones, a kernel module implementing a character device that ouputs an unbounded string of "1"'s. Build, compile, and run it as before.
- What kernel messages does the module generate? Does it create any new files (other than /dev/ones)? If so, where?
- What happens when you "cat" the device /dev/ones? How can you limit the output?
- How can you modify your module to generate a kernel "Oops" as reported in the kernel logs or outright crash the kernel?
- Before inserting your module, run trace -K 'p::__register_chrdev' in another terminal to print the kernel stack trace for __register_chrdev. Insert your module and examine the output from your trace command. Did you expect all those function calls? What do you think they do?
D: Getting process information from a module
To answer these questions on newgetpid.c, you'll need to refer to the Linux kernel source, particularly kernel/sys.c. (These are links to a cross-referenced version of the Linux kernel source, a key resource for understanding and developing kernel code.)
- Download the source newgetpid.c. Build and run it as before.
- What type is "current"? How can you figure this out?
- Modify newgetpid.c so that it creates a device file /dev/describe rather than /dev/newgetpid.
- Make /dev/describe output the calling process's parent ID (ppid), user ID (uid), group ID (gid), effective user ID (euid), and effective group ID (egid).
- (Advanced) Modify /dev/describe so that if you write a process ID to it, it will output the information on the provided process. To make this work, you'll need to:
- Add a write method by adding a write operation to the file operations struct. Write operations have the same prototype as read operations, except the buffer is marked constant (because it shouldn't be modified).
- Convert the written text to an integer and store in a global variable (to the module). Check out kstrtoint_from_user().
- Find the right task struct. See the implementation of the kill system call, and how it looks up the pid struct and then gets the right task struct using that pid struct.
- After returning info on the selected process, further calls should return info on the current process. You can do this by setting the global process ID to 0 and checking this value, using the current task if it is zero.
Code
simple module
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Anil Somayaji <soma@scs.carleton.ca>");
MODULE_DESCRIPTION("A simple module");
static int simple_init(void)
{
pr_info("Hello kernel world!\n");
return 0;
}
static void simple_exit(void)
{
pr_info("Goodbye kernel world.\n");
return;
}
module_init(simple_init);
module_exit(simple_exit);
ones module
/* Code derived from:
https://appusajeev.wordpress.com/2011/06/18/writing-a-linux-character-device-driver/
and
http://pete.akeo.ie/2011/08/writing-linux-device-driver-for-kernels.html
*/
#include <linux/module.h>
#include <linux/string.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <asm/uaccess.h>
#define dbg(format, arg...) do { if (debug) pr_info(CLASS_NAME ": %s: " format, __FUNCTION__, ## arg); } while (0)
#define err(format, arg...) pr_err(CLASS_NAME ": " format, ## arg)
#define info(format, arg...) pr_info(CLASS_NAME ": " format, ## arg)
#define warn(format, arg...) pr_warn(CLASS_NAME ": " format, ## arg)
#define alert(format, arg...) pr_alert(CLASS_NAME ": " format, ## arg)
#define DEVICE_NAME "ones"
#define CLASS_NAME "comp3000"
static struct class* ones_class = NULL;
static struct device* ones_device = NULL;
static int ones_major;
static int ones_open(struct inode *the_inode, struct file *f)
{
return 0;
}
static ssize_t ones_read(struct file *f, char *buf, size_t len, loff_t *offset)
{
size_t i;
for (i = 0; i < len; i++) {
put_user('1', buf++);
}
return i;
}
static int ones_release(struct inode *the_inode, struct file *f)
{
alert("Ones device closed\n");
return 0;
}
static struct file_operations ones_fops = {
.open = ones_open,
.read = ones_read,
.release = ones_release,
};
static char *ones_devnode(struct device *dev, umode_t *mode)
{
if (mode)
*mode = 0444;
return NULL;
}
static int ones_init(void)
{
int retval;
ones_major = register_chrdev(0, DEVICE_NAME, &ones_fops);
if (ones_major < 0) {
err("failed to register device: error %d\n", ones_major);
retval = ones_major;
goto failed_chrdevreg;
}
ones_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(ones_class)) {
err("failed to register device class '%s'\n", CLASS_NAME);
retval = PTR_ERR(ones_class);
goto failed_classreg;
}
ones_class->devnode = ones_devnode;
ones_device = device_create(ones_class, NULL, MKDEV(ones_major, 0),
NULL, DEVICE_NAME);
if (IS_ERR(ones_device)) {
err("failed to create device '%s'\n", DEVICE_NAME);
retval = PTR_ERR(ones_device);
goto failed_devreg;
}
info("Ones device registered using major %d.\n", ones_major);
return 0;
failed_devreg:
class_unregister(ones_class);
failed_classreg:
unregister_chrdev(ones_major, DEVICE_NAME);
failed_chrdevreg:
return -1;
}
static void ones_exit(void)
{
device_destroy(ones_class, MKDEV(ones_major, 0));
class_unregister(ones_class);
unregister_chrdev(ones_major, "ones");
info("Unloading Ones module.\n");
return;
}
module_init(ones_init);
module_exit(ones_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Anil Somayaji <soma@scs.carleton.ca>");
MODULE_DESCRIPTION("A write ones character device module");
newgetpid module
/* Code derived from:
https://appusajeev.wordpress.com/2011/06/18/writing-a-linux-character-device-driver/
and
http://pete.akeo.ie/2011/08/writing-linux-device-driver-for-kernels.html
*/
#include <linux/module.h>
#include <linux/string.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/sched.h>
#include <asm/uaccess.h>
#define dbg(format, arg...) do { if (debug) pr_info(CLASS_NAME ": %s: " format, __FUNCTION__, ## arg); } while (0)
#define err(format, arg...) pr_err(CLASS_NAME ": " format, ## arg)
#define info(format, arg...) pr_info(CLASS_NAME ": " format, ## arg)
#define warn(format, arg...) pr_warn(CLASS_NAME ": " format, ## arg)
#define alert(format, arg...) pr_alert(CLASS_NAME ": " format, ## arg)
#define DEVICE_NAME "newgetpid"
#define CLASS_NAME "comp3000"
static struct class* newgetpid_class = NULL;
static struct device* newgetpid_device = NULL;
static int newgetpid_major;
static int newgetpid_open(struct inode *the_inode, struct file *f)
{
return 0;
}
static ssize_t newgetpid_read(struct file *f, char *buf, size_t len, loff_t *offset)
{
size_t i, msglen;
pid_t thepid;
char message[100];
if (*offset > 0) {
return 0;
}
thepid = task_tgid_vnr(current);
snprintf(message, 100, "Your PID is %d!\n", thepid);
msglen = strlen(message);
if (len < msglen) {
msglen = len;
}
for (i = 0; i < msglen; i++) {
put_user(message[i], buf++);
}
*offset = i;
return i;
}
static int newgetpid_release(struct inode *the_inode, struct file *f)
{
alert("Newgetpid device closed\n");
return 0;
}
static struct file_operations newgetpid_fops = {
.open = newgetpid_open,
.read = newgetpid_read,
.release = newgetpid_release,
};
static char *newgetpid_devnode(struct device *dev, umode_t *mode)
{
if (mode)
*mode = 0444;
return NULL;
}
static int __init newgetpid_init(void)
{
int retval;
newgetpid_major = register_chrdev(0, DEVICE_NAME, &newgetpid_fops);
if (newgetpid_major < 0) {
err("failed to register device: error %d\n", newgetpid_major);
retval = newgetpid_major;
goto failed_chrdevreg;
}
newgetpid_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(newgetpid_class)) {
err("failed to register device class '%s'\n", CLASS_NAME);
retval = PTR_ERR(newgetpid_class);
goto failed_classreg;
}
newgetpid_class->devnode = newgetpid_devnode;
newgetpid_device = device_create(newgetpid_class, NULL, MKDEV(newgetpid_major, 0),
NULL, DEVICE_NAME);
if (IS_ERR(newgetpid_device)) {
err("failed to create device '%s'\n", DEVICE_NAME);
retval = PTR_ERR(newgetpid_device);
goto failed_devreg;
}
info("Newgetpid device registered using major %d.\n", newgetpid_major);
return 0;
failed_devreg:
class_unregister(newgetpid_class);
failed_classreg:
unregister_chrdev(newgetpid_major, DEVICE_NAME);
failed_chrdevreg:
return -1;
}
static void __exit newgetpid_exit(void)
{
device_destroy(newgetpid_class, MKDEV(newgetpid_major, 0));
class_unregister(newgetpid_class);
unregister_chrdev(newgetpid_major, "newgetpid");
info("Unloading Newgetpid module.\n");
return;
}
module_init(newgetpid_init);
module_exit(newgetpid_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Anil Somayaji <soma@scs.carleton.ca>");
MODULE_DESCRIPTION("A write newgetpid character device module");
Makefile
KDIR := /lib/modules/$(shell uname -r)/build
kbuild:
make -C $(KDIR) M=`pwd`
clean:
make -C $(KDIR) M=`pwd` clean
You also need a Kbuild file with the following:
obj-m := module.o
Replace "module.o" above with "simple.o", "ones.o", or "newgetpid.o" as appropriate.