Featured image of post Linux 内核编译及添加系统调用

Linux 内核编译及添加系统调用

改进一下老古董课程实验

前言

这学期上了一门挺感兴趣的操作系统课,课程实践内容就是标题。但问题来了,教程里面给的是华为云和openEuler发行版的教程,这让我很不理解。毋庸置疑这项任务是非常有学习价值的,但这种冷门发行版很多操作都和主流发行版不同,与其说是学习,不如说是照本宣科完成一项死板的任务。而之后真正需要用到的时候,又要重新学习一遍其他发行版的流程,无形中增加了学习成本。不过毕竟大学的教育就是这样,不合理的教学已经司空见惯了

还好这次实验的核心其实和发行版关系不大,重点在于内核编译流程和系统调用的添加方式,只要保证方法正确、编译工具完整,换环境不会影响结果。因此为了不让自己失去动力,从而随便糊弄过去,错失了宝贵的课堂学习机会,我就直接在 Ubuntu 上做了一遍

环境

我使用的是Ubuntu 24.04.4 LTS 64位 的虚拟机

  • CPU:4 核
  • 内存:8GB
  • 硬盘:50GB
  • 网络:NAT模式

img

实验开始前首先创建一个快照,这样哪怕系统崩了也可以随时恢复

安装工具,构建开发环境

1
2
sudo apt update
sudo apt install -y build-essential libncurses-dev bison flex libssl-dev libelf-dev bc dwarves zstd

以防后续步骤更新内核导致无法引导,备份现有的引导文件并保存内核版本信息

1
2
sudo tar czvf /root/boot.origin.tgz /boot/
uname -r > ~/uname_r.log

内核编译过程

获取源码

从Linux内核官方归档网站下载源码。我下载的是稳定的长期支持版 5.15.148

1
2
3
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.15.148.tar.xz
tar -xvf linux-5.15.148.tar.xz
cd linux-5.15.148

配置内核

为了确保新内核配置与当前Ubuntu系统兼容,最稳妥的做法是拷贝当前系统的配置

1
2
3
4
5
# 拷贝当前运行内核的配置文件
$ cp /boot/config-$(uname -r) .config

# 更新配置(遇到新配置项直接按回车选择默认值)
$ make olddefconfig

也可以手动改:

1
make menuconfig
1
2
3
4
5
6
7
# 查看可编译项(差异:x86架构查看的是bzImage)
$ make help | grep bzImage
  bzImage      - Compressed kernel image (arch/x86/boot/bzImage)

# 执行编译(差异:x86架构编译bzImage和modules即可,不需要dtbs)
# -j 后面的数字根据你的虚拟机CPU核心数调整,例如4核即为 -j4
$ make -j$(nproc) bzImage modules

编译

1
make -j$(nproc)

这里 -j 后面跟 CPU 核数,加快编译速度。

注意:内核编译时间较长,视机器性能通常需要20分钟至数小时不等

安装

1
2
sudo make modules_install
sudo make install

执行完之后会自动把内核拷到 /boot,并更新 grub。


启动新内核

1
sudo reboot

重启后用:

1
uname -r

如果版本变了,说明整个流程是通的。


系统调用这件事到底在干嘛

用户程序是不能直接操作内核的,中间必须走系统调用这一层。

可以简单理解为: 用户态 → syscall → 内核态 → 执行功能

系统调用就是内核暴露出来的一组“入口函数”。


添加一个自己的系统调用

这一块才是核心。

目标很简单: 写一个 syscall,在内核里 printk 打一行自己的学号。


定义系统调用函数

在内核源码里找一个合适的位置,比如:

1
kernel/sys.c

加一个函数:

1
2
3
4
5
SYSCALL_DEFINE0(mycall)
{
    printk("my syscall: 202xxxxxx\n");
    return 0;
}

这里有几个点:

  • SYSCALL_DEFINE0 表示无参数
  • printk 相当于内核里的 printf
  • 输出会进内核日志,不是在终端直接显示

注册系统调用号

不同架构位置不一样,Ubuntu x86_64 在:

1
arch/x86/entry/syscalls/syscall_64.tbl

加一行:

1
333    common    mycall    sys_mycall

333 这个号不要冲突,随便找个没用的。


声明函数

在:

1
include/linux/syscalls.h

加声明:

1
asmlinkage long sys_mycall(void);

重新编译内核

和前面一样,再来一遍:

1
2
3
4
make -j$(nproc)
sudo make modules_install
sudo make install
sudo reboot

用户态测试

写个简单 C 程序:

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

int main() {
    syscall(333);
    return 0;
}

编译运行:

1
2
gcc test.c -o test
./test

不会有输出是正常的,因为打印在内核日志里。

查看:

1
dmesg | grep my

能看到刚才 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
Like 0
本站已不稳定运行 小时 分钟
共发表文章 23 篇 ,总计 73.63 k 字
本站总访问量:
使用 Hugo 构建
主题 StackJimmy 设计