Linux kernel User Guide

概述

本文对内核的使用方法(包括构建、编译等)作一些总结。

参考 The Linux kernel user’s and administrator’s guide

准备

获取源码

可以在 内核仓库 中找到对应版本的源码压缩包、签名和补丁,也可以使用 git 克隆内核仓库。我们以 5.4 版本为例:

1
2
3
4
5
6
$ wget https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.4.tar.xz
$ wget https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.4.tar.sign

# 也可以使用国内的镜像源:
$ wget https://mirror.tuna.tsinghua.edu.cn/kernel/v5.x/linux-5.4.tar.xz
$ wget https://mirror.tuna.tsinghua.edu.cn/kernel/v5.x/linux-5.4.tar.sign

获取 Greg Kroah-HartmanLinus Torvalds 的公钥:

1
$ gpg2 --locate-keys torvalds@kernel.org gregkh@kernel.org

解压并验证签名:
$ xz -cd linux-5.4.tar.xz | gpg2 --verify linux-5.4.tar.sign -

$ tar xf linux-5.4.tar
$ cd linux-5.4

如果验证签名失败,并显示“缺少公钥”,可以根据提示的用户 ID 从密钥服务器导入公钥(前提是密钥服务器没有受到攻击):

$ gpg2 --keyserver keyserver.ubuntu.com --recv-keys 0D3B3537C4790F9D

编译环境

编译环境所依赖的软件包列表:Minimal requirements to compile the Kernel

配置

还原环境

$ make mrproper
每次需要将内核源码树还原到刚解压时的状态,可运行该命令。

$ make clean
删除生成的目标文件(不会删除.config

配置命令

make oldconfig - 基于Host系统上的.config 进行配置,如/boot/config-5.xx.xx-xx-generic

make defconfig - 按默认选项进行配置(x86_64_defconfig
make allnoconfig - 除必须选项外,其他一律不选(常用于嵌入式系统)
make allyesconfig -

make menuconfig - 交互式界面

可以查看 make help 来获取更多信息。

建议:

  • 使用 make defconfig 获取默认配置
  • 使用当前Host系统上的 config 文件,一般为 /boot/config-xxxxxxxxx
  • 其他发行版系统的配置文件

编译

make -j<N>

可以使用 ccache 加速编译。

如果遇到下面的问题:

1
arch/x86/entry/thunk_64.o: warning: objtool: missing symbol table

参考链接:https://lkml.org/lkml/2021/1/14/387

如果遇到:

1
2
ld: arch/x86/boot/compressed/pgtable_64.o:(.bss+0x0): multiple definition of `__force_order';
arch/x86/boot/compressed/kaslr_64.o:(.bss+0x0): first defined here

参考链接:https://lkml.org/lkml/2020/1/29/494

如果遇到:

1
cc1: error: code model kernel does not support PIC mode

则可以修改内核源码中的 Makefile 文件,添加-fno-pie 到变量 KBUILD_CFLAGS

很多编译问题都可以在内核邮件列表中查找到解决方法。

其他架构

如果要交叉构建基于 vexpress-a9 架构:

1
2
3
$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- vexpress_defconfig
$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- menuconfig # 注意配置 DEBUG_PLL 和 EARLY_PRINTK
$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- -j8

安装

安装 bzImage

bzImage 是源码树根目录下生成的 vmlinux 的压缩版本。它的位置在arch/x86/boot/bzImage

首先将它拷贝到Host系统的/boot目录中(可以重命名为vmlinuz-5.4-xxxxxx为区分标记,可以设置为任意特定的字符串)
$ sudo cp arch/x86/boot/bzImage /boot/vmlinuz-5.4-xxx

然后修改 grub 配置文件

安装模块

$ sudo make modules_install

System.map 文件

内核命令行参数

我们将内核也看作一个单独的程序(带main函数)时,也可以给它传入命令行参数(例如由引导程序传递)。

模块参数

模块的参数可以通过内核命令行设置,例如:
usbcore.blinkenlights=1

也可以使用 modprobe
modprobe usbcore blinkenlights=1

使用 modinfo info <module_name> 可以获取可加载模块的参数列表。对于可加载模块,当被载入内核时,会将所有参数映射为 /proc/modules/{module_name}/parameters/ 中的文件,这使得用户可以直接修改该文件来设置模块参数。

命令行参数格式

  • -_ 是等价的,例如log_buf=1M 等价于 log-buf=1M
  • 对于带空格的字符串使用双引号,例如param="spaces in here"

设备列表

内核参数

使用/proc/sys虚拟文件系统可以动态修改内核参数。直接修改该目录下的参数文件会立即生效,但重启后就失效了。如果需要永久生效,则需要修改/etc/sysctl.conf文件,并在修改后执行sysctl -p生效。使用sysctl -a查看所有可修改的变量名。

内核调试

参考 跟踪分析Linux内核启动过程 - CSDN
参考 学习ulk3,搭建linux2.6内核的调试环境
参考 使用GDB调试Linux Kernel
参考 Debugging kernel and modules via gdb

环境准备

安装 qemu 虚拟机:$ sudo apt-get install qemu 或者对于 ARM 架构:$ sudo apt-get install qemu-system-arm
安装 gdb :$ sudo apt-get install gdb

U-Boot

U-Boot 主要用于嵌入式 Linux 系统的 bootloader,编译:

1
2
3
4
$ git clone https://gitee.com/mirrors/u-boot.git 
$ cd u-boot
$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- vexpress_ca9x4_defconfig
$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- -j8

使用 qemu 虚拟机:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ qemu-system-arm -M vexpress-a9 -m 256 -kernel u-boot -nographic
U-Boot 2025.04-rc3-00052-g0fd7ee0306a8 (Mar 08 2025 - 17:55:54 +0800)

DRAM: 256 MiB
WARNING: Caches not enabled
Core: 23 devices, 11 uclasses, devicetree: embed
Flash: 128 MiB
MMC: mmci@5000: 0
Loading Environment from Flash... *** Warning - bad CRC, using default environment

In: uart@9000
Out: uart@9000
Err: uart@9000
Net: eth0: ethernet@3,02000000
Hit any key to stop autoboot: 0
=>

参考 基于 QEMU 的 ARM 仿真
参考 支持树莓派4b的 Qemu 环境搭建

构建内核

构建内核时,注意在kernel hacking 配置项中选择打开 compile the kernel with debug info 以及 Provide GDB scripts for kernel debugging。另外取消 Kernel Features -> Randomize the address of the kernel image 配置项。

制作根文件系统

使用 Buildroot 工具 来制作根文件系统。官网下载源代码后,编译构建:

1
$ make menuconfig  # 选择架构和文件系统类型

BuildRoot 会从源代码构建所以基础软件包,相对来说比较耗时。使用方法参考[BuildRoot 工具介绍]

如果只需要使用非常简单的根文件系统,可以直接使用 busybox 来制作。

准备根目录镜像并初始化

a. 创建并格式化 img 文件

1
2
$ dd if=/dev/zero of=rootfs.img bs=4096 count=1024
$ mke2fs rootfs.img

b. 挂载镜像文件

1
2
$ mkdir rootfs
$ sudo mount -o loop rootfs.img rootfs

c. 准备 dev 目录

1
2
3
4
5
6
7
$ sudo mkdir rootfs/dev

# linux 启动过程中会使用 console 设备
$ sudo mknod rootfs/dev/console c 5 1

# 另外需要一个 Linux 根设备
$ sudo mknod rootfs/dev/ram b 1 0

d. 新建其他目录(可选)

1
$ sudo mkdir rootfs/{proc,sys,var,run}

特别的,/proc 目录中包含了许多内核数据结构的映射,对于调试非常重要。一种方法是使用 qemu 进入 shell 后手动挂载:

1
$ mount -t proc none /proc

也可以手动挂载 sysfs 文件系统:

1
$ mount -t sysfs sysfs /sys

特别注意的是,如果要支持设备热插拔(如udev 或者 mdev),需要挂载 /dev/dev/pts 文件系统:

1
2
3
mount -t devtmpfs devtmpfs /dev 
mkdir /dev/pts
mount -t devpts devpts /dev/pts

但重启后配置会消失。使用 /etc/fstab 或者 /etc/init.d/rcS 来自动挂载。

构建 busybox

下载 BusyBox 源代码 并验证数字签名后,进行构建:

1
2
3
4
5
6
7
8
$ make defconfig
$ make menuconfig
# 注意修改配置:busybox settings -> build options -> build busybox as a static binary (no share libs)。
# 如果生成的不是静态链接的可执行文件,那么 busybox 就会依赖宿主机上的一些动态链接库,而一个单 linux 内核无法提供这些库。

$ make
$ sudo make CONFIG_PREFIX=<rootfs path>/ install
# 上述命令将 busybox 安装到已经挂载的 rootfs 根目录中

最后卸载 rootfs 即可:

1
$ sudo umount rootfs

使用 qemu 验证

在 树莓派5 ARM64 架构下验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 注意使用制作的 rootfs.img 镜像文件
# root=/dev/vda 使用 vda 关键字,而不是 ram 或者 sda
# init=/bin/ash 这里使用 ash 作为内核启动后第一个运行的用户程序
$ qemu-system-aarch64 -M virt -cpu cortex-a72 -kernel arch/arm64/boot/Image -drive format=raw,file=rootfs.img,media=disk -append "root=/dev/vda init=/bin/ash" -serial stdio -display none

......
[ 0.853700] EXT4-fs (vda): mounted filesystem without journal. Opts: (null)
[ 0.854244] VFS: Mounted root (ext4 filesystem) readonly on device 254:0.
[ 0.859716] devtmpfs: mounted
[ 0.893438] Freeing unused kernel memory: 5056K
[ 0.894555] Run /bin/ash as init process
/bin/ash: can't access tty; job control turned off
~ # ls
bin linuxrc proc sbin usr
dev lost+found run sys var

vexpress-a9 架构下验证:

1
2
3
$ qemu-system-arm -M vexpress-a9 -m 512M -kernel arch/arm/boot/zImage \
-dtb arch/arm/boot/dts/vexpress-v2p-ca9.dtb -nographic \
-append "root=/dev/mmcblk0 rw console=ttyAMA0" -sd rootfs.img

qemu + gdb 调试内核

在调试模式下启动 qemu,其中 “-s” 选项表示:使用 tcp 1234 端口;“-S” 选项表示只有在 gdb 连上 tcp 1234 端口后,CPU 才会继续执行。nokaslr 禁用 ASLR 安全机制。

1
$ qemu-system-x86_64 -s -S -kernel arch/x86/boot/bzImage -append "root=/dev/zero console=ttyS0 nokaslr" -serial stdio -display none

如果是 arm64 架构:

1
$ qemu-system-aarch64 -s -S -no-reboot -M virt -cpu cortex-a72 -smp 4 -kernel arch/arm64/boot/Image --append "root=/dev/vda2 console=ttyAMA0 nokaslr" -serial stdio -display none

指定 rootfs 的命令:

1
$ qemu-system-aarch64 -s -S -no-reboot -M virt -cpu cortex-a72 -smp 4 -kernel arch/arm64/boot/Image -drive format=raw,file=rootfs.img,media=disk --append "nokaslr root=/dev/vda init=/bin/ash" -serial stdio -display none

运行 gdb:$ gdb vmlinux 或者 $ gdb vmlinuz
注意这里如果提示无法加载vmlinux-gdb.py,则根据提示在 gdb 配置文件中为 add-auto-load-safe-path 设置脚本绝对路径。该脚本中扩展了一些 gdb 命令,比如查看内核缓冲区、查看进程等。

然后在 gdb 中 输入 target remote localhost:1234 并设置断点

1
2
3
4
5
6
7
8
9
(gdb) target remote localhost:1234
(gdb) break start_kernel
(gdb) continue
Continuing.

Breakpoint 1, start_kernel () at init/main.c:576
576 {

(gdb) lx-dmesg

构建模块并调试

参考 构建外部模块

在编译完内核后,将内核模块安装到 rootfs 的 lib/modules 目录中:

1
$ make INSTALL_MOD_PATH=rootfs/ modules_install

调试技巧

如果要关闭某个函数的编译优化,使用 __attribute__((optimize(0))) 编译器优化选项。如果要关闭某段代码的优化:

1
2
3
4
5
6
7
8
9
10
void foo() {
fun1();
#prama GCC push_options
#prama GCC optimize (0)
fun2();
//......
#prama GCC pop_options
fun3();
}

如果进入根文件系统后,需要创建文件或者目录,需要在内核启动时设定根文件系统的读写权限:

1
.... --append "nokaslr root=/dev/vda rw init=/bin/ash" ...

root=/dev/vda 后紧跟 rw 标志。

如果要 init 进程在进入 shell 前做一些其他任务,则启动参数init可以使用 /linuxrc,并在根文件系统中创建 /etc/init.d/rcS 可执行脚本,在脚本中可以编写启动其他用户进程。