点击这里在哔哩哔哩bilibili在线观看配套的加餐视频(就是一些补充)。
下面介绍Linux内核编译环境和测试环境的搭建过程,当然我也为各位朋友准备好了已经安装好的虚拟机镜像,只需下载运行即可。
点击这里从百度网盘下载对应平台的虚拟机镜像, x86_64
(也就是你平时用来安装windows系统的电脑,或者2020年前的苹果电脑)选择ubuntu-x64_64.zip
,arm64
(2020年末之后的苹果电脑)选择ubuntu-aarch64.zip
。虚拟机运行后,登录界面的密码是1
。
安装Linux发行版,你可以选择以下几种方式:
接下来介绍几个常用的虚拟机软件。Windows系统推荐使用VirtualBox,arm64苹果系统推荐使用UTM。 如果你在看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)。sudo apt-get install qemu qemu-kvm virt-manager qemu-system -y
安装(需要重启才能以非root用户启动)。1G
),创建虚拟机后打开配置,“VirtIO驱动器” -> “删除”,然后再“新建” -> “导入”,可以选择vmdk
或qcow2
等格式,会统一转换成qcow2
格式,保存后生效。安装后的虚拟机文件在~/Library/Containers/com.utmapp.UTM/Data/Documents
目录下,默认Finder中不显示这个目录,可以在家目录下打开Show View Options -> Show Library Folder
。需要注意一下,网络如果选择共享网络
会出现不稳定断网的情况,建议选择桥接(高级)
,选择桥接
时如果宿主机的网络切换了(如连了另一个wifi)虚拟机中的网络也要断开重连一下。如果出现虚拟机网络经常断开的情况,可以尝试宿主机换一个稳定的网络。配置虚拟机时,Windows系统cpu核数查看方法: 任务管理器->性能->CPU,苹果电脑cpu核数查看方法: sysctl hw.ncpu
或sysctl -n machdep.cpu.core_count
,Linux系统cpu核数查看方法lscpu
。
Linux发行版很多,我们选择一个使用人数相对较多的Ubuntu发行版。x86_64的ubuntu22.04,arm64的ubuntu22.04下载。x86_64的ubuntu20.04,arm64的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 binutils-aarch64-linux-gnu -y # aarch64-linux-gnu-addr2line 等工具
sudo apt install gcc-aarch64-linux-gnu -y # aarch64-linux-gnu-gcc
sudo apt install gcc-arm-linux-gnueabi gcc-arm-linux-gnueabihf -y # arm32的交叉编译软件
sudo apt install gcc-riscv64-linux-gnu -y # riscv交叉编译软件
特定版本的交叉编译软件:
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 # 指向特定版本
除了在vmware虚拟机中搭建开发环境,还可以在docker中搭建开发环境。注意qemu的权限配置请参考后面的“qemu配置”相关的章节。
参考中文翻译QEMU Documentation/Networking/NAT。
qemu命令行的网络参数修改成(model
和macaddr
可以自己指定):
-net tap \
-net nic,model=virtio,macaddr=00:11:22:33:44:01 \
注意在虚拟机中,不要手动配置ip,要运行systemctl restart networking.service
自动获取ip地址。
宿主机中桥接模式配置:
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源码托管在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:8888
(8888
是${HOME}/.config/code-server/config.yaml
配置文件中配置的端口)。
有些格式的文件可能不会自动换行显示,可以勾选View -> Word Wrap
。
注意,和vscode客户端不一样,vscode server装插件时有些插件无法搜索到,这时就需要在vscode网站上下载.vsix
文件,手动安装。
常用插件:
C语言(尤其是内核代码)推荐使用插件C/C++ GNU Global。使用命令sudo apt install global -y
安装gtags插件,Linux内核代码使用命令make gtags
生成索引文件。
C++语言推荐使用插件C/C++或clangd。浏览C/C++代码时,建议这两个插件和C/C++ GNU Global选一个,不要安装多个。
Vue.js推荐使用插件Vetur、Vue Peek、ESLint、Bracket Pair Colorizer 2、VueHelper
markdown插件Markdown Preview Enhanced
当想在vscode客户端打开远程的文件时, 可以使用 remote-ssh插件.
https://github.com/KDAB/codebrowser
这里我们不介绍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:
# 生成id_rsa_2和id_rsa_2.pub
ssh-keygen -f ~/.ssh/id_rsa_2
# 创建配置文件
cat <<EOF > ~/.ssh/config
# 默认走这里github.com
Host
HostName github.com
User git
IdentityFile ~/.ssh/id_rsa
IdentitiesOnly yes
# 指定账号specifiedaccount
Host github.com-specifiedaccount
HostName github.com
User git
IdentityFile ~/.ssh/id_rsa_2
IdentitiesOnly yes
EOF
# 重新设置url,specifiedaccount为指定的账号名 git remote set-url origin git@github.com-specifiedaccount:specifiedaccount/repo-name.git
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-pick
或git am
合补丁时如果有冲突,在解决完冲突后,在commit
信息中在Conflicts:
后列出冲突文件,如:
Conflicts:
include/linux/sunrpc/clnt.h
Commit xxxxxxxxx ("xxx: here is subject") 这里描述冲突的原因,如果后面还有补丁就用封号;
[Commit xxxxxxxxx ("xxx: here is subject") 最后一个补丁用点号.]
查看帮助文档man git format-patch
:
--stat[=<width>[,<name-width>[,<count>]]]
生成一个 diffstat。默认情况下,文件名部分将使用尽可能多的空间,其余部分用于图形部分。最大宽度默认为终端宽度,如果未连接终端则为 80 列,可以通过 <width> 覆盖。文件名部分的宽度可以通过在逗号后提供另一个宽度 <name-width> 来限制。图形部分的宽度可以通过使用 --stat-graph-width=<width> 来限制(影响所有生成统计图的命令),或通过设置 diff.statGraphWidth=<width>(不影响 git format-patch)。通过提供第三个参数 <count>,可以限制输出到前 <count> 行,如果有更多行,则以 ... 结尾。
这些参数也可以单独设置,使用 --stat-width=<width>、--stat-name-width=<name-width> 和 --stat-count=<count>。
如果文件名较长,可以用以下命令让补丁中的路径显示完整:
git format-patch -3 --cover-letter --stat=300,200
单行显示补丁,前面再加上时间:
# --oneline:简化每条提交的显示。
# --date=short:以简短的日期格式(YYYY-MM-DD)显示时间。
# --format="%ad %h %s":自定义输出格式,其中:
# %ad:显示提交的日期(由 --date 指定的格式),%d: 不包含时分秒
# %ci:显示提交的 日期和时间,%i: 包含时分秒
# %h:显示提交的简短哈希值。
# %s:显示提交的信息。
# %an:提交者的名字。
# %ae:提交者的邮箱。
git log --oneline --date=short --format="%ad %h %s %an <%ae>" --author=yourname
用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_BINFMT_MISC
CONFIG_9P_FS
CONFIG_BLK_DEV_LOOP
CONFIG_BLK_DEV_SD
CONFIG_BLK_DEV_NVME
rm build -rf && mkdir build
cp ../tmp/configs/x86_64-config build/.config
编译和安装命令如下:
make O=build menuconfig # 交互式地配置内核的编译选项,.config文件放在build目录下
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
也可以在一个仓库下编译多个体系结构,如:
rm x86_64-build -rf && mkdir x86_64-build
cp ../tmp/configs/x86_64-config x86_64-build/.config
rm aarch64-build -rf && mkdir aarch64-build
cp ../tmp/configs/aarch64-config aarch64-build/.config
make O=x86_64-build menuconfig
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- O=aarch64-build menuconfig
make O=x86_64-build bzImage -j`nproc`
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- O=aarch64-build Image -j`nproc`
老版本(如v5.17)编译如果报错FAILED: load BTF from vmlinux: Invalid argument
,可以尝试关闭CONFIG_DEBUG_INFO_BTF
配置。
举个例子,把Linux内核仓库下的fs/ext2
复制出来,修改Makefile
文件:
CONFIG_EXT2_FS := m
CONFIG_EXT2_FS_XATTR := y
CONFIG_EXT2_FS_POSIX_ACL := y
CONFIG_EXT2_FS_SECURITY := y
EXTRA_CFLAGS += -DCONFIG_EXT2_FS=1 -DCONFIG_EXT2_FS_XATTR=1 \
-DCONFIG_EXT2_FS_POSIX_ACL=1 -DCONFIG_EXT2_FS_SECURITY=1
obj-$(CONFIG_EXT2_FS) += ext2.o
ext2-y := balloc.o dir.o file.o ialloc.o inode.o \
ioctl.o namei.o super.o symlink.o trace.o
# For tracepoints to include our trace.h from tracepoint infrastructure
CFLAGS_trace.o := -I$(src)
ext2-$(CONFIG_EXT2_FS_XATTR) += xattr.o xattr_user.o xattr_trusted.o
ext2-$(CONFIG_EXT2_FS_POSIX_ACL) += acl.o
ext2-$(CONFIG_EXT2_FS_SECURITY) += xattr_security.o
KDIR := /root/code/linux/x86_64-build/
PWD := $(shell pwd)
# 设置交叉编译工具链的前缀和目标架构
CROSS_COMPILE := aarch64-linux-gnu-
ARCH := arm64
all:
$(MAKE) -C $(KDIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) clean
然后编译:
make # 生成ko文件
make clean # 清除编译结果
注意minix文件系统的Makefile
中的minix-objs
要改成minix-y
。
参考简介 — The Linux Kernel documentation。
如果你的环境还没安装依赖软件,运行make O=build SPHINXOPTS=-v htmldocs -j`nproc`
后可能报以下错误:
Documentation/Makefile:41: 找不到 'sphinx-build' 命令。请确保已安装 Sphinx 并在 PATH 中,或设置 SPHINXBUILD make 变量以指向 'sphinx-build' 可执行文件的完整路径。
DISTRIB_ID=Ubuntu
检测到的操作系统:DISTRIB_RELEASE=22.04
DISTRIB_CODENAME=jammy
DISTRIB_DESCRIPTION="Ubuntu 22.04.2 LTS"。
"convert"。
警告:最好安装 "dot"。
警告:最好安装 "dvipng"。
警告:最好安装 "ensurepip",否则构建将无法工作。
错误:请安装 "fonts-noto-cjk"。
警告:最好安装 "latexmk"。
警告:最好安装 "rsvg-convert"。
警告:最好安装 "texlive-lang-chinese"。
警告:最好安装 "xelatex"。
警告:最好安装
你应该运行:
sudo apt-get install imagemagick graphviz dvipng python3-venv fonts-noto-cjk latexmk librsvg2-bin texlive-lang-chinese texlive-xetex
Sphinx 需要通过以下方式安装:
1) 通过 pip/pypi:
/usr/bin/python3 -m venv sphinx_2.4.4
. sphinx_2.4.4/bin/activate
pip install -r ./Documentation/sphinx/requirements.txt
如果你想退出虚拟环境,可以使用:deactivate
2) 作为包安装:
sudo apt-get install python3-sphinx
Sphinx >= 3.0 会在同名用于多个类型(函数、结构、枚举等)时产生误报警告。这是已知的 Sphinx 错误。更多详情,请查看:
请注意,https://github.com/sphinx-doc/sphinx/pull/8313
2 个必需依赖项,无法构建,位于 ./scripts/sphinx-pre-install 第 997 行。
由于缺少
make[2]: *** [Documentation/Makefile:43:htmldocs] 错误 2
make[1]: *** [/home/linux/code/linux/Makefile:1692:htmldocs] 错误 2
make: *** [Makefile:234:__sub-make] 错误 2
根据提示安装所需软件:
sudo apt-get install imagemagick graphviz dvipng python3-venv fonts-noto-cjk latexmk librsvg2-bin texlive-lang-chinese texlive-xetex -y
sudo apt-get install python3-sphinx -y
再次编译:
make O=build SPHINXOPTS=-v htmldocs -j`nproc` # -v 获得更详细的输出。
# make O=build cleandocs # 删除生成的文档
如果你要更方便的使用一些调试的功能,就要加一些额外的补丁。
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_KEYS
和CONFIG_SYSTEM_REVOCATION_KEYS
配置值,在编译环境上编译安装后,删除build/mod/lib/modules/xxx/build
和build/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-grub
,x86
的grub.cfg
文件在/boot/grub/grub.cfg
,arm64
的grub.cfg
文件在/boot/efi/boot/grub/grub.cfg
。麒麟桌面系统要在把grub.cfg
新生成的启动项里的security=kysec
改成security=
(注意后面有空格)。
centos下运行grub2-mkconfig -o /boot/grub2/grub.cfg
。
麒麟server 4.19替换内核的步骤:
rpm -i kernel-4.19.90-23.29.v2101.fortest.ky10.aarch64.rpm kernel-core-4.19.90-23.29.v2101.fortest.ky10.aarch64.rpm kernel-modules-* --force
cat /boot/grub2/grubenv # 查看默认启动项
vim /boot/efi/EFI/kylin/grub.cfg # 从这里复制 Kylin Linux Advanced Server (4.19.90-23.29.v2101.fortest.ky10.aarch64) V10 (Lance)
grub2-set-default "Kylin Linux Advanced Server (4.19.90-23.29.v2101.fortest.ky10.aarch64) V10 (Lance)" # 更改默认启动项
sync # 确保落盘
前面介绍完了编译环境,编译出的代码我们不能直接在编译环境上运行,还要再启动qemu虚拟机运行我们编译好的内核。
Bochs: x86硬件平台的开源模拟器,帮助文档少,只能模拟x86处理器。
QEMU: quick emulation,高速度、跨平台的开源模拟器,能模拟x86、arm等处理器,与Linux的KVM配合使用,能达到与真实机接近的速度。
第1类虚拟机监控程序: 直接在主机硬件上运行,直接向硬件调度资源,速度快。如Linux的KVM(免费)、Windows的Hyper-V(收费)。
第2类虚拟机监控程序: 在常规操作系统上以软件层或应用的形式运行,速度慢。如Vmware Workstation、Oracal VirtualBox。
本教程中,我们使用qemu来测试运行内核代码。
测试编译好的内核我们不直接用发行版的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镜像。
也可以在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.qcow2
,image.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报错dmesg
中virtio_net virtio0 enp0s1: renamed from eth0
,解决办法:
sudo vim /etc/netplan/50-cloud-init.yaml # 把网络接口名改成enp0s1
sudo netplan apply
关于各个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`
非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
制作好的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调试来理解那些写得不好的用户态代码,尤其是公司内部一些不开源的比狗屎还难看的用户态代码(当然其中也包括我自己写的狗屎一样的代码)。
转方向做了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调试需求,也能尽量少的修改代码。
另外,也建议把需要调试的函数的inline
关键字去掉。
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 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辅助调试功能可以更方便的调试内核(如打印断点处的进程名和进程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来打印就很直观。
如结构体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的地址,也就是偏移量。
使用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模块中的代码进行打断点之类的操作了。