前言
这学期上了一门挺感兴趣的操作系统课,课程实践内容就是标题。但问题来了,教程里面给的是华为云和openEuler发行版的教程,这让我很不理解。毋庸置疑这项任务是非常有学习价值的,但这种冷门发行版很多操作都和主流发行版不同,与其说是学习,不如说是照本宣科完成一项死板的任务。而之后真正需要用到的时候,又要重新学习一遍其他发行版的流程,无形中增加了学习成本。不过毕竟大学的教育就是这样,不合理的教学已经司空见惯了
还好这次实验的核心其实和发行版关系不大,重点在于内核编译流程和系统调用的添加方式,只要保证方法正确、编译工具完整,换环境不会影响结果。因此为了不让自己失去动力,从而随便糊弄过去,错失了宝贵的课堂学习机会,我就直接在 Ubuntu 上做了一遍
环境
我使用的是Ubuntu 24.04.4 LTS 64位 的虚拟机
- CPU:4 核
- 内存:8GB
- 硬盘:50GB
- 网络:NAT模式

实验开始前首先创建一个快照,这样哪怕系统崩了也可以随时恢复
安装工具,构建开发环境
|
|
以防后续步骤更新内核导致无法引导,备份现有的引导文件并保存内核版本信息
|
|
内核编译过程
获取源码
从Linux内核官方归档网站下载源码。我下载的是稳定的长期支持版 5.15.148
|
|
配置内核
为了确保新内核配置与当前Ubuntu系统兼容,最稳妥的做法是拷贝当前系统的配置
|
|
也可以手动改:
|
|
|
|
编译
|
|
这里 -j 后面跟 CPU 核数,加快编译速度。
注意:内核编译时间较长,视机器性能通常需要20分钟至数小时不等
安装
|
|
执行完之后会自动把内核拷到 /boot,并更新 grub。
启动新内核
|
|
重启后用:
|
|
如果版本变了,说明整个流程是通的。
系统调用这件事到底在干嘛
用户程序是不能直接操作内核的,中间必须走系统调用这一层。
可以简单理解为: 用户态 → syscall → 内核态 → 执行功能
系统调用就是内核暴露出来的一组“入口函数”。
添加一个自己的系统调用
这一块才是核心。
目标很简单:
写一个 syscall,在内核里 printk 打一行自己的学号。
定义系统调用函数
在内核源码里找一个合适的位置,比如:
|
|
加一个函数:
|
|
这里有几个点:
SYSCALL_DEFINE0表示无参数printk相当于内核里的 printf- 输出会进内核日志,不是在终端直接显示
注册系统调用号
不同架构位置不一样,Ubuntu x86_64 在:
|
|
加一行:
|
|
333 这个号不要冲突,随便找个没用的。
声明函数
在:
|
|
加声明:
|
|
重新编译内核
和前面一样,再来一遍:
|
|
用户态测试
写个简单 C 程序:
|
|
编译运行:
|
|
不会有输出是正常的,因为打印在内核日志里。
查看:
|
|
能看到刚才 printk 的内容,就说明调用成功了。
一些实际踩坑
内核版本和头文件不匹配会直接编译炸掉,这种问题很常见。
系统调用号重复会导致行为异常,有时候甚至直接起不来。
printk 默认日志级别可能看不到,需要用 dmesg 过滤。
还有一点,内核一旦写错,轻则 panic,重则系统起不来,所以全程建议在虚拟机里搞。
和 openEuler 的差别
课程给的是 openEuler + arm64,这套流程在结构上是一样的,但细节差别不少:
- syscall 表的位置不一样
- 编译配置路径不同
- 某些驱动和模块名称不一样
本质还是同一套内核,只是发行版和架构带来的差异。
用 Ubuntu 做的好处是资料密集,基本不会卡死在环境上。
遇到问题基本都能搜到,不至于卡死在平台差异上。
目标其实就两件事: 一是把内核完整编译一遍并成功启动 二是自己往内核里加一个系统调用,然后从用户态调起来
| 对比项 | 原始实验 (openEuler / ARM64) | 本实验 (Ubuntu / x86-64) |
|---|---|---|
| 包管理器 | yum (基于RPM体系) |
apt (基于Debian体系) |
| 架构特定目录 | arch/arm64/ |
arch/x86/ |
| 内核镜像目标 | Image |
bzImage |
| 设备树 (Device Tree) | 需要编译 dtbs |
x86平台不需要编译 dtbs |
| 系统调用表位置 | include/uapi/asm-generic/unistd.h |
arch/x86/entry/syscalls/syscall_64.tbl |
| 系统调用实现方式 | 依赖ARM64特定的调用约定和寄存器 | 依赖x86_64特定的调用约定和 SYSCALL_DEFINE 宏 |