系统调用是用户空间和内核空间之间的接口,是用户程序与操作系统之间的桥梁。

Linux 内核提供了一系列的系统调用,用户程序可以通过这些系统调用来请求内核执行某些操作。

比如,open 系统调用用于打开一个文件,read 系统调用用于读取文件内容,write 系统调用用于写入文件内容等。

本文将介绍如何为 Linux Kernel 添加一个新的系统调用。

System calls are the interface between user space and kernel space, serving as the bridge between user programs and the operating system.

The Linux kernel provides a series of system calls that user programs can use to request the kernel to perform certain operations.

For example, the open system call opens a file, the read system call reads file content, and the write system call writes file content.

This article explains how to add a new system call to the Linux kernel.

添加系统调用

为 Linux Kernel 添加系统调用的步骤如下:

  1. 修改 arch/x86/entry/syscalls/syscall_64.tbl 文件,添加新的系统调用号。

系统调用号是一个整数,用于标识系统调用。

具体来说,我们需要在 syscall_64.tbl 文件中添加一行,格式如下:

1
<number> <abi> <name> <entry point> [<compat entry point> [noreturn]]

number 是系统调用号, 它是一个整数, read 的系统调用号是 0, write 的系统调用号是 1, 以此类推
abi 的全称是 Application Binary Interface,它通常可以指定为 common64x32
name 是系统调用的名称
entry point 是系统调用的入口地址

我们在这个文件中添加一个新的系统调用,用来获取当前机器的 CPU 数量。

1
463 common  get_cpu_num         sys_get_cpu_num 

  1. include/linux/syscalls.h 文件中声明新的系统调用。

syscalls.h 文件中添加一个新的系统调用声明,格式如下:

1
asmlinkage long sys_get_cpu_num(void);

为什么要使用 asmlinkage 呢?asmlinkage 确保系统调用函数的参数通过堆栈传递,而不是通过寄存器。这在某些架构或特定调用约定下是必要的。尽管在 x86_64 架构上,系统调用的参数通常通过寄存器传递,但使用 asmlinkage 可以确保在不同架构上参数传递的一致性。

示例
假设我们有一个系统调用 sys_my_syscall,它接受两个参数 int arg1 和 int arg2。

在 x86_64 架构上
在 x86_64 架构上,系统调用的参数通常通过寄存器传递:

1
2
3
4
asmlinkage long sys_my_syscall(int arg1, int arg2) {
// 参数 arg1 和 arg2 通过寄存器传递
return arg1 + arg2;
}

在这个例子中,arg1 和 arg2 会通过 rdi 和 rsi 寄存器传递。

在 x86 (32-bit) 架构上
在 x86 (32-bit) 架构上,系统调用的参数通过堆栈传递:

1
2
3
4
asmlinkage long sys_my_syscall(int arg1, int arg2) {
// 参数 arg1 和 arg2 通过堆栈传递
return arg1 + arg2;
}

在这个例子中,arg1 和 arg2 会通过堆栈传递。

  1. kernel/sys.c 文件中实现新的系统调用。

sys.c 文件中实现新的系统调用,格式如下:

1
2
3
4
SYSCALL_DEFINE0(get_cpu_num)
{
return num_present_cpus();
}

SYSCALL_DEFINE0 是一个宏,用于定义一个参数个数为 0 的系统调用。

num_present_cpus 是一个内核函数,用于获取当前机器的 CPU 数量。

相应地,也存在 SYSCALL_DEFINE1SYSCALL_DEFINE2SYSCALL_DEFINE3 等宏,用于定义参数个数为 1、2、3 的系统调用。

例如:

1
2
3
4
5
SYSCALL_DEFINE1(umask, int, mask)
{
mask = xchg(&current->fs->umask, mask & S_IRWXUGO);
return mask;
}

SYSCALL_DEFINE1 宏用于定义一个参数个数为 1 的系统调用,umask 是系统调用的名称,int 是参数的类型,mask 是参数的名称。

测试系统调用

添加系统调用后需要重新编译内核,如果你不知道怎么编译内核,请先阅读 Linux Kernel 从编译到运行

  1. make mrproper 清理内核源码目录
  2. make menuconfig 配置内核

  1. make -j8 bzImage 编译内核

将编译好的内核镜像文件拷贝到前一篇文章workspace 目录下。

  1. 编写测试程序

workspace/initramfs 目录下创建一个测试程序 test_sys_call.c,内容如下:

1
2
3
4
5
6
7
8
9
10
#include <unistd.h>
#include <sys/syscall.h>
#include <stdio.h>

int main() {
printf("Pid = %ld\n", syscall(39));
long present_cpus = syscall(463);
printf("Number of present CPUs: %ld\n", present_cpus);
return 0;
}
  1. 编译测试程序

因为我们在 QEMU 中运行测试程序,所以在编译时需要采用静态链接的方式。

静态链接就是将程序所需要的库文件全部打包到可执行文件中,这样在运行时就不需要再去加载库文件。

1
gcc -static test_sys_call.c -o test_sys_call

此时我的workspace目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ tree -L 2
.
├── busybox
├── bzImage
├── initramfs
│   ├── bin
│   ├── etc
│   ├── init
│   ├── proc
│   ├── sbin
│   ├── sys
│   ├── test_sys_call
│   └── test_sys_call.c
├── initramfs.img
└── Makefile

7 directories, 7 files

  1. 运行测试程序

我们要在 QEMU 中运行测试程序,首先修改一下 Makefile 文件,为 QEMU 启动时设置 CPU 数量。

-smp n 参数用于设置 CPU 数量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.PHONY: initramfs run clean

initramfs:
cd initramfs && find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../initramfs.img

run:
qemu-system-x86_64 \
-kernel bzImage \
-initrd initramfs.img \
-nographic \
-append "console=ttyS0" \
-m 1024 \
-smp 1

clean:
rm -f initramfs.cpio initramfs.img

然后我们依次执行以下命令:

1
2
3
make clean
make initramfs
make run

启动 QEMU 后,我们可以在 QEMU 中运行测试程序:

修改 Makefile 文件中的 -smp 1-smp 4,然后重新执行 make run 命令,再次执行测试程序:

可以看到此时CPU数量变成了 4, 同时出现了 Kernel Panic,暂时先不管它。

修改 -smp 15,然后重新执行 make run 命令,再次执行测试程序:

可以看到此时CPU数量变成了 15。

总结

本文介绍了如何为 Linux Kernel 添加一个新的系统调用,以及如何测试系统调用。

  1. 系统调用是一个和CPU架构相关的概念,不同的CPU架构有不同的系统调用号。
  2. x86 架构下添加一个新的系统调用的步骤是:修改 arch/x86/entry/syscalls/syscall_64.tbl 文件,添加新的系统调用号;在 include/linux/syscalls.h 文件中声明新的系统调用;在 kernel/sys.c 文件中实现新的系统调用。

Adding a System Call

The steps to add a system call to the Linux kernel are as follows:

  1. Modify the arch/x86/entry/syscalls/syscall_64.tbl file to add a new system call number.

The system call number is an integer used to identify a system call.

Specifically, we need to add a line to the syscall_64.tbl file, with the format:

1
<number> <abi> <name> <entry point> [<compat entry point> [noreturn]]

number is the system call number, an integer. read has number 0, write has number 1, and so on
abi stands for Application Binary Interface; it can usually be specified as common, 64, or x32
name is the name of the system call
entry point is the entry address of the system call

We add a new system call to this file to get the number of CPUs on the current machine.

1
463 common  get_cpu_num         sys_get_cpu_num 

  1. Declare the new system call in the include/linux/syscalls.h file.

Add a new system call declaration to the syscalls.h file, with the format:

1
asmlinkage long sys_get_cpu_num(void);

Why use asmlinkage? asmlinkage ensures that the system call function’s parameters are passed via the stack rather than registers. This is necessary on certain architectures or specific calling conventions. Although on x86_64 system call parameters are usually passed via registers, using asmlinkage ensures consistency of parameter passing across different architectures.

Example
Suppose we have a system call sys_my_syscall that takes two parameters int arg1 and int arg2.

On x86_64
On x86_64, system call parameters are usually passed via registers:

1
2
3
4
asmlinkage long sys_my_syscall(int arg1, int arg2) {
// Parameters arg1 and arg2 are passed via registers
return arg1 + arg2;
}

In this example, arg1 and arg2 are passed via the rdi and rsi registers.

On x86 (32-bit)
On x86 (32-bit), system call parameters are passed via the stack:

1
2
3
4
asmlinkage long sys_my_syscall(int arg1, int arg2) {
// Parameters arg1 and arg2 are passed via the stack
return arg1 + arg2;
}

In this example, arg1 and arg2 are passed via the stack.

  1. Implement the new system call in the kernel/sys.c file.

Implement the new system call in the sys.c file, with the format:

1
2
3
4
SYSCALL_DEFINE0(get_cpu_num)
{
return num_present_cpus();
}

SYSCALL_DEFINE0 is a macro used to define a system call with 0 parameters.

num_present_cpus is a kernel function that gets the number of CPUs on the current machine.

Correspondingly, there are also SYSCALL_DEFINE1, SYSCALL_DEFINE2, SYSCALL_DEFINE3 and other macros, used to define system calls with 1, 2, 3 parameters.

For example:

1
2
3
4
5
SYSCALL_DEFINE1(umask, int, mask)
{
mask = xchg(&current->fs->umask, mask & S_IRWXUGO);
return mask;
}

The SYSCALL_DEFINE1 macro defines a system call with 1 parameter: umask is the name, int is the parameter type, and mask is the parameter name.

Testing the System Call

After adding the system call, you need to recompile the kernel. If you don’t know how to compile the kernel, please read Linux Kernel: From Compilation to Running first.

  1. make mrproper to clean the kernel source directory
  2. make menuconfig to configure the kernel

  1. make -j8 bzImage to compile the kernel

Copy the compiled kernel image to the workspace directory from the previous article.

  1. Write a test program

Create a test program test_sys_call.c in the workspace/initramfs directory, with the following content:

1
2
3
4
5
6
7
8
9
10
#include <unistd.h>
#include <sys/syscall.h>
#include <stdio.h>

int main() {
printf("Pid = %ld\n", syscall(39));
long present_cpus = syscall(463);
printf("Number of present CPUs: %ld\n", present_cpus);
return 0;
}
  1. Compile the test program

Since we run the test program in QEMU, we need to use static linking when compiling.

Static linking packs all the library files needed by the program into the executable, so there’s no need to load libraries at runtime.

1
gcc -static test_sys_call.c -o test_sys_call

At this point my workspace directory structure is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ tree -L 2
.
├── busybox
├── bzImage
├── initramfs
│   ├── bin
│   ├── etc
│   ├── init
│   ├── proc
│   ├── sbin
│   ├── sys
│   ├── test_sys_call
│   └── test_sys_call.c
├── initramfs.img
└── Makefile

7 directories, 7 files

  1. Run the test program

We want to run the test program in QEMU. First, modify the Makefile to set the number of CPUs when QEMU starts.

The -smp n parameter sets the number of CPUs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.PHONY: initramfs run clean

initramfs:
cd initramfs && find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../initramfs.img

run:
qemu-system-x86_64 \
-kernel bzImage \
-initrd initramfs.img \
-nographic \
-append "console=ttyS0" \
-m 1024 \
-smp 1

clean:
rm -f initramfs.cpio initramfs.img

Then run the following commands in order:

1
2
3
make clean
make initramfs
make run

After QEMU boots, we can run the test program in QEMU:

Change -smp 1 in the Makefile to -smp 4, then run make run again and execute the test program:

You can see the CPU count is now 4, and a Kernel Panic also appeared — ignore it for now.

Change to -smp 15, then run make run again and execute the test program:

You can see the CPU count is now 15.

Summary

This article introduced how to add a new system call to the Linux kernel and how to test it.

  1. System calls are CPU-architecture-dependent; different CPU architectures have different system call numbers.
  2. The steps to add a new system call on x86 are: modify the arch/x86/entry/syscalls/syscall_64.tbl file to add the new system call number; declare the new system call in the include/linux/syscalls.h file; implement the new system call in the kernel/sys.c file.