Linux内核开发环境

建议点击这里查看个人主页上的最新原文

点击这里从百度网盘下载配套的视频教程

点击这里在哔哩哔哩bilibili在线观看配套的教学视频

点击这里在哔哩哔哩bilibili在线观看配套的加餐视频(就是一些补充)

点击跳转到内核课程所有目录

下面介绍Linux内核编译环境和测试环境的搭建过程,当然我也为各位朋友准备好了已经安装好的虚拟机镜像,只需下载运行即可。

点击这里从百度网盘下载对应平台的虚拟机镜像x86_64(也就是你平时用来安装windows系统的电脑,或者2020年前的苹果电脑)选择ubuntu-x64_64.ziparm64(2020年末之后的苹果电脑)选择ubuntu-aarch64.zip。虚拟机运行后,登录界面的密码是1

安装Linux发行版

安装Linux发行版,你可以选择以下几种方式:

  • 在物理机上直接安装安装Linux发行版。这是工作时比较推荐的一种安装方法,可以最大程度的利用硬件资源。
  • 在容器(如docker)中安装Linux发行版。这种方式也能最大程度的利用硬件资源,还能快速恢复开发环境。
  • 在虚拟机上安装Linux发行版。在学习阶段推荐这种方式安装,因为一旦系统出现什么问题可以快速恢复。

虚拟机软件

接下来介绍几个常用的虚拟机软件。Windows系统推荐使用VirtualBox,arm64苹果系统推荐使用UTM。如果你在看VMware虚拟机相关的视频,请转为查看这个视频

  • VirtualBox。首先在VirtualBox下载界面下载对应平台的安装包,比如如果要在Windows系统下安装VirtualBox,点击Windows hosts下载安装包。VirtualBox的安装过程很简单,只需根据安装提示操作即可。VirtualBox安装完成后,下载VirtualBox 7.0.14 Oracle VM VirtualBox Extension Pack安装插件。arm芯片的版本好像只有7.0.8版本才有。
  • VMware下载点击这篇文章,注册登录账号,下载时的信息填写类似Address 1: 1ONE, City: SACRAMENTO, Postal code: 94203-0001, Country/Territory: United States, State or province: California。安装过程很简单,只需根据提示操作即可。Linux下安装VMware时需要注意的是/tmp目录的挂载不能在/etc/fstab文件中指定noexec,还需要安装gcc较新的版本(如VMware-Workstation-Full-17.5.1-23298084.x86_64.bundle在ubuntu2204下安装时要安装gcc12,默认安装的是gcc11)。
  • Virtual Machine Manager。这个虚拟机软件只用在Linux平台上,如果你物理机上安装的操作系统是Linux,那么使用这个软件运行虚拟机就比较合适。比如在Ubuntu上使用命令sudo apt-get install qemu qemu-kvm virt-manager qemu-system -y安装(需要重启才能以非root用户启动)。
  • UTM。只针对苹果电脑系统,从github下载安装包。建议在配置比较高(尤其是内存)的苹果电脑上使用,如果配置比较低可能会遇到一些问题。从github上下载安装包。导入虚拟机时,选择"创建一个新虚拟机" -> “虚拟化” -> “其他” -> 打勾"Skip ISO boot",“Storage"选择小一点的容量(如1G),创建虚拟机后打开配置,“VirtIO驱动器” -> “删除”,然后再"新建” -> “导入”,可以选择vmdkqcow2等格式,会统一转换成qcow2格式,保存后生效。安装后的虚拟机文件在~/Library/Containers/com.utmapp.UTM/Data/Documents目录下,默认Finder中不显示这个目录,可以在家目录下打开Show View Options -> Show Library Folder。需要注意一下,网络如果选择共享网络会出现不稳定断网的情况,建议选择桥接(高级),选择桥接时如果宿主机的网络切换了(如连了另一个wifi)虚拟机中的网络也要断开重连一下。如果出现虚拟机网络经常断开的情况,可以尝试宿主机换一个稳定的网络。

配置虚拟机时,Windows系统cpu核数查看方法:任务管理器->性能->CPU,苹果电脑cpu核数查看方法: sysctl hw.ncpusysctl -n machdep.cpu.core_count,Linux系统cpu核数查看方法lscpu

安装Ubuntu发行版

Linux发行版很多,我们选择一个使用人数相对较多的Ubuntu发行版x86_64的ubuntu22.04arm64的ubuntu22.04下载。x86_64的ubuntu20.04arm64的ubuntu20.04

安装内核编译和测试所需软件:

sudo apt install git -y # 代码管理工具
sudo apt install build-essential -y # 编译所需的常用软件,如gcc等
sudo apt-get install qemu qemu-kvm qemu-system -y # qemu虚拟机相关软件
sudo apt-get install virt-manager -y # docker中不需要安装,虚拟机图形界面,会安装iptables,可能需要重启才能以非root用户启动virt-manager,当然对于内核开发来说安装这个软件是为了生成自动生成virbr0网络接口
sudo apt install flex bison bc kmod pahole -y # 内核编译所需软件
sudo apt-get install libelf-dev libssl-dev libncurses-dev -y # 内核源码编译依赖的库
sudo apt install zstd -y

交叉编译所需软件:

sudo apt-get install u-boot-tools -y
sudo apt install gcc-9-aarch64-linux-gnu -y # 指定版本的交叉编译软件
mv /usr/bin/aarch64-linux-gnu-gcc /usr/bin/aarch64-linux-gnu-gcc.bak # 原来指向其他版本
ln -s /usr/bin/aarch64-linux-gnu-gcc-9 /usr/bin/aarch64-linux-gnu-gcc # 指向特定版本

docker环境

除了在vmware虚拟机中搭建开发环境,还可以在docker中搭建开发环境。注意qemu的权限配置请参考后面的“qemu配置”相关的章节。

NAT模式

参考中文翻译QEMU Documentation/Networking/NAT

qemu命令行的网络参数修改成(modelmacaddr可以自己指定):

-net tap \
-net nic,model=virtio,macaddr=00:11:22:33:44:01 \

注意在虚拟机中,不要手动配置ip,要运行systemctl restart networking.service自动获取ip地址。

桥接模式(TODO)

宿主机中桥接模式配置:

apt install bridge-utils -y # brctl命令
brctl addbr br0
brctl stp br0 on
brctl addif br0 eth0
# brctl delif br0 eth0
ip addr del dev eth0 172.17.0.2/16 # 清除ip
ifconfig br0 172.17.0.2/16 up # 或 ifconfig virbr0 172.17.0.2 netmask 172.17.0.1 up
route add default gw 172.17.0.1
sysctl net.ipv4.ip_forward=1 # 或 echo 1 > /proc/sys/net/ipv4/ip_forward

虚拟机中:

ip addr add 172.17.0.3/16 dev ens2
# ip addr del dev ens2 172.17.0.3/16 # 删除ip
ip link set dev ens2 up
# ip link set dev ens2 down
# 网关可不配置
# route del default dev ens2
# route add default gw 172.17.0.1 # ip route add default via 172.17.0.1 dev ens2

手动配置ip没法访问外网,暂时还不知道要怎么弄,如果有知道的朋友可以指导我一下。

代码管理和编辑工具

使用code-server浏览和编辑代码

为了尽可能的方便,推荐使用code-server在网页上浏览和编辑代码,当然你也可以使用自己习惯的代码浏览和编辑工具。

code-server源码托管在GitHub,安装命令:

curl -fsSL https://code-server.dev/install.sh | sh

或者下载对应系统的安装包

设置开机启动:

sudo systemctl enable --now code-server@$USER

配置文件是${HOME}/.config/code-server/config.yaml,当不需要密码时修改成auth: none

修改完配置后,需要再重启服务:

sudo systemctl restart code-server@$USER

然后打开浏览器输入http://localhost:88888888${HOME}/.config/code-server/config.yaml配置文件中配置的端口)。

有些格式的文件可能不会自动换行显示,可以勾选View -> Word Wrap

注意,和vscode客户端不一样,vscode server装插件时有些插件无法搜索到,这时就需要在vscode网站上下载.vsix文件,手动安装。

常用插件:

当想在vscode客户端打开远程的文件时, 可以使用 remote-ssh插件.

Woboq CodeBrowser

https://github.com/KDAB/codebrowser

git的一些特殊用法

这里我们不介绍git的一般用法,仅介绍一些特殊用法。

查看帮助文档man 1 git log

       -L<start>,<end>:<file>, -L:<funcname>:<file>
           跟踪给定 <start>,<end> 或函数名正则表达式 <funcname> 所定义的行范围的演变,位于 <file> 内。您不可以提供任何路径规范限定符。目前此功能仅限于从单个修订版本开始的遍历,即您只能提供零个或一个正面修订参数,<start><end>(或 <funcname>)必须存在于起始修订版本中。您可以多次指定此选项。隐含--patch。可以使用 --no-patch 抑制补丁输出,但当前尚未实现其他差异格式(即 --raw、--numstat、--shortstat、--dirstat、--summary、--name-only、--name-status、--check)。

           <start><end> 可以采用以下形式之一:

           •   数字

               如果 <start><end> 是数字,则指定绝对行号(从 1 开始计数)。

           •   /正则表达式/

               此形式将使用与给定 POSIX 正则表达式匹配的第一行。如果 <start> 是正则表达式,则它将从前一个 -L 范围的末尾开始搜索,如果有的话,否则从文件开头开始搜索。如果 <start> 是 ^/正则表达式/,则它将从文件的开头开始搜索。如果 <end> 是正则表达式,则它将从由 <start> 给出的行开始搜索。

           •   +偏移量 或 -偏移量

               这仅对 <end> 有效,并将指定相对于由 <start> 给出的行之前或之后的行数。

           如果 :<funcname> 出现在 <start><end> 的位置,则它是一个正则表达式,表示从第一行与 <funcname> 匹配的 funcname 行开始,直到下一个 funcname 行。:<funcname> 从前一个 -L 范围的末尾开始搜索,如果有的话,否则从文件的开头开始搜索。^:<funcname> 从文件的开头开始搜索。函数名称的确定方式与 git diff 解析补丁块标题的方式相同(请参见 gitattributes(5) 中关于定义自定义块标题的说明)。

以内核主线代码fs/namespace.c文件为例,查看do_new_mount函数:

git log -L:do_new_mount:fs/namespace.c

我们发现列出的却是do_new_mount_fc的修改记录,因为do_new_mount_fc包含字符串do_new_mount,又在do_new_mount()函数前面,解决方法是在do_new_mount后面再加个\(

git log -L:do_new_mount\(:fs/namespace.c

在内核开发过程中我们经常需要找某个commit提交记录是哪个版本引入的,使用以下命令

git name-rev <commit>

如果我们有两个github账号,两个账号不能在网站上添加同一个ssh key,这时我们就要再生成一个ssh key,还要将ssh私钥添加到ssh代理:

ssh-keygen -t ed25519-sk -C "YOUR_EMAIL" # 生成新的key
eval "$(ssh-agent -s)" # 启动 SSH 代理
ssh-add ~/.ssh/id_ed25519 # 将 SSH 私钥添加到 SSH 代理

cherry-pick多个commit:

git cherry-pick <commit1>..<commitN> # 不包含commit1

如果多个commit中包含有Merge的commit,直接cherry-pick多个会报错error: 提交 xxxx 是一个合并提交但未提供 -m 选项,可以把git log --oneline的输出放到文件commits.txt中,把Merge相关的commit删除,并删除掉每行的后面的commit信息,每行只保留commit号,然后用以下脚本cherry-pick(各位朋友如果有什么更好的方法请一定要联系告诉我):

# tac 从最后一行开始 cherry-pick
tac commits.txt | while IFS= read -r commit; do
	git cherry-pick $commit
	if [ $? -eq 0 ]; then
		echo "合并成功"
	else
		echo "合并失败"
		return
	fi
done
echo "全部合并成功"

git cherry-pickgit am合补丁时如果有冲突,在解决完冲突后,在commit信息中在Conflicts:后列出冲突文件,如:

Conflicts:
        include/linux/sunrpc/clnt.h

代码编译

获取代码

用git下载内核代码,仓库链接可以点击内核网站上对应版本的[browse] -> summary查看,我们下载mainline版本的代码:

git clone https://kernel.googlesource.com/pub/scm/linux/kernel/git/torvalds/linux.git # 国内使用googlesource仓库链接比较快

也可以在/pub/linux/kernel/下载某个版本代码的压缩包。

编译步骤

建议新建一个build目录,把所有的编译输出存放在这个目录下,注意.config文件复制到build/.config.config配置文件至少要打开以下配置(建议通过make O=build menuconfig命令修改):

CONFIG_EXT4_FS
CONFIG_XFS_FS
CONFIG_VIRTIO_BLK
CONFIG_VIRTIO_NET
CONFIG_SCSI_VIRTIO
CONFIG_BLK_DEV_SD
CONFIG_BLK_DEV_NVME
CONFIG_BINFMT_MISC
rm build -rf && mkdir build
cp /home/sonvhi/chenxiaosong/code/tmp/configs/x86_64-config build/.config

编译和安装命令如下:

make O=build menuconfig # 交互式地配置内核的编译选项
make O=build olddefconfig -j`nproc`
make O=build bzImage -j`nproc` # x86_64
make O=build Image -j`nproc` # aarch64,比如2020年末之后的arm芯片的苹果电脑上vmware fusion安装的ubuntu
make O=build modules -j`nproc`
mkdir -p build/boot && make O=build install INSTALL_PATH=boot -j`nproc`
make O=build modules_install INSTALL_MOD_STRIP=1 INSTALL_MOD_PATH=mod -j`nproc`

x86_64下,如果是交叉编译其他架构,ARCH的值为arch/目录下相应的架构,编译命令是:

make ARCH=i386 O=build bzImage # x86 32bit
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi-  O=build zImage # armel, arm eabi(embeded abi) little endian, 传参数用普通寄存器
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- O=build zImage # armhf, arm eabi(embeded abi) little endian hard float, 传参数用fpu的寄存器,浮点运算性能更高
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- O=build Image
make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- O=build Image

一些额外的补丁

如果你要更方便的使用一些调试的功能,就要加一些额外的补丁。

  • 降低编译优化等级,默认的内核编译优化等级太高,用GDB调试时不太方便,有些函数语句被优化了,无法打断点,这时就要降低编译优化等级。做好的虚拟机中已经打上了降低编译优化等级的补丁。比如x86_64架构下可以在x86_64目录下选择对应版本的补丁,更多详细的内容请查看GDB调试相关的章节。

  • dump_stack()输出的栈全是问号的解决办法。如果你使用dump_stack()输出的栈全是问号,可以 revert 补丁 f1d9a2abff66 x86/unwind/orc: Don't skip the first frame for inactive tasks。主线已经有补丁做了 revert: 230db82413c0 x86/unwind/orc: Fix unreliable stack dump with gcov

  • 肯定还有一些其他有用的补丁,后面再补充哈。

发行版替换内核

用发行版/boot/config-`uname -r` 配置文件,删除CONFIG_SYSTEM_TRUSTED_KEYSCONFIG_SYSTEM_REVOCATION_KEYS配置值,在编译环境上编译安装后,删除build/mod/lib/modules/xxx/buildbuild/mod/lib/modules/xxx/source链接文件,然后压缩(文件太多,不压缩复制会很慢)打包复制到待测环境上。

build/mod/lib/modules/xxx/复制到待测环境上的/lib/modules/路径,把build/boot/目录下的文件复制到待测环境上的/boot/路径下。

生成initrd.img,其中xxx为内核版本:

# centos
mkinitrd /boot/initrd.img-xxx xxx
# ubuntu
mkinitramfs -o /boot/initrd.img-xxx xxx

ubuntu下运行update-grubx86grub.cfg文件在/boot/grub/grub.cfgarm64grub.cfg文件在/boot/efi/boot/grub/grub.cfg。麒麟桌面系统要在把grub.cfg新生成的启动项里的security=kysec改成security= (注意后面有空格)。

使用QEMU测试内核代码

前面介绍完了编译环境,编译出的代码我们不能直接在编译环境上运行,还要再启动qemu虚拟机运行我们编译好的内核。

模拟器与虚拟机

Bochs:x86硬件平台的开源模拟器,帮助文档少,只能模拟x86处理器。

QEMU:quick emulation,高速度、跨平台的开源模拟器,能模拟x86、arm等处理器,与Linux的KVM配合使用,能达到与真实机接近的速度。

第1类虚拟机监控程序:直接在主机硬件上运行,直接向硬件调度资源,速度快。如Linux的KVM(免费)、Windows的Hyper-V(收费)。

第2类虚拟机监控程序:在常规操作系统上以软件层或应用的形式运行,速度慢。如Vmware Workstation、Oracal VirtualBox。

本教程中,我们使用qemu来测试运行内核代码。

制作测试用的qcow2镜像的脚本

测试编译好的内核我们不直接用发行版的iso镜像安装的系统,而是使用脚本生成比较小的镜像(不含有图形界面)。进入目录courses,选择相应的cpu架构,如x86_64目录。执行create-raw.sh生成raw格式的镜像,这个脚本会调用到create-debian.sh,是从syzkaller的脚本经过修改而来。

注意riscv64架构的镜像,可以直接下载ubuntu2204(选择[QEMU emulator]),使用文档

生成raw格式镜像后,再执行以下命令转换成占用空间更小的qcow2格式:

# -p 显示进度, -f 源镜像格式, -O 转换后的格式, 后面再紧接的是:源文件名称,转换后的文件名称
qemu-img convert -p -f raw -O qcow2 image.raw image.qcow2

再执行脚本link-scripts.sh把脚本链接到相应的目录,执行update-base.sh启动虚拟机更新镜像(如再安装一些额外的软件),镜像更新完后关闭虚拟机,再执行create-qcow2.sh生成指向基础镜像的qcow2镜像。

通过iso安装发行版

也可以在Virtual Machine Manager中通过iso文件安装发行版,安装完成后的qcow2镜像要用命令行启动,安装时不使用LVM,而是把磁盘的某个分区挂载到根路径/

在 Virtual Machine Manager 中创建 qcow2 格式,会马上分配所有空间,所以需要在命令行中创建 qcow2:

qemu-img create -f qcow2 image.qcow2 512G
file image.qcow2 # 查看文件的格式

可以再生成一个qcow2文件image2.qcow2,指向安装好的镜像image.qcow2image.qcow2作为备份文件, 注意<有些版本的qemu-img>要求源文件和目标文件都要指定绝对路径

qemu-img create -F qcow2 -b /path/image.qcow2 -f qcow2 /path/image2.qcow2 #  -F 源文件格式

iso安装发行版本后,默认是/dev/vda1-device virtio-scsi-pci)挂载到根路径/,如果要重新制作成/dev/vda挂载到根分区/,可以把qcow2文件里的内容复制出来,qcow2格式镜像的挂载:

sudo apt-get install qemu-utils -y # 要先安装工具软件
sudo modprobe nbd max_part=8 # 加载nbd模块
sudo qemu-nbd --connect=/dev/nbd0 image.qcow2 # 连接镜像
sudo fdisk /dev/nbd0 -l # 查看分区
sudo mount /dev/nbd0p1 mnt/ # 挂载分区
sudo umount mnt # 操作完后,卸载分区
sudo qemu-nbd --disconnect /dev/nbd0 # 断开连接
sudo modprobe -r nbd # 移除模块

当然也可以把qcow2转换成raw格式,然后把raw格式文件里的内容复制出来:

qemu-img convert -p -f qcow2 -O raw image.qcow2 image.raw

ubuntu24.04报错dmesgvirtio_net virtio0 enp0s1: renamed from eth0,解决办法:

sudo vim /etc/netplan/50-cloud-init.yaml # 把网络接口名改成enp0s1
sudo netplan apply

源码安装qemu

关于各个Linux发行版怎么安装qemu,可以参考qemu官网的介绍,下面主要介绍一下源码的安装方式,源码安装方式可以使用qemu的最新特性。

先安装Ubuntu编译qemu所需的软件:

# ubuntu 22.04
sudo apt-get install libattr1-dev libcap-ng-dev -y
sudo apt install ninja-build -y
sudo apt-get install libglib2.0-dev -y
sudo apt-get install libpixman-1-dev -y
sudo apt install python3-pip
pip install tomli

CentOS发行版安装编译qemu所需的软件:

sudo dnf install glib2-devel -y
sudo dnf install iasl -y
sudo dnf install pixman-devel -y
sudo dnf install libcap-ng-devel -y
sudo dnf install libattr-devel -y

# centos 9才需要, http://re2c.org/
git clone https://github.com/skvadrik/re2c.git
./autogen.sh
./configure  --prefix=${HOME}/chenxiaosong/sw/re2c
make && make install

# centos 要安装 ninja, https://ninja-build.org/
git clone https://github.com/ninja-build/ninja.git && cd ninja
./configure.py --bootstrap

# centos9, https://sparse.docs.kernel.org/en/latest/
git clone git://git.kernel.org/pub/scm/devel/sparse/sparse.git
make

再下载编译qemu:

git clone https://gitlab.com/qemu-project/qemu.git
cd qemu
git submodule init
git submodule update --recursive
mkdir build
cd build/
../configure --enable-kvm --enable-virtfs --prefix=${HOME}/chenxiaosong/sw/qemu/
make -j`nproc`

qemu配置

非root用户没有权限的解决办法:

# 源码安装的
sudo chown root libexec/qemu-bridge-helper
sudo chmod u+s libexec/qemu-bridge-helper
# apt安装的
sudo chown root /usr/lib/qemu/qemu-bridge-helper
sudo chmod u+s /usr/lib/qemu/qemu-bridge-helper

groups | grep kvm
sudo usermod -aG kvm $USER
su - $USER # 或退出shell重新登录, 但在tmux中不起作用

允许使用virbr0网络接口:

# 源码安装的
mkdir -p etc/qemu
vim etc/qemu/bridge.conf # 添加 allow virbr0
# apt安装的
sudo mkdir -p /etc/qemu/
sudo vim /etc/qemu/bridge.conf # 添加 allow virbr0

修改virbr0网段:

virsh net-list # 查看网络情况
virsh net-edit default # 编辑
virsh net-destroy default
virsh net-start default

qemu运行qcow2镜像

制作好的Ubuntu虚拟机镜像(从百度网盘中下载的)中的${HOME}/qemu-kernel/start.sh脚本中每个选项的可选值可以使用以下命令查看:

qemu-system-aarch64 -cpu ?
qemu-system-x86_64 -machine ?

如果自己编译内核,启动时指定内核,需要指定-kernel-append选项。

如果你的镜像是一个完整的镜像(比如通过iso安装),不想指定内核,就想用镜像本身自带的内核,可以把-kernel-append选项删除。

qemu启动后,按快捷键ctrl+a c(先按ctrl+a松开后再按c)再输入quit强制退出qemu,但不建议强制退出。

在系统启动界面登录进去后(而不是以ssh登录),默认的窗口大小不会自动调整,需要手动调整:

stty size # 可以先在其他窗口查看大小
echo "stty rows 54 cols 229" > stty.sh
. stty.sh

当启用了9p文件系统,就可以把宿主机的modules目录(当然也可以是其他任何目录)共享给虚拟机,具体参考Documentation/9psetup。虚拟机中执行脚本mod-cfg.sh(直接运行bash mod-cfg.sh可以查看使用帮助)挂载和链接模块目录。

root免密登录,/etc/ssh/sshd_config(注意不是ssh_config) 修改以下内容:

PermitRootLogin yes
PasswordAuthentication yes
PermitEmptyPasswords yes

曾经使用过fedora发行版,这里记录一下fedora的一些笔记。进入fedora虚拟机后:

# fedora 启动的时候等待: A start job is running for /dev/zram0,解决办法:删除 zram 的配置文件
mv /usr/lib/systemd/zram-generator.conf /usr/lib/systemd/zram-generator.conf.bak
# fedora26 安装 vim 前,先升级
sudo dnf update vim-common vim-minimal -y

注意fedora中账号密码输完后要用ctrl+j,不要用回车。

使用GDB调试内核代码

我刚开始是做用户态开发的,习惯了利用gdb调试来理解那些写得不好的用户态代码,尤其是公司内部一些不开源的比狗屎还难看的用户态代码(当然其中也包括我自己写的狗屎一样的代码)。

转方向做了Linux内核开发后,也尝试用qemu+gdb来调试内核代码。

要特别说明的是,内核的大部分代码是很优美的,并不需要太依赖qemu+gdb这一调试手段,更建议通过阅读代码来理解。但某些写得不好的内核模块如果利用qemu+gdb将能使我们更快的熟悉代码。

这里只介绍x86_64下的qemu+gdb调试,其他cpu架构以此类推,只需要做些小改动。

如果是其他cpu架构,要安装:

sudo apt install gdb-multiarch -y

编译选项和补丁

首先确保修改以下配置:

CONFIG_DEBUG_SECTION_MISMATCH=y # 防止内联
CONFIG_DEBUG_INFO=y # 调试信息
CONFIG_DEBUG_KERNEL=y # 调试信息
CONFIG_GDB_SCRIPTS=y # gdb python
DEBUG_INFO_REDUCED=n # 关闭
CONFIG_FRAME_POINTER=y # Makefile 中选择GCC编译选项
CONFIG_RANDOMIZE_BASE = n # 关闭地址随机化

可以使用我常用的x86_64的内核配置文件

gcc的编译选项O1优化等级不需要修改就可以编译通过。O0优化等级无法编译(尝试CONFIG_JUMP_LABEL=n还是不行),要修改汇编代码,有兴趣的朋友可以和我一直尝试。Og优化等级经过修改可以编译通过,x86_64合入目录courses/kernel/src/x86_64对应版本的补丁。建议使用Og优化等级编译,既能满足gdb调试需求,也能尽量少的修改代码。

QEMU命令选项

qemu启动虚拟机时,要添加以下几个选项:

-append "nokaslr ..." # 防止地址随机化,编译内核时关闭配置 CONFIG_RANDOMIZE_BASE
-S # 挂起 gdbserver
-gdb tcp::5555 # 端口5555, 使用 -s 选项表示用默认的端口1234
-s # 相当于 -gdb tcp::1234 默认端口1234,不建议用,最好指定端口

完整的启动命令查看制作好的Ubuntu虚拟机镜像(从百度网盘中下载的)中的${HOME}/qemu-kernel/start.sh脚本。

GDB命令

启动GDB:

gdb build/vmlinux

如果是其他架构:

gdb --tui build/vmlinux # --tui: Use a terminal user interface.
(gdb) set architecture aarch64

进入GDB界面后:

(gdb) target remote:5555 # 对应qemu命令中的-gdb tcp::5555
(gdb) b func_name # 普通断点
(gdb) hb func_name # 硬件断点,有些函数普通断点不会停下, 如: nfs4_atomic_open,降低优化等级后没这个问题

gdb命令的用法和用户态程序的调试大同小异。

GDB辅助调试功能

使用内核提供的GDB辅助调试功能可以更方便的调试内核(如打印断点处的进程名和进程id等)。

内核最新版本(2024.04)使用以下命令开启GDB辅助调试功能,注意最新版本编译出的脚本无法调试4.19和5.10的代码:

echo "set auto-load safe-path /" > ~/.gdbinit # 设置自动加载共享库文件的安全路径
echo "source ${HOME}/.gdb-linux/vmlinux-gdb.py" >> ~/.gdbinit
make O=build scripts_gdb # 在内核仓库目录下执行
rm -rf ${HOME}/.gdb-linux/
mkdir ${HOME}/.gdb-linux/
cp build/scripts/gdb/* ${HOME}/.gdb-linux/ -rf # 在内核仓库目录下执行
cp scripts/gdb/vmlinux-gdb.py ${HOME}/.gdb-linux/ # 在内核仓库目录下执行
sed -i '/sys.path.insert/s/^/# /' ${HOME}/.gdb-linux/vmlinux-gdb.py # 将sys.path.insert所在的行注释掉
sed -i '/sys.path.insert/a\sys.path.insert(0, "'${HOME}'/.gdb-linux")' ${HOME}/.gdb-linux/vmlinux-gdb.py # 插入 sys.path.insert(0, "${HOME}/.gdb-linux")

内核5.10使用以下命令开启GDB辅助调试功能,也可以调试内核4.19代码,但无法调试内核最新的代码:

echo "set auto-load safe-path /" > ~/.gdbinit # 设置自动加载共享库文件的安全路径
echo "source ${HOME}/.gdb-linux-5.10/vmlinux-gdb.py" >> ~/.gdbinit
make O=build scripts_gdb # 在5.10内核仓库目录下执行
rm -rf ${HOME}/.gdb-linux-5.10/
mkdir ${HOME}/.gdb-linux-5.10/
cp build/scripts/gdb/* ${HOME}/.gdb-linux-5.10/ -rf # 在5.10内核仓库目录下执行
cp scripts/gdb/vmlinux-gdb.py ${HOME}/.gdb-linux-5.10/ # 在5.10内核仓库目录下执行
sed -i '/sys.path.insert/s/^/# /' ${HOME}/.gdb-linux-5.10/vmlinux-gdb.py # 将sys.path.insert所在的行注释掉
sed -i '/sys.path.insert/a\sys.path.insert(0, "'${HOME}'/.gdb-linux-5.10")' ${HOME}/.gdb-linux-5.10/vmlinux-gdb.py # 插入 sys.path.insert(0, "${HOME}/.gdb-linux-5.10")

重新启动GDB就可以使用GDB辅助调试功能:

(gdb) apropos lx # 查看有哪些命令
(gdb) p $lx_current().pid # 打印断点所在进程的进程id
(gdb) p $lx_current().comm # 打印断点所在进程的进程名

GDB打印结构体偏移

结构体定义有时候加了很多宏判断,再考虑到内存对齐之类的因素,通过看代码很难确定结构体中某一个成员的偏移大小,使用gdb来打印就很直观。

如结构体struct cifsFileInfo:

struct cifsFileInfo {
    struct list_head tlist;
    ...
    struct tcon_link *tlink;
    ...
    char *symlink_target;
};

想要确定tlink的偏移,可以使用以下命令:

gdb ./cifs.ko # ko文件或vmlinux
(gdb) p &((struct cifsFileInfo *)0)->tlink

(struct cifsFileInfo *)0:这是将整数值 0 强制类型转换为指向 struct cifsFileInfo 类型的指针。这实际上是创建一个指向虚拟内存地址 0 的指针,该地址通常是无效的。这是一个计算偏移量的技巧,因为偏移量的计算不依赖于结构体的实际实例。

(0)->tlink: 指向虚拟内存地址 0 的指针的成员tlink

&(0)->tlink: tlink的地址,也就是偏移量。

ko模块代码调试

使用gdb vmlinux启动gdb后,如果调用到ko模块里的代码,这时候就不能直接对ko模块的代码进行打断点之类的操作,因为找不到对应的符号。

这时就要把符号加入进来。首先,查看被调试的qemu虚拟机中的各个段地址:

cd /sys/module/ext4/sections/ # ext4 为模块名
cat .text .data .bss # 输出各个段地址

在gdb窗口中加载ko文件:

add-symbol-file <ko文件位置> <text段地址> -s .data <data段地址> -s .bss <bss段地址>

这时就能开心的对ko模块中的代码进行打断点之类的操作了。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值