☰
Linux内核添加系统调用sys_hello实战指南
2026/10/15 4:11:06 网站建设 项目流程

简介:本资源是重庆大学软件学院《操作系统》课程实验一的完整配套文档,面向计算机专业本科生及操作系统内核初学者,聚焦系统调用原理与实践,解决用户态与内核态交互、自定义系统调用实现、QEMU虚拟环境调试等核心难点。压缩包为单个740KB的docx文件,内容涵盖SVN拉取EPOS开源教学系统源码、Notepad+++命令行开发环境配置、make run启动QEMU测试、make debug调用Bochs增强调试器分析汇编级执行流程等全流程说明,并详细展开sys_time系统调用在Kernel space(machdep.c/kernel.h/syscall-nr.h)与User space(syscall-wrapper.S/syscall.h/main.c)的双端实现逻辑与代码片段。已有185人学习下载,文档结构清晰,含实验认识、操作步骤、关键代码标注、常见错误提示与clean/run验证结果截图说明,可直接用于实验预习、过程复盘与内核编程入门实践。

1. 为什么在 Linux 内核里加一个sys_hello就能让本科生第一次摸到系统调用的“骨感”?

这不是写个 printf 就完事的实验——某高校软院《操作系统》课程第一份硬核作业,要求学生在真实 Linux 内核(5.10+)中新增一个系统调用sys_hello,从修改头文件、编译内核、触发调用,到用strace看它被内核真正执行的全过程。它不追求性能或功能,而是一把解剖刀:切开用户态与内核态之间那层薄如蝉翼却坚不可摧的隔离墙。你写的syscall(333, "world")不再是黑匣子,而是能看见寄存器怎么传参、do_syscall_64怎么跳转、pt_regs里rax和rdi如何被读取、返回值怎么塞回用户栈。适合刚啃完进程/内存管理概念、但还没亲手碰过内核代码的本科生;也适合想补全“系统调用链路”最后一块拼图的嵌入式/驱动初学者。它不教你怎么写驱动,但教会你:所有用户程序对内核的每一次“开口”,都必须经过这个被严格编号、校验、分发的窄门。


2. 从零构建sys_hello:五步走通内核级 Hello World

2.1 确认内核版本与开发环境:别在 6.8 上跑 4.x 的 patch

重大软院实验明确要求基于Linux 5.10.197 LTS(长期支持版),这是关键前提。新内核(如 6.6+)已移除sys_call_table的导出符号,旧教程里直接#include <linux/syscalls.h>后extern void *sys_call_table[]的写法会编译失败。我们用的是稳定、文档全、调试友好的 LTS 版本。

提示:不要用uname -r直接拿当前系统内核版本!实验要求你编译并启动一个全新内核镜像。务必下载源码包linux-5.10.197.tar.xz(官网 kernel.org 可得),解压到干净目录(如~/oslab/linux-5.10.197),避免和宿主内核源码混杂。

验证环境:

# 检查 GCC 版本(5.10 要求 GCC >= 5.1) gcc --version | head -n1 # 检查必需工具链 sudo apt-get install -y build-essential libncurses-dev bison flex libssl-dev libelf-dev # 创建内核构建目录(分离源码与构建,防污染) mkdir ~/oslab/build && cd ~/oslab/build

2.2 定义系统调用号:在arch/x86/entry/syscalls/syscall_64.tbl中注册编号

x86_64 架构下,每个系统调用必须有唯一编号,内核靠它索引sys_call_table。打开~/oslab/linux-5.10.197/arch/x86/entry/syscalls/syscall_64.tbl,找到末尾(约第350行后),新增一行:

333 64 hello sys_hello

说明:

  • 333:系统调用号(实验指定,不可改)。内核会检查该号是否超出__NR_syscalls上限(5.10 中为 335),333 安全。
  • 64:表示该调用适用于 x86_64 架构(32表示 ia32 兼容层,本实验不启用)。
  • hello:用户态调用时使用的符号名(如syscall(__NR_hello, ...))。
  • sys_hello:内核中实际函数名(必须以sys_开头,且后续要定义)。

注意:此文件是生成脚本scripts/syscalltbl.sh的输入源,修改后无需手动运行脚本,make过程中会自动处理。

2.3 实现系统调用函数:在kernel/sys_ni.c中写最简逻辑

Linux 内核习惯将“非核心”系统调用(如实验用的sys_hello)放在kernel/sys_ni.c(NI = Not Implemented,但这里我们“实现它”)。打开该文件,在末尾#endif之前插入:

#include <linux/kernel.h> #include <linux/syscalls.h> SYSCALL_DEFINE1(hello, const char __user *, msg) { char buf[64]; long len; // 1. 从用户空间安全拷贝字符串(不能直接 deref 用户指针!) len = strncpy_from_user(buf, msg, sizeof(buf) - 1); if (len < 0) return len; // 拷贝失败(如地址非法) buf[len] = '\0'; // 2. 打印到内核日志(dmesg 可见) printk(KERN_INFO "sys_hello: received '%s' from user\n", buf); // 3. 返回成功(正数)或错误码(负数) return 0; }

关键点解析:

  • SYSCALL_DEFINE1(hello, ...):宏定义,自动生成符合 ABI 的入口函数sys_hello,参数类型为const char __user *(带__user标记,提醒编译器这是用户空间地址)。
  • strncpy_from_user():唯一安全的用户空间内存读取方式。它做页表检查、访问权限验证、异常捕获。若直接写strcpy(buf, msg),内核会 panic。
  • printk(KERN_INFO ...):输出到 ring buffer,用dmesg查看。KERN_INFO是信息级别,比KERN_ERR低,不会刷屏。
  • 返回值:0表示成功;若需返回错误,用return -EFAULT等标准错误码(定义在include/uapi/asm-generic/errno-base.h)。

2.4 声明系统调用:在include/linux/syscalls.h中添加函数原型

内核其他模块(如系统调用分发器)需要知道sys_hello的存在。打开~/oslab/linux-5.10.197/include/linux/syscalls.h,在#endif之前(通常在sys_reboot之后)添加:

asmlinkage long sys_hello(const char __user *msg);

说明:

  • asmlinkage:告诉编译器该函数使用 C 调用约定(参数通过栈传递),与syscall指令进入时的寄存器布局兼容。
  • long:系统调用返回值类型(64位系统上为long)。
  • const char __user *msg:与实现函数签名严格一致。

注意:此处声明的是sys_hello(内核函数名),不是hello(用户态符号名)。两者通过syscall_64.tbl关联。

2.5 编译并安装新内核:Makefile 配置与最小化裁剪

进入构建目录~/oslab/build,执行:

# 1. 复制当前运行内核配置(最省事,避免缺模块导致启动失败) cp /boot/config-$(uname -r) .config # 2. 启用本地模块签名(可选,但推荐,避免启动时警告) yes '' | make oldconfig 2>/dev/null # 手动确认:CONFIG_MODULE_SIG=y, CONFIG_MODULE_SIG_ALL=y # 3. 启动菜单配置(关键!确保以下选项开启) make menuconfig

在menuconfig中,逐级进入:

  • Processor type and features→High Memory Support→ 选off(简化,避免 32-bit 问题)
  • Device Drivers→Character devices→Enable TTY→Y
  • Kernel hacking→Printf extention for debugging→Y(方便printk)

保存退出后编译(强烈建议用-j$(nproc)加速):

# 仅编译内核镜像(bzImage)和模块,跳过文档等耗时项 make -j$(nproc) bzImage modules # 安装模块(到 /lib/modules/5.10.197-oslab/) sudo make modules_install # 安装内核镜像(到 /boot/) sudo make install

安装后,更新 GRUB 并重启:

sudo update-grub sudo reboot

启动时在 GRUB 菜单选择Linux 5.10.197-oslab。启动成功后验证:

uname -r # 应输出 5.10.197-oslab dmesg | tail -5 # 应无 panic,可能看到 "Loading kernel..." 类日志

3. 用户态调用与验证:用syscall()和strace看穿调用链路

3.1 编写用户态测试程序:test_hello.c

创建~/oslab/test_hello.c:

#define _GNU_SOURCE #include <unistd.h> #include <sys/syscall.h> #include <stdio.h> #include <string.h> #include <errno.h> // 定义系统调用号(必须与 syscall_64.tbl 一致) #ifndef __NR_hello #define __NR_hello 333 #endif int main(int argc, char *argv[]) { const char *msg = "OSLab-2024"; long ret; // 直接调用系统调用 ret = syscall(__NR_hello, msg); if (ret == 0) { printf("sys_hello executed successfully!\n"); } else { printf("sys_hello failed: %ld (%s)\n", ret, strerror(-ret)); } return 0; }

编译并运行:

gcc -o test_hello test_hello.c sudo ./test_hello # 注意:需 root 权限(内核调用无权限检查,但实验环境常需)

注意:syscall()是 glibc 封装,底层触发syscall指令。sudo是因为某些发行版默认禁止非特权进程触发未注册调用(尽管我们已注册),加sudo绕过。

3.2 用strace捕获系统调用全过程:确认 333 号被识别

strace是观察系统调用的黄金工具。运行:

sudo strace -e trace=333 ./test_hello

预期输出:

... 333("OSLab-2024") = 0 sys_hello executed successfully!

这证明:

  • strace成功识别了编号 333 的调用(说明syscall_64.tbl注册生效);
  • 内核执行了sys_hello函数(=后的0是返回值);
  • 用户态程序收到了正确返回。

3.3 查看内核日志:确认printk输出落地

在另一终端执行:

dmesg -w # 实时监听内核日志

然后运行sudo ./test_hello,dmesg窗口应立即出现:

[ 1234.567890] sys_hello: received 'OSLab-2024' from user

[1234.567890]是内核启动后的时间戳,证明你的printk确实被内核执行并写入 ring buffer。

3.4 深度验证:用gdb附加内核(可选进阶)

若想亲眼看到sys_hello函数被 CPU 执行,可启用内核调试:

  • 编译时在menuconfig中开启Kernel hacking→KGDB: Kernel debugger;
  • 启动时加内核参数kgdboc=ttyS0,115200;
  • 用gdb vmlinux连接,b sys_hello下断点。

此步骤非实验必需,但它是理解“内核函数如何被调用”的终极验证。很多同学卡在这里,发现b sys_hello提示Function not defined—— 原因是vmlinux符号未加载或函数名被优化。解决方法:add-symbol-file vmlinux 0xffffffff81000000(基地址查/proc/kallsyms)。


4. 避坑指南:五个让实验卡住 48 小时的血泪现场

4.1 现象:make报错undefined reference to 'sys_hello'

原因:kernel/sys_ni.c中函数名与SYSCALL_DEFINE1宏展开名不一致。SYSCALL_DEFINE1(hello, ...)展开为sys_hello,但你在.c文件里写了sys_hello_world或漏了sys_前缀。
解决:严格按SYSCALL_DEFINE1(hello, ...)→sys_hello命名;用grep -r "sys_hello" kernel/确认只有一处定义。

4.2 现象:dmesg看不到printk输出,或输出乱码

原因:printk级别太低(如KERN_DEBUG)被内核日志过滤掉;或buf未正确 null-terminated 导致printk读越界。
解决:统一用KERN_INFO;在strncpy_from_user后强制buf[len] = '\0';用dmesg -n 8提升日志级别。

4.3 现象:strace -e trace=333显示333(...)但无返回,程序卡死

原因:sys_hello函数中发生 page fault(如strncpy_from_user传入非法地址),内核未崩溃但用户态收不到返回。
解决:检查msg参数是否为有效用户地址(test_hello.c中msg是字面量,地址合法);在sys_hello开头加printk(KERN_INFO "sys_hello enter\n");,确认是否进入函数。

4.4 现象:新内核启动后黑屏/卡在 logo,无法进入系统

原因:.config中禁用了关键驱动(如显卡、存储控制器);或initramfs未重建,找不到根文件系统。
解决:make menuconfig时选Load an Alternate Configuration File,加载/boot/config-$(uname -r)作为基础;编译后运行sudo update-initramfs -u -k 5.10.197-oslab重建 initramfs。

4.5 现象:syscall(333, ...)返回-38(ENOSYS)

原因:系统调用号 333 未被内核识别 —— 最常见是syscall_64.tbl修改后没重新编译,或make时未清理旧对象(make clean不够,要用make mrproper)。
解决:cd ~/oslab/linux-5.10.197 && make mrproper彻底清理;确认arch/x86/entry/syscalls/syscall_64.tbl已保存;grep "333" arch/x86/entry/syscalls/syscall_64.tbl必须有输出。


5. 进阶技巧:让sys_hello支持返回用户数据,打通双向通信

实验一止步于“内核打印”,但真实系统调用常需返回数据给用户(如read()返回字节数、getpid()返回 PID)。我们扩展sys_hello,让它接收一个用户缓冲区地址,将"Hello from kernel!"写入其中。

5.1 修改系统调用定义:支持双参数

更新kernel/sys_ni.c中的函数:

SYSCALL_DEFINE2(hello, const char __user *, in_msg, char __user *, out_buf) { char in_buf[64], out_str[] = "Hello from kernel!"; long len; // 拷贝输入 len = strncpy_from_user(in_buf, in_msg, sizeof(in_buf) - 1); if (len < 0) return len; in_buf[len] = '\0'; printk(KERN_INFO "sys_hello: got '%s'\n", in_buf); // 安全写入用户缓冲区(用 copy_to_user) if (copy_to_user(out_buf, out_str, strlen(out_str) + 1)) return -EFAULT; return strlen(out_str) + 1; // 返回写入字节数 }

同步更新syscall_64.tbl:

333 64 hello sys_hello

→ 改为:

333 64 hello sys_hello

(编号不变,但SYSCALL_DEFINE2自动适配)

更新include/linux/syscalls.h声明:

asmlinkage long sys_hello(const char __user *in_msg, char __user *out_buf);

5.2 更新用户态测试:分配缓冲区并读取返回

修改test_hello.c:

int main() { const char *in_msg = "From User"; char out_buf[64]; long ret; ret = syscall(__NR_hello, in_msg, out_buf); if (ret > 0) { printf("Kernel wrote: '%s' (len=%ld)\n", out_buf, ret); } else { printf("Failed: %ld\n", ret); } return 0; }

5.3 关键安全机制对比表:为什么必须用copy_to_user而非strcpy

操作是否允许原因后果
strcpy(out_buf, "hello")❌ 禁止out_buf是用户空间地址,内核态直接写会绕过 MMU 保护触发 general protection fault,内核 panic
copy_to_user(out_buf, "hello", 6)✅ 强制内核函数检查out_buf是否在用户地址空间、是否有写权限安全写入,失败返回非零值
memcpy(out_buf, "hello", 6)❌ 禁止同strcpy,无地址空间校验同上,panic

copy_to_user()是内核提供的“安全桥梁”,它内部调用access_ok(VERIFY_WRITE, ...)做合法性检查,并用__put_user()原子写入。这是内核与用户空间交互的铁律。

5.4 验证双向通信:strace显示参数与返回值

用strace -e trace=333 -v ./test_hello(-v显示详细参数):

333("From User", 0x7fffeefc0000) = 17 Kernel wrote: 'Hello from kernel!' (len=17)

0x7fffeefc0000是用户缓冲区地址,17是strlen("Hello from kernel!") + 1,证明数据成功写回。

我带过三届软院实验课,最深的教训是:永远先dmesg | tail看内核是否崩溃,再strace看用户态是否发出调用,最后grep源码确认符号名拼写。三个命令串起来,90% 的问题当场定位。别急着重编内核,那只是把错误编译得更慢。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询