---
# System prepended metadata

title: 新增 System Call 至 Linux 核心
tags: [Linux, OS]

---

---
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)