Linux kernel User Guide
概述
本文对内核的使用方法(包括构建、编译等)作一些总结。
准备
获取源码
可以在 内核仓库 中找到对应版本的源码压缩包、签名和补丁,也可以使用 git 克隆内核仓库。我们以 5.4 版本为例:
1 | wget https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.4.tar.xz |
获取 Greg Kroah-Hartman 和 Linus 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 | ld: arch/x86/boot/compressed/pgtable_64.o:(.bss+0x0): multiple definition of `__force_order'; |
参考链接: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 | make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- vexpress_defconfig |
安装
安装 bzImage
bzImage 是源码树根目录下生成的 vmlinux 的压缩版本。它的位置在arch/x86/boot/bzImage。
首先将它拷贝到Host系统的/boot目录中(可以重命名为vmlinuz-5.4-xxx,xxx为区分标记,可以设置为任意特定的字符串)$ 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 | git clone https://gitee.com/mirrors/u-boot.git |
使用 qemu 虚拟机:
1 | qemu-system-arm -M vexpress-a9 -m 256 -kernel u-boot -nographic |
构建内核
构建内核时,注意在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 | dd if=/dev/zero of=rootfs.img bs=4096 count=1024 |
b. 挂载镜像文件
1 | mkdir rootfs |
c. 准备 dev 目录
1 | sudo mkdir rootfs/dev |
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 | mount -t devtmpfs devtmpfs /dev |
但重启后配置会消失。使用 /etc/fstab 或者 /etc/init.d/rcS 来自动挂载。
构建 busybox
下载 BusyBox 源代码 并验证数字签名后,进行构建:
1 | make defconfig |
最后卸载 rootfs 即可:
1 | sudo umount rootfs |
使用 qemu 验证
在 树莓派5 ARM64 架构下验证:
1 | 注意使用制作的 rootfs.img 镜像文件 |
在 vexpress-a9 架构下验证:
1 | qemu-system-arm -M vexpress-a9 -m 512M -kernel arch/arm/boot/zImage \ |
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 | (gdb) target remote localhost:1234 |
构建模块并调试
参考 构建外部模块
在编译完内核后,将内核模块安装到 rootfs 的 lib/modules 目录中:
1 | make INSTALL_MOD_PATH=rootfs/ modules_install |
调试技巧
如果要关闭某个函数的编译优化,使用 __attribute__((optimize(0))) 编译器优化选项。如果要关闭某段代码的优化:
1 | void foo() { |
如果进入根文件系统后,需要创建文件或者目录,需要在内核启动时设定根文件系统的读写权限:
1 | .... --append "nokaslr root=/dev/vda rw init=/bin/ash" ... |
在 root=/dev/vda 后紧跟 rw 标志。
如果要 init 进程在进入 shell 前做一些其他任务,则启动参数init可以使用 /linuxrc,并在根文件系统中创建 /etc/init.d/rcS 可执行脚本,在脚本中可以编写启动其他用户进程。