☰
Linux内核系统调用开发:从sys_hello到ftrace深度验证
2026/10/15 5:01:21 网站建设 项目流程

简介:本资源是重庆大学软件学院《操作系统》课程的实验一配套文档,面向计算机专业本科生及操作系统初学者,聚焦系统调用原理与内核级编程实践。内容完整覆盖EPOS嵌入式POSIX操作系统的环境搭建、SVN源码获取、QEMU虚拟机运行与Bochs调试全流程,并详解如何在Kernel Space(machdep.c/kernel.h/syscall-nr.h)和User Space(syscall-wrapper.S/syscall.h/main.c)协同实现自定义time系统调用,包括汇编接口封装、C语言调用约定及时间戳返回逻辑。资源为1个740KB的docx文档,结构清晰,含实验步骤、代码片段、关键截图说明与常见问题提示,便于对照源码理解系统调用从用户态到内核态的完整链路。已有185人学习下载,适合需要动手深化OS底层机制认知、掌握跨空间函数调用与调试技巧的学习者。

1. 为什么在 Linux 内核里加一个sys_hello就能让学生当场怀疑人生:软院操作系统实验一的真实水深

“重大软院操作系统实验一:系统调用”——这行字出现在实验手册第一页时,多数人只当是照着文档敲几行汇编、改个 Makefile 就能交差的入门任务。直到第一次make -j4报出undefined reference to 'sys_hello',dmesg | tail看不到任何输出,strace ./test显示unimplemented syscall 333,才意识到:这不是在用户态写个printf,而是在内核的呼吸间隙里,亲手缝合一条通往用户空间的神经通路。这个实验真正考的,不是“会不会改代码”,而是“能不能看懂内核怎么把一次int 0x80或syscall指令,拆解成寄存器搬运、表索引、函数跳转、权限校验、返回值封装这一整套黑匣子流程”。它面向的是刚啃完《现代操作系统》前四章、但还没摸过真实内核源码的本科生;目标不是做出工业级系统调用,而是让getpid()调用路径在你脑中能具象成userspace → entry_SYSCALL_64 → sys_call_table[20] → sys_getpid → return这样可追踪的链条。如果你正卡在“编译不通过”“调用无响应”“返回值总是 -1”,说明你已经踩进这个实验最硬的三块石头:ABI 对齐、符号导出、调用号注册——它们不写在实验指导书里,但决定你能否在 deadline 前看到那一行Hello from kernel!。


2. 从零手撕sys_hello:基于 Linux 5.10 内核的最小可行路径

这个实验不是让你魔改整个内核,而是以“最小侵入、最易验证”为原则,在稳定内核版本上打一个可复现、可调试、可回滚的补丁。我们选 Linux 5.10(LTS 版本,社区支持久、文档全、学生机常见),所有操作均在干净虚拟机(Ubuntu 20.04 + GCC 9.4)中验证。核心动作只有三步:定义系统调用函数、注册到系统调用表、暴露给用户态调用。每一步都必须和内核 ABI(Application Binary Interface)严丝合缝,否则就是静默失败。

2.1 在kernel/目录下新增hello.c并实现核心逻辑

不要试图把函数塞进sys.c或fork.c这类已有文件——那会增加 diff 复杂度,也违背“最小修改”原则。新建独立文件更利于定位问题、后续清理。

// kernel/hello.c #include <linux/kernel.h> #include <linux/syscalls.h> #include <linux/uaccess.h> SYSCALL_DEFINE0(hello) { printk(KERN_INFO "Hello from kernel!\n"); return 0; }

逻辑说明:

  • SYSCALL_DEFINE0是内核提供的宏,专用于定义无参数系统调用(0表示参数个数)。它自动处理寄存器保存、返回值封装、错误码转换等底层细节,比手写asmlinkage long sys_hello(void)更安全、更符合当前内核惯例。
  • printk(KERN_INFO "...")是内核态唯一可靠的日志输出方式,KERN_INFO级别确保默认可见(dmesg可查),且不依赖用户态syslogd。
  • 关键点:函数名必须是hello(非sys_hello),因为SYSCALL_DEFINE0(hello)宏会展开为sys_hello符号,但注册时只认hello这个逻辑名。

2.2 修改kernel/Makefile,将hello.o编译进内核镜像

内核构建系统不会自动扫描新文件,必须显式声明编译目标。打开kernel/Makefile,找到类似obj-y += sched/core.o的行,在其后添加:

obj-y += hello.o

参数说明:

  • obj-y表示该模块强制编译进内核镜像(vmlinux),而非作为可加载模块(.ko)。系统调用必须静态链接,否则sys_call_table初始化时无法解析符号。
  • 若误写为obj-m += hello.o,编译虽通过,但sys_hello符号根本不会进入vmlinux,后续注册必失败。这是新手第一大翻车点。

2.3 在arch/x86/entry/syscalls/syscall_64.tbl中分配并注册调用号

x86_64 架构使用独立的系统调用表文件,必须为你的调用分配一个未被占用的编号。打开arch/x86/entry/syscalls/syscall_64.tbl,找到最后一行(如332 common pkey_mprotect sys_pkey_mprotect),在其后新增:

333 64 hello sys_hello

字段说明(制表符分隔):

  • 第一列333:系统调用号。Linux 5.10 当前最大号为 332,因此 333 是安全选择(实验环境无需考虑冲突,生产环境需查tools/syscalltbl/工具生成)。
  • 第二列64:架构标识,64表示仅 x86_64 支持(common表示通用,32表示 i386)。实验限定 x86_64,填64即可。
  • 第三列hello:系统调用名称,必须与SYSCALL_DEFINE0(hello)中的名字完全一致(小写、无下划线)。
  • 第四列sys_hello:内核函数符号名,由SYSCALL_DEFINE0宏自动生成,不可手写为hello或my_hello。
  • 致命陷阱:若此处写成sys_hello但hello.c里用SYSCALL_DEFINE0(my_hello),链接时sys_call_table将指向一个不存在的符号,内核启动即 panic。

2.4 重新编译并安装新内核(关键验证步骤)

完成上述修改后,执行标准内核构建流程。注意:不要跳过make modules_install,部分符号依赖模块符号表:

# 清理旧对象(避免缓存干扰) make clean # 配置:直接复用当前运行内核配置(最稳) cp /boot/config-$(uname -r) .config make olddefconfig # 编译(-j$(nproc) 加速,但首次建议 -j1 便于定位错误) make -j$(nproc) # 安装模块(必需!否则某些符号解析失败) sudo make modules_install # 安装内核镜像和 System.map sudo make install

验证是否成功编译:
编译完成后,检查vmlinux是否包含sys_hello符号:

nm vmlinux | grep sys_hello # 应输出类似:ffffffff81a01234 T sys_hello

若无输出,说明hello.o未正确链接进vmlinux,立即回查kernel/Makefile和SYSCALL_DEFINE0宏名一致性。


3. 用户态调用:用syscall(2)直接触发,绕过 glibc 封装

内核侧就绪后,用户态不能直接#include <unistd.h>然后hello()—— glibc 根本不认识这个调用。必须用syscall(2)系统调用接口,传入调用号和参数(本例无参)。这是实验要求的核心能力:理解用户态如何与内核约定通信协议。

3.1 编写测试程序test_hello.c

#define _GNU_SOURCE #include <unistd.h> #include <sys/syscall.h> #include <stdio.h> // 声明系统调用号(必须与 syscall_64.tbl 一致) #ifndef __NR_hello #define __NR_hello 333 #endif int main() { long ret = syscall(__NR_hello); if (ret == 0) { printf("System call hello executed successfully.\n"); } else { perror("syscall hello failed"); } return 0; }

逻辑说明:

  • #define __NR_hello 333是关键:syscall(2)函数需要知道调用号才能构造正确的rax寄存器值。glibc 不提供此宏,必须手动定义。
  • perror()会打印errno对应的字符串,但sys_hello返回 0,所以这里主要用于捕获-1错误(如调用号无效)。
  • 为什么不用int 0x80?实验环境为 x86_64,int 0x80是 32 位兼容模式,会走不同入口,且调用号映射不同(__NR_hello在 32 位表中是另一编号),极易出错。坚持用syscall(2)是最可靠路径。

3.2 编译并运行测试程序

gcc -o test_hello test_hello.c sudo ./test_hello

预期输出:

System call hello executed successfully.

同时,在另一终端执行dmesg | tail -n 5,应看到:

[12345.678901] Hello from kernel!

为什么必须sudo?
系统调用本身无权限限制(sys_hello未做鉴权),但dmesg默认只允许 root 查看内核日志。若想普通用户也能验证,可临时执行:

sudo dmesg -n 8 # 提高日志级别,使 KERN_INFO 可见 echo 1 | sudo tee /proc/sys/kernel/printk # 允许用户态读取部分日志(仅调试用,勿留生产环境)

4. 避坑指南:那些让软院学生熬夜到三点的五个真实血泪现场

这个实验的失败几乎从不报错在表面,而是静默失效:syscall返回 -1、dmesg无输出、内核启动失败……以下是我在某高校助教期间,从上百份实验报告中提炼出的五大高频坑,每一条都对应真实git blame记录和dmesg截图。

4.1 现象:syscall(__NR_hello)返回 -1,dmesg无任何Hello输出

原因:系统调用号333在syscall_64.tbl中填写错误,或SYSCALL_DEFINE0名称与表中第三列不一致。内核找不到对应函数,直接返回-ENOSYS(系统调用未实现)。
解决:

  • 用nm vmlinux | grep sys_hello确认符号存在;
  • 用grep -n "hello" arch/x86/entry/syscalls/syscall_64.tbl确认行格式为333\t64\thello\tsys_hello(制表符,非空格);
  • 检查hello.c中宏是否为SYSCALL_DEFINE0(hello),而非SYSCALL_DEFINE0(sys_hello)。

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

原因:kernel/Makefile中未添加obj-y += hello.o,或添加位置错误(如放在注释行后导致被忽略),导致hello.o未参与链接。
解决:

  • 执行make V=1 | grep hello.o,确认编译命令中是否出现gcc ... -c ... hello.c;
  • 检查kernel/Makefile是否有语法错误(如漏掉换行符、多出空格),可用make -f kernel/Makefile --dry-run预览构建步骤。

4.3 现象:新内核启动后ls、ps等基础命令全部Segmentation fault

原因:修改syscall_64.tbl时,误删或破坏了原有行末的换行符,导致表格解析错位,sys_call_table数组长度异常,后续所有系统调用地址偏移错误。
解决:

  • 用hexdump -C arch/x86/entry/syscalls/syscall_64.tbl | tail检查最后几行是否以0a(LF)结尾;
  • 用wc -l arch/x86/entry/syscalls/syscall_64.tbl对比修改前后行数,确保只增加 1 行;
  • 玄学技巧:用vim打开该文件,输入:set list显示不可见字符,确认每行结尾是$(LF)而非^M(CRLF)。

4.4 现象:dmesg显示Hello from kernel!,但用户态程序仍返回 -1

原因:用户态程序中__NR_hello宏定义错误,或未重新编译(gcc缓存了旧的.o文件)。
解决:

  • 在test_hello.c中添加printf("__NR_hello = %d\n", __NR_hello);,确认输出为333;
  • 强制重新编译:gcc -o test_hello test_hello.c -Werror=implicit-function-declaration(开启隐式声明警告,避免syscall未声明错误);
  • 删除旧可执行文件再编译:rm test_hello && gcc -o test_hello test_hello.c。

4.5 现象:内核编译通过,但sudo ./test_hello提示Operation not permitted

原因:SELinux 或 AppArmor 强制访问控制策略拦截了未知系统调用。Ubuntu 20.04 默认启用 AppArmor。
解决:

  • 临时禁用验证(仅实验用):sudo systemctl stop apparmor && sudo aa-teardown;
  • 永久方案(不推荐实验环境):为test_hello创建 AppArmor profile,但远超实验范围;
  • 更优实践:在虚拟机中进行实验,快照备份后直接关闭安全模块,避免污染主机环境。

5. 进阶验证:用ftrace动态追踪sys_hello的完整执行路径

当dmesg看到Hello只是“能跑”,而ftrace才能证明“真正在内核里跑了”。ftrace是 Linux 内置的轻量级函数跟踪器,无需重启、无需 recompile,5 分钟就能可视化sys_hello从用户态陷入到printk的每一帧调用。

5.1 启用ftrace并过滤sys_hello相关函数

# 挂载 tracefs(若未挂载) sudo mount -t tracefs nodev /sys/kernel/tracing # 清空旧 trace echo 0 > /sys/kernel/tracing/tracing_on echo > /sys/kernel/tracing/trace # 设置跟踪器为 function_graph(显示调用栈) echo function_graph > /sys/kernel/tracing/current_tracer # 过滤只显示含 "hello" 或 "sys_" 的函数 echo '*hello*' > /sys/kernel/tracing/set_ftrace_filter echo 'sys_*' >> /sys/kernel/tracing/set_ftrace_filter # 开启 trace echo 1 > /sys/kernel/tracing/tracing_on

5.2 触发调用并捕获 trace

在另一个终端运行测试程序:

sudo ./test_hello

然后立即查看 trace 结果:

cat /sys/kernel/tracing/trace

预期输出节选:

# tracer: function_graph # # CPU DURATION FUNCTION CALLS # | | | | | | | 1) 0.232 us | sys_hello(); 1) 0.112 us | printk(); 1) 0.089 us | /* sys_hello returns */

这段输出明确告诉你:sys_hello()函数被调用、耗时 0.232 微秒、内部调用了printk()、最终正常返回。它比dmesg多提供了两个关键信息:函数确实被执行了(而非printk被优化掉),以及执行上下文是内核态(无用户态函数混入)。

5.3 关键参数表:ftrace常用控制文件与作用

文件路径作用典型值注意事项
/sys/kernel/tracing/current_tracer选择跟踪器类型function_graph(推荐)、function、irqsofffunction_graph显示调用栈,function只列函数名
/sys/kernel/tracing/set_ftrace_filter白名单过滤函数sys_hello、*hello*、sys_*通配符*有效,多行用>>追加
/sys/kernel/tracing/tracing_on开关 trace 记录1(开)、0(关)关闭后 trace 文件停止更新,但内容保留
/sys/kernel/tracing/trace查看实时 trace 结果cat读取大量输出时用tail -n 50,避免阻塞

为什么不用perf?
perf功能强大但依赖vmlinux符号表、需debuginfo包、学习成本高。ftrace是内核自带、零依赖、开箱即用,完美匹配教学场景——它不追求性能分析,只回答一个朴素问题:“我的sys_hello到底有没有进内核?”

我自己的习惯:每次新增系统调用,必跑一遍ftrace。它像一个内核的“心电图”,printk只是心跳声,而ftrace能看到心室收缩、血液流动的完整生理过程。有一次发现sys_hello调用后紧跟着do_page_fault,顺藤摸瓜揪出copy_from_user未加access_ok检查的越界访问——这种深度验证,是实验手册绝不会写的,却是工程师真正的护城河。

希望帮到你。

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

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

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

立即咨询