linux内核之Hello World

221 阅读4分钟

1. 问题描述

梳理内核函数dump_stack的源码实现时,PC上竟然没有该函数打印内容的日志截图,又不想从网上盗图,所以学习下如何在内核中打印Hello World。

2. 解决方案

科普:Kernel Module是一个二进制文件,用于运行时拓展内核功能,如果功能不被需要时,可被卸载。直观上看,与dlopen机制类似,程序运行时显示加载so,当不使用该库时,调用dlclose关闭so。

解决思路:基于Kernel Module机制,执行命令"insmod + 模块名" 安装模块,执行打印。

2.1 基础版

注意:测试使用的系统是Ubuntu,目录/lib/modules下已有内核相应的头文件,无需自己安装源码。

  1. 模块被加载时,内核执行函数hello_init,卸载则执行hello_exit。源文件名:hello.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

MODULE_LICENSE("GPL");

static int hello_init(void)
{
    pr_info("%s\n", "Hello World!");
    return 0;
}

static void hello_exit(void)
{
    pr_info("%s\n", "Byebye");
    return;
}

module_init(hello_init);
module_exit(hello_exit);

仅从代码来看,该机制类似于Java中调用System.loadLibrary加载so,so的JNI_OnLoad方法被执行。

  1. Makefile,不要迟疑,文件名也是Makefile
obj-m := hello.o
KDIR := /lib/modules/`uname -r`/build

modules:
	make -C $(KDIR) M=`pwd`
clean:
	make -C $(KDIR) M=`pwd` clean
  1. 代码目录结构如下所示:

截屏2023-07-25 10.24.59.png 执行make命令即可,编译成功后,将生成hello.ko文件

  1. 安装模块,命令:insmod hello.ko

日志文件 /var/log/syslog,可看到如下打印。 截屏2023-07-25 10.43.27.png 使用命令lsmod,可查看内核加载的模块。 截屏2023-07-25 10.44.04.png 从图中也不难看出,hello模块也确实被加载到内核。 5. 卸载模块,命令:rmmod hello

截屏2023-07-25 10.52.55.png

2.2 进阶版

需求场景:用户态进程使用C++ std::thread启动线程,该线程内部调用Linux系统调用,然后系统调用内部使用dump_stack打印内核堆栈。显然只有"Hello World"还是不够的。如果实现该需求场景,将面临一个问题,那就是如何让系统调用将用户请求转发到自己写的Kernel Module中来呢 ?

浏览Linux内核打印Hello World相关的技术文章时,偶然碰见有一篇文章使用了字符设备驱动的方法,如果仅仅是为了完成该任务,可以不假思索地采取该办法,如果仔细思考一下,为什么要使用这个方法 ?有没有更简单的方法呢 ?

通过学习kernel module的概念,了解到大多数的设备驱动都以kernel module的形式存在,那么设备驱动是什么呢 ?在此之前,对于驱动这个名词,脑海中其实并没有很准确的概念,仅仅是从同事那里听说,驱动开发之类的名词。其实,设备驱动是与硬件设备交互的内核组件,更细致来讲,用户通过读写特定的设备文件来完成硬件设备的访问。这样看来,我要做的事情其实就是写一个驱动来处理用户的请求。接下来便是学习驱动的相关基础概念,驱动包含两种类型,字符类型和块类型,选择哪一种类型呢 ?用户的系统调用直接转发至字符设备驱动的相关函数,而块设备与系统调用之间还夹杂着文件系统等诸多模块,文件系统又是另外一件复杂的事情,实现块设备驱动对于目前的我而言,是一件不可能完成的事情,而且对于要实现的需求,也没有必要,所以选择字符设备驱动是唯一的选择。

修改后的hello.c文件,内容如下:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>

static int hello_open(struct inode *inode, struct file *file)
{
    pr_info("%s\n", "hello open");
    dump_stack();
    return 0;
}

static int hello_release(struct inode *inode, struct file *file)
{
    pr_info("%s\n", "hello release");
    return 0;
}

static ssize_t hello_read(struct file *file, char __user *user_buffer,
                      size_t size, loff_t *offset)
{
    pr_info("%s\n", "hello read");
    return 0;
}

static ssize_t hello_write(struct file *file, const char __user *user_buffer,
                       size_t size, loff_t * offset)
{
    pr_info("%s\n", "hello write");
    return 0;
}

#define MY_MAJOR       99

struct cdev dev;
struct class *hello_char_class;

const struct file_operations hello_fops = {
    .owner = THIS_MODULE,
    .open = hello_open,
    .read = hello_read,
    .write = hello_write,
    .release = hello_release,
};

MODULE_LICENSE("GPL");

static int hello_init(void)
{
    pr_info("%s\n", "Hello Driver!");
    int err;
    err = register_chrdev_region(MKDEV(MY_MAJOR, 0), 1,
                                 "hello_driver");
    if (err != 0) {
        return err;
    }
    cdev_init(&dev, &hello_fops);
    cdev_add(&dev, MKDEV(MY_MAJOR, 0), 1);
    
    hello_char_class = class_create(THIS_MODULE, "hello_driver");
    device_create(hello_char_class, NULL, MKDEV(MY_MAJOR, 0), NULL, "hello_driver1");
    return 0;
}

static void hello_exit(void)
{
    pr_info("%s\n", "Byebye Driver!");
    device_destroy(hello_char_class, MKDEV(MY_MAJOR, 0));
    class_destroy(hello_char_class);
    
    cdev_del(&dev);
    unregister_chrdev_region(MKDEV(MY_MAJOR, 0), 1);
    return;
}

module_init(hello_init);
module_exit(hello_exit);

编译,安装,卸载模块等过程同上,安装成功后,在/dev/目录可以查看到设备文件hello_driver1。 截屏2023-07-26 23.52.10.png 执行命令 cat hello_driver1,便可触发dump_stack函数。 截屏2023-07-26 23.56.08.png 到此为止,使用c++ std::thread新建线程执行系统调用,在内核中打印dump_stack的任务已完成99%,剩下的一丢丢c++相关代码就不在此处继续呈现了。

3. 总结

通过上述实验及查找相关资料,学习到的知识点如下:

  1. kernel module的概念,安装,卸载module的简易实践。
  2. 设备的标识符的组成原理,MAJOR, MINOR等表示的多种含义。
  3. 设备驱动的类别,及不同类别驱动之间的比较,应用场景等,字符设备驱动的简易实践。

概括一下,实践完上述过程,能够更具体地认知驱动,设备等概念,更近一步地了解linux。

4. 参考

  1. Kernel Modules 基础
  2. kali2022编译Linux内核驱动ko文件