Featured image of post [小白篇] 编译 Linux 内核完整笔记

[小白篇] 编译 Linux 内核完整笔记

- 带你从零开始编译 Linux 内核,了解 Linux 内核的编译过程

前言

自己编译内核听起来很硬核,其实流程很固定:拿源码 → 准备配置 → 编译 → 安装 → 重启验证。只要留好退路(旧内核一直都在),基本不会把系统搞坏。

本文以 Debian 12 / Ubuntu 22.04 及更新版本、x86_64 架构为例,面向第一次编译内核的同学,尽量把每一步为什么这么做讲清楚。

整体流程如下:

编译 Linux 内核的整体流程

准备工作

环境需求

  • 一台 Debian/Ubuntu 机器,强烈建议先在虚拟机里练手,熟悉之后再上物理机;
  • 磁盘空间至少预留 30 GB(发行版的配置默认开启了调试信息,编译产物会非常大);
  • 内存 4 GB 以上,CPU 核心越多越快。编译时间从十几分钟到几个小时不等;
  • 有 sudo 权限。

安装编译依赖

1
2
3
sudo apt update
sudo apt install -y build-essential bc bison flex libssl-dev libelf-dev \
    libncurses-dev dwarves rsync cpio xz-utils zstd kmod wget

这些包分别是干什么的:

软件包用途
build-essentialgcc、make 等基础编译工具
bison、flex解析内核配置系统(Kconfig)需要
libssl-dev模块签名、证书处理需要
libelf-devobjtool 等工具需要
libncurses-devmake menuconfig 的终端界面
dwarves提供 pahole,发行版配置开启了 BTF,没有它会编译失败
bc、rsync、cpio、zstd编译和安装过程中的各种辅助工具

获取内核源码

打开 kernel.org,首页列出了 mainline、stable 和 longterm 几类版本。新手建议选 stable 或 longterm(LTS) 版本。

下面以 6.12 系列为例,版本号请换成你在 kernel.org 上看到的实际版本:

1
2
3
4
5
mkdir -p ~/kernel && cd ~/kernel
KVER=6.12.10
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-${KVER}.tar.xz
tar -xf linux-${KVER}.tar.xz
cd linux-${KVER}

如果想校验源码完整性,可以同时下载同目录下的 linux-${KVER}.tar.sign。注意签名针对的是解压后的 .tar,校验方法见 kernel.org 的 Using GnuPG to verify kernel signatures。

准备内核配置

内核有上万个配置项,从零开始一个个选不现实。对小白来说,最稳妥的做法是复用当前系统正在使用的配置——它肯定能在你的机器上正常启动。

复用当前系统的配置(推荐)

1
2
cp /boot/config-$(uname -r) .config
make olddefconfig
  • /boot/config-$(uname -r) 是发行版编译当前内核时用的配置;
  • 新内核会多出一些旧配置里没有的选项,make olddefconfig 会把这些新选项全部设为默认值,不会一个个问你。

用 menuconfig 调整配置(可选)

想开关某个功能,或者只是好奇内核里都有什么,可以打开图形化的配置菜单:

1
make menuconfig

方向键移动,空格切换 [*](编进内核)/ [M](编成模块)/ [ ](不编译),/ 搜索配置项,? 查看帮助,最后选 Save 保存到 .config。

给新内核加个后缀

建议给自己编的内核加一个版本后缀,方便在 GRUB 菜单和 uname -r 里一眼认出来:

1
scripts/config --set-str LOCALVERSION "-mybuild"

缩小编译范围(可选)

发行版配置会把几千个驱动都编成模块,编译很慢。如果只是想在当前这台机器上用,可以只保留当前已加载的模块:

1
make localmodconfig

它会根据 lsmod 的结果裁剪配置,编译时间能缩短很多。代价是当前没插的设备(比如某个 U 盘的文件系统、某块网卡)的驱动也会被裁掉,之后可能用不了。第一次编译不确定的话,跳过这一步。

Debian/Ubuntu 上的常见坑

SYSTEM_TRUSTED_KEYS 报错

直接复用 Debian/Ubuntu 的配置编译,大概率会遇到类似下面的错误:

1
make[3]: *** No rule to make target 'debian/canonical-certs.pem', needed by 'certs/x509_certificate_list'.  Stop.

部分 Debian 版本上则是 debian/certs/debian-uefi-certs.pem。原因是发行版配置里写死了只存在于发行版打包环境中的证书路径。把这两个选项清空即可:

1
2
3
scripts/config --set-str SYSTEM_TRUSTED_KEYS ""
scripts/config --set-str SYSTEM_REVOCATION_KEYS ""
make olddefconfig

清空后,内核会在编译时自动生成一把模块签名密钥(certs/signing_key.pem),不影响使用。

Secure Boot 启动失败

自己编译的内核没有经过发行版的签名。如果机器开启了 UEFI Secure Boot,新内核可能启动不了或拒绝加载模块。最简单的办法是在 BIOS/UEFI 里关闭 Secure Boot;想保留 Secure Boot 的话,需要自己生成密钥并通过 MOK 登记,这超出了本文范围。

磁盘被撑爆

发行版配置默认开启了调试信息,模块文件会非常大,装到 /lib/modules 后可能占好几个 GB。后面安装模块时加上 INSTALL_MOD_STRIP=1 可以去掉调试符号,体积会小很多。

编译

1
make -j$(nproc)
  • -j$(nproc) 表示用全部 CPU 核心并行编译;
  • 编译完成后,内核镜像在 arch/x86/boot/bzImage,模块散落在源码树各处;
  • 中途报错时,往上翻找到第一个 error:,后面的错误通常都是它引起的。可以用 make -j$(nproc) 2>&1 | tee build.log 把日志保存下来慢慢看。

可以随时用下面的命令看看即将生成的内核版本号:

1
make kernelrelease

安装

安装模块和内核

1
2
sudo make INSTALL_MOD_STRIP=1 modules_install
sudo make install
  • modules_install:把模块安装到 /lib/modules/<版本号>/;
  • install:把 vmlinuz、System.map、config 复制到 /boot。在 Debian/Ubuntu 上,它会调用 installkernel,顺带触发 /etc/kernel/postinst.d/ 里的钩子,自动生成 initramfs 并更新 GRUB。

保险起见,确认一下 initramfs 已经生成,再手动更新一次 GRUB:

1
2
3
4
ls /boot | grep "$(make -s kernelrelease)"
# 如果没有 initrd.img-<版本号>,手动生成:
# sudo update-initramfs -c -k "$(make -s kernelrelease)"
sudo update-grub

另一种方式:打包成 deb(可选)

内核源码自带打包成 deb 的目标,装卸都交给 apt,管理起来更清爽:

1
2
3
make -j$(nproc) bindeb-pkg
ls ../*.deb
sudo apt install ../linux-image-6.12.10-mybuild_*.deb ../linux-headers-6.12.10-mybuild_*.deb

生成的 deb 包在源码目录的上一级,文件名里的版本号以 ls 看到的为准;带 -dbg 的是调试符号包,体积很大,一般不用装。如果打包时提示缺少依赖,按提示安装对应的软件包即可。用这种方式安装的话,就不需要再执行上面的 modules_install 和 install 了。

重启并验证

1
sudo reboot

GRUB 默认会启动版本号最高的内核。重新登录后检查:

1
uname -r

输出带有 -mybuild 后缀的新版本号,就说明成功启动了自己编译的内核:

重启后验证内核版本

还可以顺便看看有没有明显的报错:

1
sudo dmesg --level=err,warn | head -n 30

回退方法

新内核启动不了怎么办

旧内核一直都在 /boot 里,GRUB 菜单可以选:

  1. 开机时调出 GRUB 菜单。Ubuntu 默认隐藏菜单,BIOS 启动时按住 Shift,UEFI 启动时按 Esc;
  2. 进入 Advanced options for Ubuntu(Debian 上是 Advanced options for Debian GNU/Linux);
  3. 选择原来的内核版本启动。

卸载自己编译的内核

先确认当前运行的不是要删除的内核(uname -r),再执行:

1
2
3
4
5
KREL=6.12.10-mybuild   # 换成你要删除的版本号
sudo rm -f /boot/vmlinuz-$KREL /boot/initrd.img-$KREL \
    /boot/System.map-$KREL /boot/config-$KREL
sudo rm -rf /lib/modules/$KREL
sudo update-grub

如果是用 deb 包安装的,直接卸载即可:

1
sudo apt remove linux-image-6.12.10-mybuild linux-headers-6.12.10-mybuild

删除之前务必再核对一次版本号,千万别把当前发行版的内核删了。

总结

整个过程可以浓缩成下面几条命令:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
cp /boot/config-$(uname -r) .config
scripts/config --set-str SYSTEM_TRUSTED_KEYS ""
scripts/config --set-str SYSTEM_REVOCATION_KEYS ""
scripts/config --set-str LOCALVERSION "-mybuild"
make olddefconfig
make -j$(nproc)
sudo make INSTALL_MOD_STRIP=1 modules_install
sudo make install
sudo update-grub
sudo reboot

第一次跑通之后,就可以尝试用 make menuconfig 精简配置、打补丁、调试驱动了。

参考

由 Hugo 强力驱动