---
# System prepended metadata

title: 'Reading [The Linux Kernel Module Programming Guide](https://sysprog21.github.io/lkmpg/) (1)'
tags: [Linux]

---

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