--- title: 新增 System Call 至 Linux 核心 tags: - Linux - OS description: 透過實際操作來了解如何新增一個 System call 至 Linux 核心。 --- # 新增 System Call 至 Linux 核心 在網路上尋找有關新增 system call 至 Linux 核心的文章時,發現有些文章所提供的方式無法順利執行,可能是核心版本的差異導致作法不同,因此想紀錄下自己嘗試後能夠成功的方法。 這篇文章除了是自己的筆記之外,也告訴大家如何新增一個簡單的 system call,並且透過 QEMU 運作。同時也感謝網路上許多大神的無私奉獻,提供各種教學文章。 :::warning 在本篇筆記的命令列範例中,若前綴為 $ 者,表示其執行在 host 端;而前綴為 # 者,表示其須執行在 guest 端 (QEMU 內) ::: 測試環境如下: ``` OS: Ubuntu 22.04 ARCH: X86_64 Linux Kernel Source Version: 6.6 ``` # Build Linux Kernel 首先需要安裝以下套件: ```shell $ sudo apt update && sudo apt upgrade $ sudo apt -y -q install \ bc \ flex \ bison \ build-essential \ expect \ git \ libncurses-dev \ libssl-dev \ libelf-dev \ u-boot-tools \ wget \ xz-utils \ qemu-kvm \ iproute2 \ python3 \ python3-pip ``` 新增一個專案資料夾並進入該資料夾。 ```shell $ mkdir -p linux-kernel && cd linux-kernel ``` 下載 kernel 的 source code 並且 build 起來,可以使用 wget 下載核心壓縮檔或者使用 git clone 取得原始碼,這裡使用 wget 作為示範e。 ```shell $ wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.6.tar.xz $ tar -xvf linux-6.6.tar.xz $ cd linux-6.6 $ make allnoconfig ``` > 使用 git clone: > ```shell > $ git clone --depth=5 https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux > $ cd linux > $ git checkout -b linux-6.1.y origin/linux-6.1.y > ``` 接著設定 config。 ```shell $ make menuconfig ``` 將下列所有選項都勾起來: ```shell Linux/x86 6.6.0 Kernel Configuration ├─ General Setup │ └─ Initial RAM filesystem and RAM disk (initramfs/initrd) support -> Enable ├─ 64-bit kernel -> Enable ├─ Process type and features │ └─ Linux guest support -> Enable │ └─ Support for running PVH guests -> Enable ├─ Executable file formats -> Enable all └─ Device Drivers └─ Character devices └─ Serial drivers ├─ 8250/16550 and compatible serial support -> Enable └─ Console on 8250/16550 and compatible serial port -> Enable ``` 編譯 kernel,參數 $(nproc) 代表系統最大核心數量。 ```shell $ make ARCH=x86 CROSS_COMPILE=x86_64-linux-gnu- -j$(nproc) ``` > 針對 Arm64 處理器架構,將指令更改為: > ```shell > $ make ARCH=arm64 CORSS_COMPILE=aarch64-linux-gun- -j$(nproc) > ``` 編譯結束後,預期會見到以下訊息: ```shell Kernel: arch/x86/boot/bzImage is ready ``` # Build Root FS 回到專案資料夾 `linux-kernel` 內下載 busybox 並編譯。 ```shell $ cd .. $ wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2 $ tar -xf busybox-1.36.1.tar.bz2 ``` 截至目前為止,專案資料夾結構應如下: ```shell linux-kernel ├─ busybox-1.36.1 └─ linux-6.6 ``` 移動至 `busybox-1.36.1` 資料夾內並執行 `make menuconfig`。 ```shell $ cd busybox-1.36.1 $ make menuconfig ``` 選擇 `Settings ---> Build static binary` 並執行。 ```shell $ make install ``` 接著要製作 mount 至 kernel 的資料夾。 ```shell $ cd _install $ mkdir -p lib lib64 proc sys etc etc/init.d ``` 寫入開機之後需要的腳本,首先利用 vim 建立 rcS 檔案。 ```shell $ vim ./etc/init.d/rcS ``` 將以下腳本寫入至 rcS 並儲存 (# 為註解符號,並非執行於 QEMU 內): ```shell #!/bin/sh # Mount the /proc and /sys filesystems mount -t proc none /proc mount -t sysfs none /sys # Populate /dev /sbin/mdev -s ``` 設定 rcS 腳本的權限並且建立 rootfs 的 image。 ```shell $ chmod +x etc/init.d/rcS $ find . | cpio -o --format=newc | gzip > ../../linux-6.6/rootfs.img.gz ``` 測試 kernel 是否能順利運作,首先移動至 linux-6.6 資料夾並啟動 QEMU 。 ```shell $ cd ../../linux-6.6 $ qemu-system-x86_64 -kernel vmlinux -nographic -initrd rootfs.img.gz -append "root=/dev/ram rdinit=/sbin/init console=ttyS0" ``` 當看到以下畫面,代表順利進入 QEMU 執行環境,可以輸入 `ls` 指令做確認: ![image](https://hackmd.io/_uploads/H1rh_i_fke.png) 若要離開測試環境,可以按下 Ctrl-A 放開再按下 X 按鍵。 # Add System Call ## 修改 `syscall_64.tbl` 首先打開 `arch/x86/entry/syscalls/syscall_64.tbl` 並且在 377 行後面新增我們自己的 system call 以及對應的編號。 ![image](https://hackmd.io/_uploads/HJA_p9Bmkg.png) 這行有 4 個部份,每項之間由空白或是 tab 相隔。,它們代表的意義是: - `454` system call number ,在使用系統呼叫所使用的數字。 - `common` 支援的 ABI ,只能是 `64` 、 `x32` 或 `common` ,分別表示「只支援 amd 64」、「只支援 x32」或是「都支援」。 - `my_syscall` system call 的名字 - `sys_my_syscall` system call 對應的實作,Linux 核心中通常會用 `sys` 開頭來代表 system call 的實作 在 `syscall_64.tbl` 檔案的最上方也相有對應的說明。 ``` # # 64-bit system call numbers and entry vectors # # The format is: # <number> <abi> <name> <entry point> # # The __x64_sys_*() stubs are created on-the-fly for sys_*() system calls # # The abi is "common", "64" or "x32" for this file. # ``` ## 修改 `syscalls.h` 開啟 `include/linux/syscalls.h` 並在 `asmlinkage long sys_map_shadow_stack();` 的後方 (約第 943 行) 加入宣告。 ```diff=939 asmlinkage long sys_cachestat(unsigned int fd, struct cachestat_range __user *cstat_range, struct cachestat __user *cstat, unsigned int flags); asmlinkage long sys_map_shadow_stack(unsigned long addr, unsigned long size, unsigned int flags); + asmlinkage long sys_my_syscall(); /* * Architecture-specific system calls */ /* x86 */ asmlinkage long sys_ioperm(unsigned long from, unsigned long num, int on); ``` ## 撰寫 system call 程式碼 新增一個檔案叫做 `my_syscall.c` ,路徑為 `kernel/my_syscall.c`。 ```c #include <linux/syscalls.h> SYSCALL_DEFINE0(my_syscall) { printk("Hello Linux"); return 0; } ``` > `SYSCALL_DEFINE0` 表示定義該 system call 無參數, `SYSCALL_DEFINE1` 代表可定義一個參數,依此類推。 e.g. > ```c > SYSCALL_DEFINE1(my_syscall, long, arg1) > ``` 接下來要將檔案新增到 makefile 裡面,開啟 `kernel/Makefile` 並將新增我們的 system call 。 ```makefile obj-y = fork.o exec_domain.o panic.o \ cpu.o exit.o softirq.o resource.o \ sysctl.o capability.o ptrace.o user.o \ signal.o sys.o umh.o workqueue.o pid.o task_work.o \ extable.o params.o \ kthread.o sys_ni.o nsproxy.o \ notifier.o ksysfs.o cred.o reboot.o \ async.o range.o smpboot.o ucount.o regset.o ksyms_common.o \ my_syscall.o ``` 再重新編譯一次 kernel。 ```shell $ make ARCH=x86 CROSS_COMPILE=x86_64-linux-gnu- -j$(nproc) ``` # 測試 System Call 接下來我們需要在外部先編譯靜態連結的測試程式碼,並呼叫我們所寫好的 system call 。首先在 `linux-6.6` 資料夾的外面 (專案資料夾內) 新增一個檔案叫做 `project1.c`。 ```shell $ cd .. $ vim project1.c ``` 在 `project1.c` 內輸入以下程式碼: ```c #include <unistd.h> long my_syscall() { return syscall(454); } int main() { my_syscall(); return 0; } ``` 到目前為止,專案資料夾內的檔案架構應如下: ```shell linux-kernel ├─ busybox-1.36.1 ├─ linux-6.6 └─ project1.c ``` 接下來編譯這個檔案。 ```shell $ gcc -static project1.c -o project1 ``` 將編譯完成的執行檔複製到 `busybox-1.36.1/_install` 內,並重新製作 image。 ```shell $ cd busybox-1.36.1/_install $ cp ../../project1 . $ find . | cpio -o --format=newc | gzip > ../../linux-6.6/rootfs.img.gz ``` # Run User Program 到 `linux-6.6` 底下再執行一次 QEMU。 ```shell $ cd ../../linux-6.6/ $ qemu-system-x86_64 -kernel vmlinux -nographic -initrd rootfs.img.gz -append "root=/dev/ram rdinit=/sbin/init console=ttyS0" ``` ![image](https://hackmd.io/_uploads/HJN64-mEke.png) 按下 enter 之後就可以開始輸入指令了,首先輸入 `ls` 。 ![image](https://hackmd.io/_uploads/SJrrB-mEJe.png) 可以看到我們的執行檔案 `project1` 在裡面,接下來直接執行它。 ![image](https://hackmd.io/_uploads/S1Rec-7NJx.png) # 參考資料 - [實作一個回傳物理位址的系統呼叫](https://hackmd.io/@Mes/make_phy_addr_syscall) - [Add system call to Linux 6.6 and test it in QEMU](https://hackmd.io/@ppodds/SyCuLJrmT) - [測試 Linux 核心的虛擬化環境](https://hackmd.io/@sysprog/linux-virtme) - [利用 QEMU 與 BusyBox 建立 Linux 測試環境](https://hackmd.io/@oscarshiang/linux-on-qemu-busybox)