「Kernel」Linux kernel lab 浅浅跟随
在此之前
在这篇文章中,我们将跟随 Linux Kernel Teaching,进行由浅入深的内核学习,以适应未来(可能出现的)内核开发工作。
值得注意的是,这个课程也拥有 中文版本,你可以在 linux-kernel-labs-zh/docs-linux-kernel-labs-zh-cn 进行 star 以支持他们的工作。
在接下来的博客中,我可能仅对 课程 部分进行简述,重复抄写已有内容而不加自己的思考总是没有意义的。我们的重点将放在 实验 部分。
基础设施
在这一节中,我们将准备实验环境。我使用 WSL2 内的 Docker 作为实验环境,这无疑是非常方便的。
curl -LO https://raw.githubusercontent.com/linux-kernel-labs-zh/so2-labs/main/local.sh
chmod +x ./local.sh
sudo ./local.sh docker interactive
之后,通过设置环境变量 LABS
,使用 make skels
即可生成不同的实验骨架。例如:
root@MuelNova-Laptop:/linux/tools/labs# LABS=kernel_modules make skels -j$(nproc)
mkdir -p skels
cd templates && find kernel_modules -type f | xargs ./generate_skels.py --output ../skels --todo 0
skel kernel_modules/5-oops-mod/oops_mod.c
skel kernel_modules/5-oops-mod/Kbuild
skel kernel_modules/3-error-mod/err_mod.c
skel kernel_modules/3-error-mod/Kbuild
skel kernel_modules/1-2-test-mod/hello_mod.c
skel kernel_modules/1-2-test-mod/Kbuild
skel kernel_modules/9-dyndbg/dyndbg.c
skel kernel_modules/9-dyndbg/Kbuild
skel kernel_modules/8-kdb/hello_kdb.c
skel kernel_modules/8-kdb/Kbuild
skel kernel_modules/7-list-proc/list_proc.c
skel kernel_modules/7-list-proc/Kbuild
skel kernel_modules/4-multi-mod/mod1.c
skel kernel_modules/4-multi-mod/mod2.c
skel kernel_modules/4-multi-mod/Kbuild
skel kernel_modules/6-cmd-mod/cmd_mod.c
skel kernel_modules/6-cmd-mod/Kbuild
rm -f skels/Kbuild
root@MuelNova-Laptop:/linux/tools/labs# ls skels/kernel_modules/
1-2-test-mod 3-error-mod 4-multi-mod 5-oops-mod 6-cmd-mod 7-list-proc 8-kdb 9-dyndbg
具体说明可以看 https://github.com/linux-kernel-labs-zh/so2-labs
wsl2 环境
由于我的 WSL2 开启了 mirrored 模式,导致它的 console 模式进不去,我花了一些时间进行探索,可以看 #3。
简单而言就是把已使用的网段换了一个没使用的网段。
diff --git a/tools/labs/qemu/Makefile b/tools/labs/qemu/Makefile
index e9ee4ec1b..6e10ac6f0 100644
--- a/tools/labs/qemu/Makefile
+++ b/tools/labs/qemu/Makefile
@@ -135,7 +135,7 @@ rootfs: $(YOCTO_ROOTFS)
printf '%s\n' '#!/bin/sh' '/bin/login -f root' > rootfs/sbin/rootlogin
chmod +x rootfs/sbin/rootlogin
mkdir -p rootfs/home/root/skels
- echo "//10.0.2.1/skels /home/root/skels cifs port=4450,guest,user=dummy 0 0" >> rootfs/etc/fstab
+ echo "//172.31.2.1/skels /home/root/skels cifs port=4450,guest,user=dummy 0 0" >> rootfs/etc/fstab
echo "hvc0:12345:respawn:/sbin/getty 115200 hvc0" >> rootfs/etc/inittab
$(YOCTO_ROOTFS):
diff --git a/tools/labs/qemu/create_net.sh b/tools/labs/qemu/create_net.sh
index c97b6fa0a..f803ed1e4 100755
--- a/tools/labs/qemu/create_net.sh
+++ b/tools/labs/qemu/create_net.sh
@@ -18,7 +18,7 @@ case "$device" in
subnet=172.30.0
;;
"lkt-tap-smbd")
- subnet=10.0.2
+ subnet=172.31.2
;;
*)
echo "Unknown device" 1>&2
diff --git a/tools/labs/qemu/run-qemu.sh b/tools/labs/qemu/run-qemu.sh
index 9938ec18e..abd245be1 100755
--- a/tools/labs/qemu/run-qemu.sh
+++ b/tools/labs/qemu/run-qemu.sh
@@ -24,7 +24,7 @@ case "$mode" in
;;
gui)
# QEMU_DISPLAY = sdl, gtk, ...
- qemu_display="-display ${QEMU_DISPLAY:-"sdl"}"
+ qemu_display="-display ${QEMU_DISPLAY:-"gtk"}"
linux_console=""
;;
checker)
@@ -56,13 +56,13 @@ linux_loglevel=${LINUX_LOGLEVEL:-"15"}
linux_term=${LINUX_TERM:-"TERM=xterm"}
linux_addcmdline=${LINUX_ADD_CMDLINE:-""}
-linux_cmdline=${LINUX_CMDLINE:-"root=/dev/cifs rw ip=dhcp cifsroot=//10.0.2.1/rootfs,port=4450,guest,user=dummy $linux_console loglevel=$linux_loglevel pci=noacpi $linux_term $linux_addcmdline"}
+linux_cmdline=${LINUX_CMDLINE:-"root=/dev/cifs rw ip=dhcp cifsroot=//172.31.2.1/rootfs,port=4450,guest,user=dummy $linux_console loglevel=$linux_loglevel pci=noacpi $linux_term $linux_addcmdline"}
user=$(id -un)
cat << EOF > "$SAMBA_DIR/smbd.conf"
[global]
- interfaces = 10.0.2.1
+ interfaces = 172.31.2.1
smb ports = 4450
private dir = $SAMBA_DIR
bind interfaces only = yes
VS-Code 开发环境
我建立了一个 vsc 的环境。首先 VSC 可以直接用 dev contained 连容器,然后装 clangd
Method1
在容器环境里,装 clang 和 bear
root@MuelNova-Laptop:/linux# apt install -y bear clang
之后生成 compile_commands.json
root@MuelNova-Laptop:/linux# bear make CC=clang
然后打开 remote 的 settings.json,加这么一句
{
"clangd.arguments": [
"--compile-commands-dir=/linux"
]
}
注意你需要再重新 make 一下,不然里面环境就炸了。
Method2
下起来太麻烦了,直接创一个 compile_commands.json
写入
[
{
"arguments": [
"clang",
"-c",
"-Wp,-MMD,scripts/mod/.empty.o.d",
"-nostdinc",
"-isystem",
"/usr/lib/llvm-10/lib/clang/10.0.0/include",
"-I./arch/x86/include",
"-I./arch/x86/include/generated",
"-I./include",
"-I./arch/x86/include/uapi",
"-I./arch/x86/include/generated/uapi",
"-I./include/uapi",
"-I./include/generated/uapi",
"-include",
"./include/linux/kconfig.h",
"-include",
"./include/linux/compiler_types.h",
"-D__KERNEL__",
"-Qunused-arguments",
"-fmacro-prefix-map=./=",
"-Wall",
"-Wundef",
"-Werror=strict-prototypes",
"-Wno-trigraphs",
"-fno-strict-aliasing",
"-fno-common",
"-fshort-wchar",
"-fno-PIE",
"-Werror=implicit-function-declaration",
"-Werror=implicit-int",
"-Werror=return-type",
"-Wno-format-security",
"-std=gnu89",
"-no-integrated-as",
"-Werror=unknown-warning-option",
"-mno-sse",
"-mno-mmx",
"-mno-sse2",
"-mno-3dnow",
"-mno-avx",
"-m32",
"-msoft-float",
"-mregparm=3",
"-freg-struct-return",
"-fno-pic",
"-mstack-alignment=4",
"-march=i686",
"-Wa,-mtune=generic32",
"-ffreestanding",
"-Wno-sign-compare",
"-fno-asynchronous-unwind-tables",
"-mretpoline-external-thunk",
"-fno-delete-null-pointer-checks",
"-Wno-address-of-packed-member",
"-O2",
"-Wframe-larger-than=1024",
"-fstack-protector-strong",
"-Wno-format-invalid-specifier",
"-Wno-gnu",
"-mno-global-merge",
"-Wno-unused-const-variable",
"-fno-omit-frame-pointer",
"-fno-optimize-sibling-calls",
"-g",
"-gdwarf-4",
"-Wdeclaration-after-statement",
"-Wvla",
"-Wno-pointer-sign",
"-Wno-array-bounds",
"-fno-strict-overflow",
"-fno-stack-check",
"-Werror=date-time",
"-Werror=incompatible-pointer-types",
"-fcf-protection=none",
"-Wno-initializer-overrides",
"-Wno-format",
"-Wno-sign-compare",
"-Wno-format-zero-length",
"-Wno-tautological-constant-out-of-range-compare",
"-DKBUILD_MODFILE=\"scripts/mod/empty\"",
"-DKBUILD_BASENAME=\"empty\"",
"-DKBUILD_MODNAME=\"empty\"",
"-o",
"scripts/mod/empty.o",
"scripts/mod/empty.c"
],
"directory": "/linux",
"file": "scripts/mod/empty.c"
}
]
然后打开 remote 的 settings.json,加这么一句
{
"clangd.arguments": [
"--compile-commands-dir=/linux/tools/lab"
]
}
Kernel Modules
实验目标
- 创建简单的模块
- 描述内核模块编译的过程
- 展示如何在内核中使用模块
- 简单的内核调试方法
0. 引言
使用 cscope 或 LXR 在 Linux 内核源代码中查找以下符号的定义:
module_init()
和module_exit()
这两个宏的作用是什么?init_module
和cleanup_module
是什么?ignore_loglevel
这个变量用于什么?
Docker 已经配好了虚拟机,所以可以直接用 cscope 来搜。
vim -t module_init
但是这样搜出来的都是引用而不是定义,所以我们还是用 Linux source code (v6.9.9) - Bootlin 搜吧
/* Each module must use one module_init(). */
#define module_init(initfn) \
static inline initcall_t __maybe_unused __inittest(void) \
{ return initfn; } \
int init_module(void) __copy(initfn) \
__attribute__((alias(#initfn))); \
___ADDRESSABLE(init_module, __initdata);
/* This is only required if you want to be unloadable. */
#define module_exit(exitfn) \
static inline exitcall_t __maybe_unused __exittest(void) \
{ return exitfn; } \
void cleanup_module(void) __copy(exitfn) \
__attribute__((alias(#exitfn))); \
___ADDRESSABLE(cleanup_module, __exitdata);
#endif
宏真是难看。这里定义了一个 __inittest 函数,返回我们传入的 initfn 指针,在模块插入后作为入口函数调用。之后,它定义了一个 int 类型的函数 init_module,这里 __copy
宏设置了 __copy__
属性,同时设置了别名为 #initfn,这些用于给编译器提供信息。
module_exit
也是类似,我们不再解释。
对于 ignore_loglevel
,字面意义,就是忽略日志等级,全部输出。
static bool __read_mostly ignore_loglevel;
static bool suppress_message_printing(int level)
{
return (level >= console_loglevel && !ignore_loglevel);
}
1. 内核模块
使用 make console 启动虚拟机,并执行以下任务:
- 加载内核模块。
- 列出内核模块并检查当前模块是否存在。
- 卸载内核模块。
- 使用 dmesg 命令查看加载/卸载内核模块时显示的消息。
首先我们生成骨架
LABS=kernel_modules make skels
注意有一个 skels 是 error-mod,意味着它有错误,因此我们先去删除它,等到后面修复了我们再生成它。
root@MuelNova-Laptop:/linux/tools/labs# rm skels/kernel_modules/3-error-mod/ -r
root@MuelNova-Laptop:/linux/tools/labs# make build
root@MuelNova-Laptop:/linux/tools/labs# make console
按理来说 make console 就可以直接进入了,但是我按回车没用。于是我们先 make copy 将驱动复制进入虚拟机,make boot 生成虚拟机,然后再手动连接
# tmux 1
make boot
# tmux 2
minicom -D serial.pts
# <回车>
Poky (Yocto Project Reference Distro) 2.3 qemux86 /dev/hvc0
qemux86 login: root
root@qemux86:~/skels/kernel_modules/1-2-test-mod# insmod hello_mod.ko
Hello!
root@qemux86:~/skels/kernel_modules/1-2-test-mod# lsmod
Tainted: G
hello_mod 16384 0 - Live 0xd085f000 (O)
root@qemux86:~/skels/kernel_modules/1-2-test-mod# rmmod hello_mod.ko
Goodbye!
2. Printk
观察虚拟机控制台。为什么消息直接显示在虚拟机控制台上?
配置系统,使消息不直接显示在串行控制台上,只能使用 dmesg 命令来查看。
观察代码,可以看到它是 pr_debug,也就是 loglevel=7,我们可以查看 /proc/sys/kernel/printk
的等级
root@qemux86:~# cat /proc/sys/kernel/printk
15 4 1 7
可以看到当前等级是 14,默认输出的日志等级是 4,最低等级是 1,默认控制台日志等级为 7
我们直接把他改成 4 就好
root@qemux86:~# insmod skels/kernel_modules/1-2-test-mod/hello_mod.ko
Hello!
root@qemux86:~# rmmod skels/kernel_modules/1-2-test-mod/hello_mod.ko
Goodbye!
root@qemux86:~# echo 4 > /proc/sys/kernel/printk
root@qemux86:~# insmod skels/kernel_modules/1-2-test-mod/hello_mod.ko
root@qemux86:~# rmmod skels/kernel_modules/1-2-test-mod/hello_mod.ko
root@qemux86:~#
3. 错误
生成名为 3-error-mod 的任务的框架。编译源代码并得到相应的内核模块。
为什么会出现编译错误? 提示: 这个模块与前一个模块有什么不同?
修改该模块以解决这些错 误的原因,然后编译和测试该模块。
生成它的代码
LABS=kernel_modules/3-error-mod make skels
首先我们对它进行编译,看看报错是什么
/linux/tools/labs/skels/./kernel_modules/3-error-mod/err_mod.c:5:20: error: expected declaration specifiers or '...' before string constant
5 | MODULE_DESCRIPTION("Error module");
| ^~~~~~~~~~~~~~
/linux/tools/labs/skels/./kernel_modules/3-error-mod/err_mod.c:6:15: error: expected declaration specifiers or '...' before string constant
6 | MODULE_AUTHOR("Kernel Hacker");
| ^~~~~~~~~~~~~~~
/linux/tools/labs/skels/./kernel_modules/3-error-mod/err_mod.c:7:16: error: expected declaration specifiers or '...' before string constant
7 | MODULE_LICENSE("GPL");
可以看到,似乎是这些函数的入参出了问题,那么很有可能是没引入头。
在 1 的里面是
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
在 3 的里面少了一个 <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
查询这些宏定义,可以发现都是来自于 include/linux/module.h
,因此我们加入这个头就好
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
root@qemux86:~/skels/kernel_modules/3-error-mod# ls err_mod.ko
root@qemux86:~/skels/kernel_modules/3-error-mod# insmod err_mod.ko
err_mod: loading out-of-tree module taints kernel.
n1 is 1, n2 is 2