Making a “Hello World” System Call
Our goal is to make a system call (syscall) that takes a user-space string buffer as its input, prepends “Hello “ to it, and then copies it to another user-space string buffer.
The prototype of the syscall is:
int hello(const char *input, size_t in_sz, char *output, size_t out_sz);
where “input” is the input buffer to read the input string from, “in_size” is the size of the input buffer, excluding the null-terminator, “output” is the output buffer to place the new string to, and “out_size” is the size of the output buffer.
Start simple
Since the syscall we are creating seems to provide generic functionality, not necessarily tied to any specific core kernel subsystem, we will put its implementation just inside the kernel sub-directory, and it will look something like this:
// SPDX-License-Identifier: GPL-2.0
/*
* hello system call implementation
*/
#include <linux/kernel.h>
#include <linux/syscalls.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#define HELLO_MSG "Hello "
/**
*
* Documentation will go here.
*
*/
static int do_k22_hello(const char *input, size_t in_sz, char *output,
size_t out_sz)
{
pr_info("I miss Alexis Delis! But I managed to invoke a dummy syscall...\n");
return 0;
}
SYSCALL_DEFINE4(k22_hello, const char __user *, input, size_t, in_sz, char __user *,
output, size_t, out_sz)
{
return do_k22_hello(input, in_sz, output, out_sz);
}
The above is a generic template, and it is a good idea to follow it when writing syscalls. Assuming the name of the file containing the implementation of your syscall is named “hello_syscall.c” and it is properly placed inside a fresh 6.14 Linux kernel source code tree, with no additional commits, create a new branch and name it lab0.
$ git checkout -b lab0
Switched to a new branch 'lab0'
$ git log
commit 38fec10eb60d687e30c8c6b5420d86e8149f7557 (grafted, HEAD -> lab0, tag: v6.14, hmkw1)
Author: Linus Torvalds <torvalds@linux-foundation.org>
Date: Mon Mar 24 07:02:41 2025 -0700
Linux 6.14
$ git status
On branch lab0
Untracked files:
(use "git add <file>..." to include in what will be committed)
hello_syscall.c
nothing added to commit but untracked files present (use "git add" to track)
After adding and committing the draft implementation to your branch:
$ git log
commit d8784fb6767b001befb4611c6a34f859ee208719 (HEAD -> lab0)
Author: vaggelis <valtidak@di.uoa.gr>
Date: Thu Oct 16 12:25:25 2025 +0300
adding draft
commit 38fec10eb60d687e30c8c6b5420d86e8149f7557 (grafted, tag: v6.14, hmkw1)
Author: Linus Torvalds <torvalds@linux-foundation.org>
Date: Mon Mar 24 07:02:41 2025 -0700
Linux 6.14
Your local commit(s) should differ from the remote references for Linux kernel v6.14, as follows:
$ git diff refs/tags/v6.14
diff --git a/kernel/hello_syscall.c b/kernel/hello_syscall.c
new file mode 100644
index 000000000..c447ab328
--- /dev/null
+++ b/kernel/hello_syscall.c
@@ -0,0 +1,28 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * hello system call implementation
+ */
+
+#include <linux/kernel.h>
+#include <linux/syscalls.h>
+#include <linux/uaccess.h>
+#include <linux/slab.h>
+
+#define HELLO_MSG "Hello "
+
+/**
+ *
+ * Documentation will go here.
+ *
+ */
+static int do_k22_hello(const char *input, size_t in_sz, char *output,
+ size_t out_sz)
+{
+ pr_info("I miss Alexis Delis! But I managed to invoke a dummy syscall...\n");
+ return 0;
+}
+
+SYSCALL_DEFINE4(k22_hello, const char __user *, input, size_t, in_sz, char __user *,
+ output, size_t, out_sz)
+{
+ return do_k22_hello(input, in_sz, output, out_sz);
+}
Expose the syscall
To make our syscall system-wide usable, we need to define it with a corresponding number in the syscall table, so that the OS know to invoke it when a user programs request to trap to it. Since the way the OS traps to execute syscalls is architecture-specific functionality (see slides from Lecture 03, page 22), the required changes must be somewhere inside the arch sub-directories.
- If your computer has an ARM64 CPU the changes should go under
arch/arm64/tools/syscall_64.tbl:
463 common setxattrat sys_setxattrat
464 common getxattrat sys_getxattrat
465 common listxattrat sys_listxattrat
466 common removexattrat sys_removexattrat
467 common sys_k22_hello sys_k22_hello <----
- If you computer has an x86_64 CPU the changes should go under
arch/x86/entry/syscalls/syscall_64.tbl:
464 common getxattrat sys_getxattrat
465 common listxattrat sys_listxattrat
466 common removexattrat sys_removexattrat
477 common sys_k22_hello sys_k22_hello <----
Build the kernel with it
Moreover, we should let the kernel’s build system know that I want the source code file with the implementation of the new syscall to be compiled and linked. Therefore, we need to update the Makefile inside the kernel sub-directory.
$ git diff Makefile
diff --git a/kernel/Makefile b/kernel/Makefile
index 87866b037..a151a2145 100644
--- a/kernel/Makefile
+++ b/kernel/Makefile
@@ -10,7 +10,7 @@ obj-y = fork.o exec_domain.o panic.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
+ async.o range.o smpboot.o ucount.o regset.o ksyms_common.o hello_syscall.o
obj-$(CONFIG_USERMODE_DRIVER) += usermode_driver.o
obj-$(CONFIG_MULTIUSER) += groups.o
Although so far the changes we have made are “dummy” (and there is no actual syscall implementation, just a placeholder), what we have should at least compile and link successfully to a patched kernel. The complete patch (i.e., the changes on top of the original state of the source code of the repository) should, so far, look like this:
- For ARM64:
$ git diff refs/tags/v6.14
diff --git a/kernel/Makefile b/kernel/Makefile
index 87866b037..a151a2145 100644
--- a/kernel/Makefile
+++ b/kernel/Makefile
@@ -10,7 +10,7 @@ obj-y = fork.o exec_domain.o panic.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
+ async.o range.o smpboot.o ucount.o regset.o ksyms_common.o hello_syscall.o
obj-$(CONFIG_USERMODE_DRIVER) += usermode_driver.o
obj-$(CONFIG_MULTIUSER) += groups.o
diff --git a/kernel/hello_syscall.c b/kernel/hello_syscall.c
new file mode 100644
index 000000000..afa3bec44
--- /dev/null
+++ b/kernel/hello_syscall.c
@@ -0,0 +1,28 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * hello system call implementation
+ */
+
+#include <linux/kernel.h>
+#include <linux/syscalls.h>
+#include <linux/uaccess.h>
+#include <linux/slab.h>
+
+#define HELLO_MSG "Hello "
+
+/**
+ *
+ * Documentation will go here.
+ *
+ */
+static int do_k22_hello(const char *input, size_t in_sz, char *output,
+ size_t out_sz)
+{
+ pr_info("I miss Alexis Delis! But I managed to invoke a dummy syscall...\n");
+ return 0;
+}
+
+SYSCALL_DEFINE4(k22_hello, const char __user *, input, size_t, in_sz, char __user *,
+ output, size_t, out_sz)
+{
+ return do_k22_hello(input, in_sz, output, out_sz);
+}
diff --git a/scripts/syscall.tbl b/scripts/syscall.tbl
index ebbdb3c42..1d4d5d7db 100644
--- a/scripts/syscall.tbl
+++ b/scripts/syscall.tbl
@@ -407,3 +407,4 @@
464 common getxattrat sys_getxattrat
465 common listxattrat sys_listxattrat
466 common removexattrat sys_removexattrat
+467 common sys_k22_hello sys_k22_hello
Note: The file arch/arm64/tools/syscall_64.tbl is actually a symlink to scripts/syscall.tbl. For this reason, when generating the patch, the diff shows the target file (scripts/syscall.tbl) instead of the symlink path we originally edited. Both refer to the same underlying file, so the modification is applied correctly.
- For x86_64:
$ git diff main
diff --git a/arch/x86/entry/syscalls/syscall_64.tbl b/arch/x86/entry/syscalls/syscall_64.tbl
index 5eb708bff..adc82debd 100644
--- a/arch/x86/entry/syscalls/syscall_64.tbl
+++ b/arch/x86/entry/syscalls/syscall_64.tbl
@@ -390,6 +390,7 @@
464 common getxattrat sys_getxattrat
465 common listxattrat sys_listxattrat
466 common removexattrat sys_removexattrat
+477 common sys_k22_hello sys_k22_hello
#
# Due to a historical design error, certain syscalls are numbered differently
diff --git a/kernel/Makefile b/kernel/Makefile
index 87866b037..a151a2145 100644
--- a/kernel/Makefile
+++ b/kernel/Makefile
@@ -10,7 +10,7 @@ obj-y = fork.o exec_domain.o panic.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
+ async.o range.o smpboot.o ucount.o regset.o ksyms_common.o hello_syscall.o
obj-$(CONFIG_USERMODE_DRIVER) += usermode_driver.o
obj-$(CONFIG_MULTIUSER) += groups.o
diff --git a/kernel/hello_syscall.c b/kernel/hello_syscall.c
new file mode 100644
index 000000000..4ed94a0d8
--- /dev/null
+++ b/kernel/hello_syscall.c
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * hello system call implementation
+ */
+
+#include <linux/kernel.h>
+#include <linux/syscalls.h>
+#include <linux/uaccess.h>
+#include <linux/slab.h>
+
+#define HELLO_MSG "Hello "
+
+/**
+ *
+ * Documentation will go here.
+ *
+ */
+static int do_k22_hello(const char *input, size_t in_sz, char *output,
+ size_t out_sz)
+{
+ pr_info("I miss Alexis Delis! But I managed to invoke a dummy syscall...\n");
+ return 0;
+}
+
+SYSCALL_DEFINE4(k22_hello, const char __user *, input, size_t, in_sz, char __user *,
+ output, size_t, out_sz)
+{
+ return do_k22_hello(input, in_sz, output, out_sz);
+}
Boot with the patched kernel
After booting to the patched kernel (see instructions here), you must at the very least be successfully able to invoke the dummy syscall from a user-space program.
$ cat ./test1.c
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
int main(void) {
int ret = syscall(467, (char *)0, 0, (char *)0, 0);
printf("ret: %d\n", ret);
if (ret < 0)
perror("syscall error:");
return 0;
}
$ gcc -o test test.c
$ ./test
ret: 0
$ sudo dmesg | grep -i "delis"
[ 23.308410] I miss Alexis Delis! But I managed to invoke a dummy syscall...
Implement a correct and robust syscall
If all goes well, we are ready to add the intended syscall implementation. The complete patch will look as follows:
- For ARM64:
$ pwd
/home/k22/linux-6.14
$ git diff refs/tags/v6.14 | cat
diff --git a/kernel/Makefile b/kernel/Makefile
index 87866b037..a151a2145 100644
--- a/kernel/Makefile
+++ b/kernel/Makefile
@@ -10,7 +10,7 @@ obj-y = fork.o exec_domain.o panic.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
+ async.o range.o smpboot.o ucount.o regset.o ksyms_common.o hello_syscall.o
obj-$(CONFIG_USERMODE_DRIVER) += usermode_driver.o
obj-$(CONFIG_MULTIUSER) += groups.o
diff --git a/kernel/hello_syscall.c b/kernel/hello_syscall.c
new file mode 100644
index 000000000..fecb92a0e
--- /dev/null
+++ b/kernel/hello_syscall.c
@@ -0,0 +1,106 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * hello system call implementation
+ */
+
+#include <linux/kernel.h>
+#include <linux/syscalls.h>
+#include <linux/uaccess.h>
+#include <linux/slab.h>
+
+#define HELLO_MSG "Hello "
+
+/**
+ *
+ * hello: Reads a string <s> and prepends "Hello " to it
+ * -
+ * @input: The input buffer to read the input string from
+ * @in_size: The size of the input buffer, excluding the null-terminator
+ *
+ * @output: The output buffer to place the new string at
+ * @out_size: The size of the output buffer
+ *
+ * This syscall returns an error if the generated string does not fit in the
+ * output buffer, or if (in_sz + 1) including the null terminator does not
+ * fit within 2 pages, or if the output buffer does not fit within 2 pages;
+ * otherwise it returns the length of the new string on success.
+ *
+ * NOTE: This function calls kmalloc with the GFP_KERNEL flag on, which means it
+ * could sleep. Therefore, the caller of this function should probably not invoke
+ * it while holding a spinlock...
+ *
+ */
+static int do_k22_hello(const char *input, size_t in_sz, char *output,
+ size_t out_sz)
+{
+ int return_code;
+ int buf_in_sz;
+ int actual_in_sz;
+ int expected_out_sz;
+ char *input_kbuf;
+ char *output_kbuf;
+
+ pr_info("I miss Alexis Delis! But I managed to invoke a syscall...\n");
+
+ if (!input || !output || in_sz == 0 || in_sz >= 2*PAGE_SIZE || out_sz == 0) {
+ return_code = -EINVAL;
+ goto out;
+ }
+
+ /* Allocate buffer to store user-provided string */
+ buf_in_sz = in_sz + 1;
+ input_kbuf = kmalloc(buf_in_sz, GFP_KERNEL);
+ if (!input_kbuf) {
+ return_code = -ENOMEM;
+ goto out;
+ }
+
+ input_kbuf[in_sz] = '\0';
+
+ /* Safely copy user-provided string from user space to kernel space */
+ if (copy_from_user(input_kbuf, input, in_sz)) {
+ return_code = -EFAULT;
+ goto free_input;
+ }
+
+ actual_in_sz = strlen(input_kbuf);
+ expected_out_sz = strlen(HELLO_MSG) + actual_in_sz + 1;
+
+ /* Make sure output fits into the output buffer */
+ if (expected_out_sz > out_sz || expected_out_sz > 2*PAGE_SIZE) {
+ return_code = -EINVAL;
+ goto free_input;
+ }
+
+ /* Allocate the output buffer */
+ output_kbuf = kmalloc(expected_out_sz, GFP_KERNEL);
+ if (!output_kbuf) {
+ return_code = -ENOMEM;
+ goto free_input;
+ }
+
+ /* Format desired string into the temporary output buffer */
+ snprintf(output_kbuf, expected_out_sz, HELLO_MSG "%s", input_kbuf);
+
+ /* Write resulting string back to user */
+ if (copy_to_user(output, output_kbuf, expected_out_sz)) {
+ return_code = -EFAULT;
+ goto free_output;
+ }
+
+ return_code = expected_out_sz;
+
+free_output:
+ kfree(output_kbuf);
+
+free_input:
+ kfree(input_kbuf);
+out:
+ return return_code;
+}
+
+SYSCALL_DEFINE4(k22_hello, const char __user *, input, size_t, in_sz, char __user *,
+ output, size_t, out_sz)
+{
+ return do_k22_hello(input, in_sz, output, out_sz);
+}
diff --git a/scripts/syscall.tbl b/scripts/syscall.tbl
index ebbdb3c42..1d4d5d7db 100644
--- a/scripts/syscall.tbl
+++ b/scripts/syscall.tbl
@@ -407,3 +407,4 @@
464 common getxattrat sys_getxattrat
465 common listxattrat sys_listxattrat
466 common removexattrat sys_removexattrat
+467 common k22_hello sys_k22_hello
- For x86_64:
$ pwd
/home/k22/linux-6.14
$ git diff main | cat
diff --git a/arch/x86/entry/syscalls/syscall_64.tbl b/arch/x86/entry/syscalls/syscall_64.tbl
index 5eb708bff..adc82debd 100644
--- a/arch/x86/entry/syscalls/syscall_64.tbl
+++ b/arch/x86/entry/syscalls/syscall_64.tbl
@@ -390,6 +390,7 @@
464 common getxattrat sys_getxattrat
465 common listxattrat sys_listxattrat
466 common removexattrat sys_removexattrat
+477 common k22_hello sys_k22_hello
#
# Due to a historical design error, certain syscalls are numbered differently
diff --git a/kernel/Makefile b/kernel/Makefile
index 87866b037..a151a2145 100644
--- a/kernel/Makefile
+++ b/kernel/Makefile
@@ -10,7 +10,7 @@ obj-y = fork.o exec_domain.o panic.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
+ async.o range.o smpboot.o ucount.o regset.o ksyms_common.o hello_syscall.o
obj-$(CONFIG_USERMODE_DRIVER) += usermode_driver.o
obj-$(CONFIG_MULTIUSER) += groups.o
diff --git a/kernel/hello_syscall.c b/kernel/hello_syscall.c
new file mode 100644
index 000000000..8ac38391f
--- /dev/null
+++ b/kernel/hello_syscall.c
@@ -0,0 +1,107 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * hello system call implementation
+ */
+
+#include <linux/kernel.h>
+#include <linux/syscalls.h>
+#include <linux/uaccess.h>
+#include <linux/slab.h>
+
+#define HELLO_MSG "Hello "
+
+/**
+ *
+ * hello: Reads a string <s> and prepends "Hello " to it
+ * -
+ * @input: The input buffer to read the input string from
+ * @in_size: The size of the input buffer, excluding the null-terminator
+ *
+ * @output: The output buffer to place the new string at
+ * @out_size: The size of the output buffer
+ *
+ * This syscall returns an error if the generated string does not fit in the
+ * output buffer, or if (in_sz + 1) including the null terminator does not
+ * fit within 2 pages, or if the output buffer does not fit within 2 pages;
+ * otherwise it returns the length of the new string on success.
+ *
+ * NOTE: This function calls kmalloc with the GFP_KERNEL flag on, which means it
+ * could sleep. Therefore, the caller of this function should probably not invoke
+ * it while holding a spinlock...
+ *
+ */
+static int do_k22_hello(const char *input, size_t in_sz, char *output,
+ size_t out_sz)
+{
+ int return_code;
+ int buf_in_sz;
+ int actual_in_sz;
+ int expected_out_sz;
+ char *input_kbuf;
+ char *output_kbuf;
+
+ pr_info("I miss Alexis Delis! But I managed to invoke a syscall...\n");
+
+ if (!input || !output || in_sz == 0 || in_sz >= 2*PAGE_SIZE || out_sz == 0) {
+ return_code = -EINVAL;
+ goto out;
+ }
+
+ /* Allocate buffer to store user-provided string */
+ buf_in_sz = in_sz + 1;
+ input_kbuf = kmalloc(buf_in_sz, GFP_KERNEL);
+ if (!input_kbuf) {
+ return_code = -ENOMEM;
+ goto out;
+ }
+
+ input_kbuf[in_sz] = '\0';
+
+ /* Safely copy user-provided string from user space to kernel space */
+ if (copy_from_user(input_kbuf, input, in_sz)) {
+ return_code = -EFAULT;
+ goto free_input;
+ }
+
+ actual_in_sz = strlen(input_kbuf);
+ expected_out_sz = strlen(HELLO_MSG) + actual_in_sz + 1;
+
+ /* Make sure output fits into the output buffer */
+ if (expected_out_sz > out_sz || expected_out_sz > 2*PAGE_SIZE) {
+ return_code = -EINVAL;
+ goto free_input;
+ }
+
+ /* Allocate the output buffer */
+ output_kbuf = kmalloc(expected_out_sz, GFP_KERNEL);
+ if (!output_kbuf) {
+ return_code = -ENOMEM;
+ goto free_input;
+ }
+
+ /* Format desired string into the temporary output buffer */
+ snprintf(output_kbuf, expected_out_sz, HELLO_MSG "%s", input_kbuf);
+
+ /* Write resulting string back to user */
+ if (copy_to_user(output, output_kbuf, expected_out_sz)) {
+ return_code = -EFAULT;
+ goto free_output;
+ }
+
+ return_code = expected_out_sz;
+
+free_output:
+ kfree(output_kbuf);
+
+free_input:
+ kfree(input_kbuf);
+out:
+ return return_code;
+}
+
+SYSCALL_DEFINE4(k22_hello, const char __user *, input, size_t, in_sz, char __user *,
+ output, size_t, out_sz)
+{
+ return do_k22_hello(input, in_sz, output, out_sz);
+}
+
After booting to the patched kernel with the proper syscall implementation, executing the user-space test program should now look like this:
$ ./test1
ret: -1
syscall error:: Invalid argument
Checking your patch for linting conformance errors
Finally, we will use the “checkpatch” script to confirm that our patch is conformant to the Linux kernel coding style.
$ pwd
/home/k22/linux-6.14
$ git diff refs/tags/v6.14 | ./scripts/checkpatch.pl
WARNING: added, moved or deleted file(s), does MAINTAINERS need updating?
#15:
new file mode 100644
total: 0 errors, 1 warnings, 118 lines checked
NOTE: For some of the reported defects, checkpatch may be able to
mechanically convert to the typical style using --fix or --fix-inplace.
Your patch has style problems, please review.
NOTE: If any of the errors are false positives, please report
them to the maintainer, see CHECKPATCH in MAINTAINERS.
Authors
A. Kostas, C. Komis, C. Kotsis, and V. Atlidakis.