# 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的是一樣的所以知道這件事就可以了。