# Reading [The Linux Kernel Module Programming Guide](https://sysprog21.github.io/lkmpg/) (1)
學習linux kernel module,我是用qemu riscv來跑這次的範例程式一開始先記錄環境的配置與用到的工具。
* 下載gnu toolchain(編譯kernel和module)
* `sudo apt install qemu-system-misc qemu-system-riscv`
* 如果要做bare-metal programming可以下載
`sudo apt install gcc-riscv64-unknown-elf`
(但是要小心很多linux kernel的功能就不能用,也不能用它來編kernel)
* 其他會用到的工具
* `sudo apt install make libncurses-dev flex bison libssl-dev bc`
* 下載linux
* `git clone https://github.com/torvalds/linux.git`
* 編譯kernel,編譯完的kernel Image會在`linux/arch/riscv/boot/Image`,另外這邊會跑很久
```
make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- defconfig
make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- -j$(nproc)
make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- modules_prepare
```
* Filesystem tools (BusyBox 1.36.1版):
* `git clone https://github.com/mirror/busybox.git`
* 編譯filesystem tools,編譯完的檔案會在 `busybox-1.36.1/_install/`
(簡單來說裡面會有各種user操作filesystem的工具,常見的指令如cd, ls, mount, umount ..,沒有的話你就要自己寫要跑的程式,然後放進init script讓他跑,如果init跑完所有要求的程式之後就會kernel panic)
```
make distclean
make menuconfig ARCH=riscv
# Enable: Settings → Build static binary (no shared libs)
make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- all install
```
* 接下來要做filesystem的空間
1.複製_install/底下的東西到你想要的位置,假設是`FSPATH`
2.另外要寫個init讓OS開機時知道要做什麼,"init" script可以放在`FSPATH`下面,qemu開機會去找到它執行,init的內容我放在下面
3.`dd if=/dev/zero of=rootfs.img bs=1M count=256` <- 建立並初始化為0的filesystem的空間,大小為256M (block size = 1M, 有256個,這裡可以自己調整)
4.`mkfs.ext4 rootfs.img` <- setting rootfs.img to ext4 format
5.建立一個資料夾並mount rootfs.img在上面
6.將BusyBox編好的工具複製進去,接著umount
```
mkdir MOUNTPATH
sudo mount rootfs.img MOUNTPATH
sudo cp -a FSPATH/* MOUNTPATH/
sudo umount MOUNTPATH
```
結束之後MOUNTPATH下面要補上dev,sys,proc的路徑,像是:
```
~ # ls
bin dev init proc sbin sys usr
```
"init" script (這邊主要就是開機後開一個bash介面,注意這個bash也是由BusyBox提供的程式,放在/bin/sh,但是這樣寫會有一些問題,我們留到後面講tty的章節會在說明,目前這樣設定可以跑一些例子的):
```
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t debugfs none /sys/kernel/debug
echo -e "\n\nWelcome to BusyBox RootFS on RISC-V!"
/bin/sh
```
到這邊filesystem就完成了,重點是之後如果要加kernel module,或是你寫好程式要放上去跑,都要把rootfs.img mount上host的作業系統並且將編譯好的檔案放進去qemu裡面的OS才讀的到,因為單純kernel是沒有編譯器在上面的,只能在host系統上編譯完再丟到qemu裡面的OS去跑
啟動qemu,裡面的Image, rootfs.img, /init 的位置要自己調整
```
qemu-system-riscv64 \
-machine virt \
-nographic \
-m 512M \
-kernel Image \
-append "root=/dev/vda rw console=ttyS0 init=/init" \
-drive file=rootfs.img,format=raw,id=hd0 \
-device virtio-blk-device,drive=hd0 \
-bios default
```
至此算是告一個小段落,如果有漏掉的可以跟我說
## Introduction
這邊書上先打預防針怕你沒法跑它的例子,可能的問題會是
* Modversioning 在編譯時沒開(不了解,書上寫之後會交代)
* SecureBoot 因為安全問題可能會擋掉不受信任的kernel module
(以上問題我在使用qemu是沒遇到)
<!-- header的設定也是透過`make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- modules_prepare`編譯kernel的時候就解決了 -->
## Hello World
接下來就要跑hello world,直接使用它的例子,但是要修改Makefile,因為這邊要指定gnu linux riscv的編譯器
* Makefile
```
ARCH = riscv
CROSS_COMPILE = riscv64-linux-gnu-
KERNEL_DIR = LINUX_SOURCE_CODE_PATH
PWD := $(CURDIR)
obj-m += hello-1.o
all:
make -C $(KERNEL_DIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) modules
clean:
make -C $(KERNEL_DIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) clean
```
* obj-m 代表編譯的是external module
* obj-y 編譯進kernel image裡面(由memuconfig設定)
再將底下的.ko檔搬進去rootfs.img裡面
啟動qemu
執行`insmod hello-1.ko`掛上module
`lsmod` 查看現有kernel module
`rmmod hello-1.ko` 移除hello-1.ko
另外書上提到sudo make和沒有sudo的差別,權限不一樣時PWD的表現會差很多(要考慮到安全因素,sudo預設會env_reset)
給一個例子給大家玩玩,觀察輸出哪裡不同
```
all:
echo $(PWD)
# if use sudo, PWD is empty
echo $(CURDIR)
# if use sudo, CURDIR is work
```
## init_module() vs __init (cleanup_module() vs __exit)
使用__init 可以替代 init_module() (__exit 同理)
在include/linux/module.h中定義如下
```
/* 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);
```
上面的code給了module_init的接口將沒有定義的init_module用initfn替代,所以用`module_init(hello_2_init);`定義kernel module的入口。
如果是要編譯進kernel可以不用module_exit因為module不能被unload,使用__exit的話編譯器則會自動忽略,省下編譯的空間。
要定義module的使用協議及作者資料可以用
```
MODULE_LICENSE("GPL");
MODULE_AUTHOR("LKMPG");
MODULE_DESCRIPTION("A sample driver");
```
## Passing Command Line Arguments to a Module
如果要在Load module時設定參數,要用module_param(),module_param_string(),或module_param_array()
這邊的話,定義就自己去查,比較要提的就是permission,這裡的permission是指這個參數在module load上kernel之後誰有權限改動,可以在`/sys/module/YOURMODULE/parameters/` 底下找到你的parameters要查看的話可以直接`cat YOURPARAM`(如果你有權限的話)。這邊注意到如果你設成0000的權限,參數就不會列在這了,雖然你可以在load module時設定它,但沒辦法在之後查看和更改。
第四章的最後說的是kernel版本及設定和編譯module的設定不一樣的話會被拒絕,但我這邊用的kernel跟編譯module的是一樣的所以知道這件事就可以了。