分类 Linux 相关 下的文章

写一个有参数的 Linux 内核模块

本文讲写一个简单的hello world 内核模块, 但是可以设置参数.

本系列:

  1. 写一个 Linux 内核 hello world 模块
  2. 写一个有依赖的Linux 内核模块

源代码

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/moduleparam.h>

MODULE_LICENSE("GPL");

// 定义模块参数变量
static char* name = "John";
static int age = 30;

// 注册模块参数
module_param(name, charp, S_IRUGO);
MODULE_PARM_DESC(name, "Name parameter");
module_param(age, int, S_IRUGO);
MODULE_PARM_DESC(age, "Age parameter");

// 模块初始化函数
static int __init hello_init(void) {
    printk(KERN_INFO "Hello, %s! Your age is %d.\n", name, age);
    return 0;
}

// 模块退出函数
static void __exit hello_exit(void) {
    printk(KERN_INFO "Goodbye, %s!\n", name);
}

// 注册模块初始化和退出函数
module_init(hello_init);
module_exit(hello_exit);

Makefile

obj-m += hello.o

tag ?= `uname -r`
KDIR := /lib/modules/${tag}/build/

all:
    make -C $(KDIR) M=$(PWD) modules

clean:
    make -C $(KDIR) M=$(PWD) clean

编译并执行

$ make all 

$ sudo insmod hello.ko

$ tail -n 1 /var/log/syslog
Jul 11 01:27:56 supra kernel: [ 8683.334440] Hello, Eric! Your age is 35.

查看内核模块参数

$ cat /sys/module/hello/parameters/age
35
$ cat /sys/module/hello/parameters/name
Eric

改变内核模块参数

$ sudo echo 28 > /sys/module/hello/parameters/age
bash: /sys/module/hello/parameters/age: Permission denied

上面的权限问题, 是由于我们设置的参数权限导致的: S_IRUGO, 可以改它为 0660 就可以了.

写一个 Linux 内核 hello world 模块

本文介绍如何一步步写一个 Linux 内核 hello world 模块.

内核模块分类

分为如下2类:

  1. Builtin modules 内置模块, 包含在 Linux image 里面
  2. External modules 外部模块, 可以随时加载/卸载的内核模块.
    本文例子中的模块属于 外部模块.

Kernel Build System

更多 Linux 内核 Kbuild 系统的更多信息, 参看官方文档.

hello world 内核模块

创建一个 hello.c 文件. 源代码如下:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

static int __init hello_init(void)
{
    printk(KERN_ALERT "hello world\n");
    return 0;
}

static void __exit hello_exit(void)
{
    printk(KERN_ALERT "goodbye\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_DESCRIPTION("my first module");
MODULE_LICENSE("free");
MODULE_AUTHOR("Eric Tian");

设置内核 build 环境

安装 Kernel header: 找到对应的包, 然后安装, 然后查看安装的包.

$ apt search linux-headers-$(uname -r)
$ sudo apt install linux-headers-$(uname -r)

$ file /lib/modules/$(uname -r)/build
/lib/modules/5.15.92/build: symbolic link to /home/supra/work/jammy/jammy-Ubuntu-5.15.0-70.77-test

Makefile

在 hello.c 相同的目录, 创建一个新文件 Makefile. 内容如下, 缩进符号使用 tab.

obj-m += hello.o

tag ?= `uname -r`
KDIR := /lib/modules/${tag}/build/

all:
    make -C $(KDIR) M=$(PWD) modules

clean:
    make -C $(KDIR) M=$(PWD) clean

编译运行

$ make clean
$ make all
make -C /lib/modules/`uname -r`/build/ M=/home/supra/work/c/kernel modules
make[1]: Entering directory '/usr/src/linux-headers-5.15.0-76-generic'
  CC [M]  /home/supra/work/c/kernel/hello.o
  MODPOST /home/supra/work/c/kernel/Module.symvers
  CC [M]  /home/supra/work/c/kernel/hello.mod.o
  LD [M]  /home/supra/work/c/kernel/hello.ko
  BTF [M] /home/supra/work/c/kernel/hello.ko
Skipping BTF generation for /home/supra/work/c/kernel/hello.ko due to unavailability of vmlinux
make[1]: Leaving directory '/usr/src/linux-headers-5.15.0-76-generic'

查看模块信息

$ modinfo hello.ko
modinfo hello.ko
filename:       /home/supra/work/c/kernel/hello.ko
author:         Eric Tian
license:        free
description:    my first module
srcversion:     01F8F75DC7D8708707AA062
depends:
retpoline:      Y
name:           hello
vermagic:       5.15.0-76-generic SMP mod_unload modversions

运行模块

$ sudo insmod hello.ko
insmod: ERROR: could not insert module hello.ko: Operation not permitted

虽然你用了 sudo, 仍然得到上面的错误, 原因很可能是: Check if your system has secure boot enabled, which can prevent loading unsigned kernel modules. Disable secure boot in your system's BIOS/UEFI settings and try again.

解决上述问题后, 执行 insmod 然后通过 dmesg 查看日志信息.

$ sudo insmod hello.ko
$ sudo dmesg 

[  176.149789] hello: loading out-of-tree module taints kernel.
[  176.149794] hello: module license 'free' taints kernel.
[  176.149794] Disabling lock debugging due to kernel taint
[  176.149815] hello: module verification failed: signature and/or required key missing - tainting kernel
[  176.150529] hello world

查看加载的模块信息

$ lsmod | grep hello
Module                    Size   Used by
hello                  16384  0

$ sudo cat /proc/modules | grep hello
hello 16384 0 - Live 0xffffffffc0797000 (POE)

卸载模块 并查看 exit 日志

$ sudo rmmod hello
$ tail -n 10 /var/log/syslog
Jul 10 12:16:14 supra kernel: [  596.219986] goodbye

写一个有依赖的Linux 内核模块

接上一篇 写一个 Linux内核 hello world 模块, 这次我们写2个内核模块 hello & world, 并且 world 模块依赖于 hello 模块.

hello 模块

源代码: hello.c

#include <linux/module.h>
#include <linux/init.h>

static int __init hello_init(void)
{
        pr_info("hello module is loaded\n");
        return 0;
}

static void __exit hello_exit(void)
{
        pr_info("hello module is unloaded\n");
}

void say_hello(void)
{
        pr_info("hello ");
}

EXPORT_SYMBOL(say_hello);

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("free");

world 模块

源代码: world.c

#include <linux/module.h>
#include <linux/init.h>

void say_hello(void);

static int __init world_init(void)
{
        pr_info("world module is loaded\n");
    say_hello();
        pr_info("world\n");
        return 0;
}

static void __exit world_exit(void)
{
        pr_info("world module is unloaded\n");
}

module_init(world_init);
module_exit(world_exit);
MODULE_LICENSE("free");

Makefile

源代码: Makefile. 缩进使用 tab.

obj-m := world.o hello.o

KDIR := /lib/modules/`uname -r`/build/
PWD := $(shell pwd)

default:
    $(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
    $(MAKE) -C $(KDIR) M=$(PWD) clean

编译模块

$ make
make -C /lib/modules/`uname -r`/build/ M=/home/supra/work/modules/deps modules
make[1]: Entering directory '/home/supra/work/jammy/jammy-Ubuntu-5.15.0-70.77-test'
  CC [M]  /home/supra/work/modules/deps/world.o
  CC [M]  /home/supra/work/modules/deps/hello.o
  MODPOST /home/supra/work/modules/deps/Module.symvers
  CC [M]  /home/supra/work/modules/deps/hello.mod.o
  LD [M]  /home/supra/work/modules/deps/hello.ko
  BTF [M] /home/supra/work/modules/deps/hello.ko
  CC [M]  /home/supra/work/modules/deps/world.mod.o
  LD [M]  /home/supra/work/modules/deps/world.ko
  BTF [M] /home/supra/work/modules/deps/world.ko
make[1]: Leaving directory '/home/supra/work/jammy/jammy-Ubuntu-5.15.0-70.77-test'

加载模块

$ sudo insmod hello.ko

$ tail -n 1  /var/log/syslog
Jul 10 14:15:22 supra kernel: [ 3313.748762] hello module is loaded

$ sudo cat /proc/modules | grep hello
hello 16384 0 - Live 0xffffffffc0797000 (POE)

$ sudo insmod world.ko

$ tail -n 3 /var/log/syslog
Jul 10 14:19:22 supra kernel: [ 3553.077383] world module is loaded
Jul 10 14:19:22 supra kernel: [ 3553.077385] hello
Jul 10 14:19:22 supra kernel: [ 3553.077386] world

$ sudo cat /proc/modules | grep hello
hello 16384 1 world, Live 0xffffffffc0797000 (POE)

查看模块依赖

$ lsmod | grep hello
hello                  16384  1 world

$ lsmod | grep world
world                  16384  0
hello                  16384  1 world

相反顺序卸载模块

$ sudo rmmod world.ko
$ sudo rmmod hello.ko

$ tail -n 2 /var/log/syslog
Jul 10 14:26:00 supra kernel: [ 3949.339943] world module is unloaded
Jul 10 14:26:05 supra kernel: [ 3954.185122] hello module is unloaded

Mac 安装 VirtualBox, 创建 Ubuntu 虚拟机

为了学习 Linux Kernel 的准备工作, 要在本地安装 VirtualBox, 然后使用虚拟机. 即使把VM搞挂, 也不用担心.

MAC 安装 VirtualBox

  1. https://www.virtualbox.org/wiki/Downloads 下载合适的安装包.
  2. 双击下载的 dmg 文件, 按照步骤一步步安装.
  3. 启动 VirtualBox 程序.

VirtualBox 安装 Ubuntu

  1. https://ubuntu.com/download/server 下载 Ubuntu ISO image(我选的服务器版本, 不是桌面版).
  2. 在 VirtualBox 界面上面的菜单中 点击 New(新建), 填入名字, 选择ISO 文件, 下一步(next)
    portForward.png
  3. 设置 用户名/密码, 设置内存/CPU/虚拟磁盘, 查看设置, 完成. 过程中, MAC 可能问你要一些权限, 给.
  4. 然后安装 Ubuntu: 选择语言, 键盘, 一路next, 最后 安装完成. 选择 “reboot now”.
    在上面的步骤里, 其中有一步是 安装 ssh server, 注意要手动选上, 后面可以直接本地 ssh.
  5. 启动后, 输入刚才设置的 用户名/密码 就能登录了.

本地 ssh 连接

虽然上面是安装的服务器版本, 但是直接从 VirtualBox 的界面操作还是不方便, 最好是本地ssh 连接. 上面的安装步骤里 已经选择了安装 ssh 服务器, 如果你没有安装, 可以从 VirtualBox 的界面登录进入, 安装 ssh 服务.

要本地ssh进入, 必须设置本地 host 到 VM 的端口转发.
设置端口转发步骤如下:

  1. 如果虚拟机没有 power off, 先 power off shutdown now.
  2. 进入 VirtualBox 界面, 选择虚拟机, 右键, 点击设置(Settings), 然后选择 网络(network), 点开高级(Advanced), 点击 端口转发(Port Forwarding)
    portForward.png
  3. 点击 添加 按钮, 添加名字, 主机端口, Guest 端口, 其它留空. 点击 OK 保存.
    forwordDetail.png
  4. 双击 VM 启动VM.
  5. 然后本地 命令行 登录. 端口是刚才设置的, 用户名是VM的登录用户名.

     $ ssh -p 2222 supra@localhost

使用 ssh 登录 qemu 启动的 VM

在实验 https://github.com/int0x03/kernel-utils 的过程中, 我们最后通过 qemu 启动 img 文件, 然后通过我们预设的用户名/密码进入 shell. 但是这个shell 和启动VM 用的同一个tty, 所以系统的一些message 会源源不断的输出到这个tty, 如果你要在这个shell 改些东西, 会非常尴尬.

如何启动另外一个tty?

要启动另外一个tty, 我们可以通过 ssh 登录这个VM, 就需要安装 openssh-server, 所以我们更改 config/env.sh 在需要安装到软件那行, 加入 openssh-server.

改后:

packages_to_install="systemd-resolved bpftrace bpfcc-tools gdb iptables openssh-server"

另外需要本地端口做转换, 所以需要更改 qemu 的启动参数, 在 boot 文件改动如下:

netdev_args="user,id=network0,net=192.168.0.0/24 -device e1000,netdev=network0 -net nic -net user,hostfwd=tcp::2222-:22"

sshd server 默认是不让以 root 用户登录的, 所以要更改 /etc/ssh/sshd_config 运行root 访问:

PermitRootLogin yes

然后重启 sshd server

systemctl restart ssh

如何登录

在宿主机 ssh -p 2222 root@localhost