最近摸鱼摸太多了,编译个内核模块压压惊。

购买服务器

因为实验内容是需要编译 Linux 源码的,而 Linux 源码内容非常多,编译起来很耗时,错过了优惠券的本人还是勇敢购入了一台按量计费的 24c96g 的服务器。报销真香

然后 VNC 反手装一个宝塔

yum install -y wget && wget -O install.sh http://download.bt.cn/install/install.sh && sh install.sh

openEuler 内核的编译与安装

登录系统并查看当前内核版本

uname -r

内核版本

切换 yum 源

参考的他人的实验步骤中,应该先运行以下命令

yum group install -y "Development Tools"

但是在我的实践中不幸地报了以下错误

Error: Failed to download metadata for repo 'powertools': Cannot prepare internal mirrorlist: No URLs in mirrorlist

必应了一下,热心网友说可能是 yum 源的问题,于是开始切换 yum 源。此处本人参考该博客切换到了阿里的源

  1. 查看 CentOS 发行版本

利用lsb_release -a查看得知本服务器 CentOS 的发行版本为8.4.2105

服务器CentOS的发行版本

  1. 进入 yum 源设置文件夹
cd /etc/yum.repos.d/
  1. 查看 yum 源信息
yum repolist
  1. 备份旧配置文件

(以下内容待考证。因为下载阿里源文件之后重新 ls,发现有了CentOS-Base.repoCentOS-Linux-BaseOS.repo反而没有了)

按教程是sudo mv CentOS-Base.repo CentOS-Base.repo.bak,不过 ls 查看了一下发现本地服务器并没有CentOS-Base.repo,只有CentOS-Linux-BaseOS.repo,于是稍做了修改

sudo mv CentOS-Linux-BaseOS.repo CentOS-Linux-Base.repo.bak
  1. 下载阿里源文件
sudo wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo
  1. 清理缓存
yum clean all
  1. 重新生成缓存
yum makecache

此处由于我的服务器并不是阿里云购买的,所以报了以下错误:

yum cache报错

参考这篇博客,修改了一下相关配置

sed -i -e '/mirrors.cloud.aliyuncs.com/d' -e '/mirrors.aliyuncs.com/d' /etc/yum.repos.d/CentOS-Base.repo

重新运行yum makecache,报错改变为如下提示

yum makecache报错2

参考这篇博客,重新配置了一下阿里源

# 备份
mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup
# 下载阿里源
wget -O/etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-vault-8.5.2111.repo

可惜又回到最初的起点

yum makecache报错3

最终是靠这篇博客解决了问题,居然是我搜到的第一个博客,可恶,博客提到:

那么第二种情况,便是 CentOS 已经停止维护的问题。2020 年 12 月 8 号,CentOS 官方宣布了停止维护 CentOS Linux 的计划,并推出了 CentOS Stream 项目,CentOS Linux 8 作为 RHEL 8 的复刻版本,生命周期缩短,于 2021 年 12 月 31 日停止更新并停止维护(EOL),更多的信息可以查看 CentOS 官方公告。如果需要更新 CentOS,需要将镜像从 mirror.centos.org 更改为 vault.centos.org

根据这篇博客,运行命令行如下

# 进入yum的repos目录
cd /etc/yum.repos.d/
# 修改CentOS文件内容
sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-*
sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-*
# 生成缓存更新
yum makecache

命令运行成功,感觉可喜可贺了

yum makecache成功

安装工具并构建开发环境

cd ~
yum group install -y "Developments Tools"
yum install -y bc
yum install -y openssl-devel

运行第一个命令时出现如下报错:

Repository extras is listed more than once in the configuration
Last metadata expiration check: 0:00:17 ago on Tue 15 Nov 2022 07:15:51 PM CST.
Module or Group 'Developments Tools' is not available.
Error: Nothing to do.

参考该博客运行yum --disablerepo='epel' groupinstall "Development Tools"成功解决

备份 boot 目录以防后续步骤更新内核失败

tar czvf boot.origin.tgz /boot
# 保存当前内核版本信息
uname -r > uname_r.log

下载 Linux 内核源码并解压

此处可以自由选择版本,我选择的版本是 v4.x 下的 4.19

wget https://mirrors.edge.kernel.org/pub/linux/kernel/v4.x/linux-4.19.tar.xz

然后解压源码压缩包

tar -xf linux-4.19.tar.xz

至此准备工作已经完成。

实验主体内容

os 的两个个人实验分别是添加内核模块添加系统调用。前者模块会直接挂载到服务器内核本身,不需要重新编译内核;而后者涉及重新编译内核源码,稍微麻烦一些。以下分别记录一下实验过程。

添加内核模块

本部分本人的实验内容如下:

设计一个带参数的模块,其参数为倒计时的秒数,模块功能是让系统倒计时指定秒数后重启

整体思路是编写模块代码实现 -> 编写 Makefile 文件 -> make 编译 -> 装载使用

为方便起见,先创建一个文件夹存放模块代码

mkdir os-exp
cd os-exp

然后利用宝塔的在线文本编辑器编写模块程序和对应的 Makefile 文件。

内核模块的编写

由于我的实验涉及带参编程,所以以下实例揉合了带参模块编程的部分。

以一个内核模块myhello.c为例。该模块被载入内核时会向系统日志文件中写入用户输入的名字,被卸载时向系统日志文件写入“goodbye”

// 头文件声明
#include <linux/init.h> // 模块初始化和清理函数的定义
#include <linux/module.h> // 大量加载模块所需要的函数和符号的定义
#include <linux/moduleparam.h>  // 当模块在加载时允许用户传递参数时需要该头文件
#include <linux/kernel.h>

static char *who; // 声明参数
module_param(who,char,0644);  // 参数说明

// 内核模块中没有main函数,每个模块必须定义初始化函数和清理函数
// 声明为static后,函数不会在特定文件之外可见
// 初始化函数主要完成模块注册和申请资源。返回0表示初始化成功,其他值表示失败
static int hello_init(void){
  // 使用参数
  prink("%s\n",who);
  return 0;
}
// 清理函数的主要完成注销和释放资源
static void hello_exit(void){
  prink("goodbye\n");
}

// 用这两个宏注册初始化函数和清理函数
module_init(hello_init);
module_exit(hello_exit);
// 模块许可申明。未声明时,当模块被加载时会收到"kernel tainted"的警告。可以写在非函数内部的任意部分。按惯例是写在模块最后。
// GPL表示这是“通用公共许可协议”的任意版本
MODULE_LICENSE("GPL");

模块的编译和加载

编译过程首先会到内核源码目录下读取顶层的 Makefile 文件,然后返回模块源代码所在的目录继续编译。

如果想要针对编写的模块进行编译,需要在与模块源码文件的同级目录下新建名为Makefile的无后缀文件,并进行编写。

以“hello world”模块为例,编写一个简单的 Makefile 如下:

obj-m :=hello.o   // 生成的模块名称是 hello.ko
hello-objs:=myhello.o // 格式为 <模块名>-objs := <目标文件>。如果模块是由多个c文件构成的,则模块名不能与目标文件名字相同
// 内核源码目录。此处表示通过当前运行内核使用的模块目录中的build符号链接指定
// 也可以直接给出源码目录(如/home/linux-4.19)
KDIR :=/lib/modules/$(shell uname -r)/build
PWD :=$(shell pwd)  // PWD 是当前目录
default:
    make -C $(KDIR) M=$(PWD) modules  // -C指定内核源码目录,M指定模块源码所在目录
clean:
    make -C $(KDIR) M=$(PWD) clean

在当前目录的终端运行makemake会自动检测此目录下的 Makefile 文件,然后按照规则进行编译

不过第一次运行时报错"makefile:5: *** missing separator. Stop.",查阅资料了解到这是表示makefile中的命令前没有使用TAB,回到文件编辑器中,发现编辑器默认使用的是空格缩进

Makefile

于是修改缩进方式为 tab 缩进,重新运行make,运行成功

Makefile

测试模块

在模块源码同级目录下对 c 程序进行编译

gcc -o myhello myhello.c
./myhello

即可查看运行结果。

添加系统调用

注意:需要以 root 身份才能完成下述操作

本部分本人的实验内容如下:

设计一个带参数的模块,其参数为新主机名,模块功能是改变原主机名称为参数传入的字符串(新主机名)

分配系统调用号,修改系统调用表

系统调用表在 linux 源码根目录下的arch/x86/entry/syscalls/syscall_64.tbl

系统调用表

每个系统调用在表中占一个表项,其格式为

<系统调用号><common/64/x32><系统调用名><服务例程入口地址>

在编写模块时,我们选择一个未使用的系统调用号进行分配。比如在上图中,当前系统使用到 334 号,那么我们可以为自己的模块myhostname分配一个 335 号的系统调用号,然后在 syscall_64.tbl 里为新调用添加一条记录。

在我本人的实践过程中,发现自己的系统调用表和课本上展示的并不一致,我的entry_point__x64_sys_xxx,而课本上的entry_pointsys_xxx,而且在后续申明系统调用服务例程原型时发现文件声明的名字也变回了sys_xxx。回去翻了一下系统调用表的文件,发现在开头有这样一段声明:

系统调用表开头注释

所以其实还是一一对应的,只要仿照文件其他记录的格式写就好了。

申明系统调用服务例程原型

系统调用服务例程的原型声明在 linux 源码目录下的/include/linux/syscalls.h中。为方便对照,我直接全局搜索了要模拟实现的函数名,并把新的原型记录添加到了其下方。

申明系统调用服务例程原型

实现系统调用服务例程

系统调用服务例程的实现文件在 linux 源码目录下的/kernel/sys.c

实现系统调用服务例程-myhostname

可以看到此处使用了 SYSCALL_DEFINE2。我个人比较好奇这里的 2 是什么魔法数字,于是了解了一下 SYSCALL_DEFINEx。对此,这篇博客是这样解释的:

宏定义 SYSCALL_DEFINEx(xxx,…),展开后对应的方法则是 sys_xxx。方法参数的个数 x,对应于 SYSCALL_DEFINEx

编译内核

编译内核需要 root 权限。首先先进到之前下载的 linux 源码根目录。

清除残留的.config 和.o 文件

在完全开始重新编译前,需要清除残留的 .config 和 .o 文件。如果后续编译中出现错误,则再次开始完全重新编译之前,也需要先如此清理。

具体操作为,进入 linux 源码目录,运行#makemrproper

配置内核

虽然书上说需要终端运行make menuconfig进行配置,不过问了一下朋友说其实直接使用默认配置就好,所以我这里跳过了。

命令行一条龙
make -j48 # 编译内核。数字可填服务器cpu的两倍。我是24c,所以写了48
make modules # 编译模块
make modules_install # 安装模块
make install # 安装内核
reboot -n # 立即重启

之后将用新内核启动 Linux。启动完成后进入终端可使用命令”uname -a”查看内核版本

编写用户态程序测试代码

在服务器中新建一个 c 文件如下

#include <unistd.h>
#include <sys/syscall.h>
#include <stdio.h>
#define __NR_mysyscall 335  // 此前为系统调用设定的系统调用号

int main(){
  char name[1000];
  scanf("%s", name);
  // 第一个参数填写系统调用号,之后的参数为该系统调用需要的所有参数
  syscall(__NR_mysyscall, name, 7);
  return 0;
}

测试文件

编译该程序并运行后,重新打开终端发现 hostname 改变。

感谢 lyle 老师和 rzgg 的帮助呜呜。学校操作系统的书原来是真滴有用(

吹爆 24c 服务器。其实我一开始用的是 8c 服务器,到 linux 源码下载这里显示要下载将近两个小时。加上朋友告诉我他做实验用的 8c 服务器,每次编译内核要 1h,但朋友的 24c 服务器只需要十几分钟,于是果断重买了个服务器,而 24c 下载源码只花了一两分钟

不过就是,内核编译真的要好久啊,我现在敲完博客了还没编译完。心疼我的流量和钱,搞实验这两天直接烧了 200r,哪怕能报销但感觉还是有亿点心疼 QAQ