新增 System Call 至 Linux 核心

新增 System Call 至 Linux 核心

在網路上尋找有關新增 system call 至 Linux 核心的文章時,發現有些文章所提供的方式無法順利執行,可能是核心版本的差異導致作法不同,因此想紀錄下自己嘗試後能夠成功的方法。

這篇文章除了是自己的筆記之外,也告訴大家如何新增一個簡單的 system call,並且透過 QEMU 運作。同時也感謝網路上許多大神的無私奉獻,提供各種教學文章。

在本篇筆記的命令列範例中,若前綴為 $ 者,表示其執行在 host 端;而前綴為 # 者,表示其須執行在 guest 端 (qemu 內)

測試環境如下:

OS: Ubuntu 22.04
ARCH: X86_64
Source Version: 6.6

Build Linux Kernel

首先需要安裝以下套件:

$ 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 \
qemu-kvm \
iproute2 \
python3 \
python3-pip

新增一個專案資料夾並進入該資料夾。

$ mkdir -p linux-kernel && cd linux-kernel

下載 kernel 的 source code 並且 build 起來,可以使用 wget 下載核心壓縮檔或者使用 git clone 取得原始碼,這裡使用 wget 作為示範e。

$ 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:

$ 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。

$ make menuconfig

將下列所有選項都勾起來:

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) 代表系統最大核心數量。

$ make ARCH=x86 CROSS_COMPILE=x86_64-linux-gnu- -j$(nproc)

針對 Arm64 處理器架構,將指令更改為:

$ make ARCH=arm64 CORSS_COMPILE=aarch64-linux-gun- -j$(nproc)

編譯結束後,預期會見到以下訊息:

Kernel: arch/x86/boot/bzImage is ready

Build Root FS

回到專案資料夾 linux-kernel 內下載 busybox 並編譯。

$ cd ..
$ wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2
$ tar -xf busybox-1.36.1.tar.bz2

截至目前為止,專案資料夾結構應如下:

linux-kernel
├─ busybox-1.36.1
└─ linux-6.6

移動至 busybox-1.36.1 資料夾內並執行 make menuconfig

$ cd busybox-1.36.1
$ make menuconfig

選擇 Settings ---> Build static binary 並執行。

$ make install

接著要製作 mount 至 kernel 的資料夾。

$ cd _install
$ mkdir -p lib lib64 proc sys etc etc/init.d

寫入開機之後需要的腳本,首先利用 vim 建立 rcS 檔案。

$ vim ./etc/init.d/rcS

將以下腳本寫入至 rcS 並儲存:

#!/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。

$ chmod +x etc/init.d/rcS
$ find . | cpio -o --format=newc | gzip > ../../linux-6.6/rootfs.img.gz

測試 kernel 是否能順利運作,首先移動至 linux-6.6 資料夾並啟動 qemu 。

$ 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

若要離開測試環境,可以按下 Ctrl-A 放開再按下 X 按鍵。

Add System Call

修改 syscall_64.tbl

首先打開 arch/x86/entry/syscalls/syscall_64.tbl 並且在 377 行後面新增我們自己的 system call 以及對應的編號。

image

這行有 4 個部份,每項之間由空白或是 tab 相隔。,它們代表的意義是:

  • 454
    System call numver ,在使用系統呼叫所使用的數字。
  • common
    支援的 ABI ,只能是 64x32common ,分別表示「只支援 amd 64」、「只支援 x32」或是「都支援」。
  • my_get_physical_addresses
    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 行) 加入宣告。

939
940
941
942
943
944
945
946
947
948
949
950
951
  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

#include <linux/syscalls.h>

SYSCALL_DEFINE0(my_syscall)
{
printk("Hello Linux");
return 0;
}

SYSCALL_DEFINE0 表示定義該 system call 無參數, SYSCALL_DEFINE1 代表可定義一個參數,依此類推。
e.g.

SYSCALL_DEFINE1(my_syscall, long, arg1)

接下來要將檔案新增到 makefile 裡面,開啟 kernel/Makefile 並將新增我們的 system call 。

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。

$ make ARCH=x86 CROSS_COMPILE=x86_64-linux-gnu- -j$(nproc)

測試 System Call

接下來我們需要在外部先編譯靜態連結的測試程式碼,並呼叫我們所寫好的 system call 。首先在 linux-6.6 資料夾的外面 (專案資料夾內) 新增一個檔案叫做 project1.c

$ cd ..
$ vim project1.c

project1.c 內輸入以下程式碼:

#include <unistd.h>

long my_syscall()
{
return syscall(454);
}

int main()
{
my_syscall();
return 0;
}

到目前為止,專案資料夾內的檔案架構應如下:

linux-kernel
├─ busybox-1.36.1
├─ linux-6.6
└─ project1.c

接下來編譯這個檔案。

$ gcc -static project1.c -o project1

將編譯完成的執行檔複製到 busybox-1.36.1/_install 內,並重新製作 image。

$ 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。

$ 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

按下 enter 之後就可以開始輸入指令了,首先輸入 ls

image

可以看到我們的執行檔案 project1 在裡面,接下來直接執行它。

image

參考資料