用Qemu搭建Linux和u-boot的调试环境
目录
用Qemu虚拟机来研究Linux和u-boot有很多好处,可以脱离硬件环境束缚,成本更低,相对于其他虚拟机速度快效率高,调试更方便。缺点是无法模拟实际的硬件外设,主要是研究内部原理。
1. 准备宿主机 #
需要在一个Linux主机上启动Qemu,以 WSL 的 Ubuntu24.04 系统为例,在开始之前,我们需要准备一些必要的工具和库:
sudo apt-get install git cmake build-essential bison flex swig python3-dev \
libssl-dev libncurses-dev libelf-dev bc zstd libtirpc-dev rpcbind libnsl-dev pkgconf
安装Qemu虚拟机:
sudo apt-get install qemu-system
默认安装的Qemu版本是8.2.2:
> qemu-system-x86_64 --version
QEMU emulator version 8.2.2 (Debian 1:8.2.2+ds-0ubuntu1.10)
Copyright (c) 2003-2023 Fabrice Bellard and the QEMU Project developers
2. 编译Linux内核 #
以 Linux 5.15.193 版本为例。
2.1 下载 #
wget https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/snapshot/linux-5.15.193.tar.gz
tar -zxvf linux-5.15.193.tar.gz
cd linux-5.15.193
下载linux-5.15.193的内核源码,这是一个LTS版本。
2.2 配置 #
export ARCH=x86
make O=./build x86_64_defconfig
选项参数O=./build表示编译过程生成的文件都输出到./build路径下。我们构建的是x86_64架构的虚拟机,所以要配置ARCH=x86,这样编译系统会在相应架构的路径arch/x86/configs/下找到配置文件x86_64_defconfig:
> ls -l arch/x86/configs/
total 24
-rw-r--r-- 1 lsc lsc 5970 Sep 11 23:17 i386_defconfig
-rw-r--r-- 1 lsc lsc 147 Sep 11 23:17 tiny.config
-rw-r--r-- 1 lsc lsc 5919 Sep 11 23:17 x86_64_defconfig
-rw-r--r-- 1 lsc lsc 744 Sep 11 23:17 xen.config
2.3 修改配置 #
# 添加RAMDisk支持
./scripts/config --file ./build/.config --enable CONFIG_BLK_DEV_RAM
./scripts/config --file ./build/.config --set-val CONFIG_BLK_DEV_RAM_COUNT 1
./scripts/config --file ./build/.config --set-val CONFIG_BLK_DEV_RAM_SIZE 65536
make O=./build olddefconfig
./scripts/config是Linux内核源码提供的一个实用工具脚本,用于在命令行中修改内核配置文件(通常是.config文件),而无需通过交互式的配置界面(如make menuconfig等)。基本语法是./scripts/config [option] <command>,默认修改的是./.config文件,可以用--file选项指定配置文件,常用的命令有:
--enable [option],将指定的内核配置设为y。--disable [option],将指定的内核配置设为n。--set-val [option] [value],设置指定内核配置的数值。--set-str [option] [value],设置指定内核配置的字符串。
因为我们要用 ramdisk 启动根文件系统,所以这里要使能内核的RAMdisk设备支持,并设置数量和大小。
使用./scripts/config就是手动修改.config文件,不会处理依赖关系,所以需要运行make olddefconfig,它会自动启用所有依赖项,解决一些冲突,还会使用默认值填充新的配置选项。
2.4 编译 #
make O=./build -j$(nproc)
选项-j$(nproc)表示根据CPU核心数量启动并行编译,编译完成后,内核镜像文件位于build/arch/x86/boot/bzImage:
> file build/arch/x86/boot/bzImage
build/arch/x86/boot/bzImage: Linux kernel x86 boot executable bzImage, version 5.15.193 (lsc@Bob) #1 SMP Wed Sep 24A
bzImage 是一个经过gzip压缩的内核镜像文件。
3. 编译BusyBox并生成rootfs #
BusyBox是一个集成了众多Unix工具的单一可执行文件,非常适合构建微型Linux系统。
3.1 下载 #
wget https://www.busybox.net/downloads/busybox-1.36.1.tar.bz2
tar -xvf busybox-1.36.1.tar.bz2
cd busybox-1.36.1
3.2 配置 #
make defconfig
先使用默认配置,然后改为静态编译:
make menuconfig
Location:
-> Settings
[*] Build static binary (no shared libs)
另外注意,在Ubuntu24.04上编译有个Bug 15934,需要禁用CONFIG_TC,否则无法编译通过。
3.3 编译 #
make -j$(nproc)
make install
编译完成后,生成的安装文件位于_install目录下:
> ls -l _install/
total 12
drwxr-xr-x 2 lsc lsc 4096 Sep 24 16:50 bin
lrwxrwxrwx 1 lsc lsc 11 Sep 24 16:50 linuxrc -> bin/busybox
drwxr-xr-x 2 lsc lsc 4096 Sep 24 16:50 sbin
drwxr-xr-x 4 lsc lsc 4096 Sep 24 16:50 usr
3.4 创建基本的根文件系统 #
cd _install
# 新建必要的目录
mkdir -p etc proc sys mnt dev tmp
# 创建inittab文件
cat > etc/inittab << EOF
::sysinit:/etc/init.d/rcS
::respawn:-/bin/sh
::askfirst:-/bin/sh
::ctrlaltdel:/bin/umount -a -r
EOF
chmod 755 etc/inittab
# 创建启动脚本
mkdir -p etc/init.d
cat > etc/init.d/rcS << EOF
#!/bin/sh
/bin/mount -a
EOF
chmod 755 etc/init.d/rcS
# 创建fstab文件
cat > etc/fstab << EOF
proc /proc proc defaults 0 0
tmpfs /tmp tmpfs defaults 0 0
sysfs /sys sysfs defaults 0 0
EOF
根文件系统的结构可以参考 Filesystem Hierarchy Standard,这是Linux基金会维护的文件系统层次结构标准,最新版本是3.0:https://refspecs.linuxfoundation.org/FHS_3.0/fhs-3.0.html。主要的几个文件:
etc/inittab,根文件系统启动后第一个执行的是 init 进程,这是 init 进程的核心配置文件,第一行定义了 init 首先要执行/etc/init.d/rcS,接下来要启动/bin/shshell。详细说明可以参考busybox源码的examples/inittab文件。etc/init.d/rcS,这是init 进程执行的第一个脚本,/bin/mount -a命令的作用是挂载/etc/fstab文件中定义的所有文件系统。etc/fstab,描述了默认的文件系统挂载配置。
用上面制作的根文件系统生成 ext4 格式的initial RAM disk (initrd) 镜像:
dd if=/dev/zero of=rootfs.ext4 bs=1M count=32
mkfs.ext4 rootfs.ext4
mkdir rootfs_tmp
sudo mount -o loop rootfs.ext4 rootfs_tmp
sudo cp -rf _install/* rootfs_tmp/
sudo umount rootfs_tmp
gzip --best -c rootfs.ext4 > rootfs.img.gz
4. 用Qemu虚拟机启动 #
至此,我们已经准备好了内核镜像 bzImage 和根文件系统镜像 rootfs.img.gz,可以使用Qemu启动我们的微型Linux系统了:
qemu-system-x86_64 \
-name kardel \
-smp 2 -m 1024 -nographic \
-kernel ./bzImage \
-initrd ./rootfs.img.gz \
-append "root=/dev/ram rw rootfstype=ext4 console=ttyS0 init=/sbin/init"
-name定义了虚拟机的名字-smp 2 -m 1024 -nographic是虚拟机的硬件配置,表示2个CPU核,1024M内存,没有图形界面,使用文本控制台。-kernel定义了使用的内核文件,这个配置会跳过BIOS/UEFI启动过程,直接启动内核。-initrd指定了初始RAM磁盘文件(init RAM Disk),initrd是在启动阶段被Linux内核调用的临时文件系统,用于根目录被挂载之前的准备工作,我们直接用它来研究Linux比较简单。-append定义了内核启动参数字符串:root=/dev/ram表示用/dev/ram作为挂载根文件系统的设备,内核将压缩的ext4镜像解压到RAM Disk设备console=ttyS0定义了控制台输出到串口0(配合-nographic使用)init=/sbin/init定义了init进程。传统的 initrd (Initial RAM Disk) 系统会默认执行/linuxrc作为临时初始化脚本进行过渡,执行完毕后,内核会尝试加载真正的rootfs并执行init进程,现在已经基本废弃这种方式。我们直接定义/sbin/init为系统启动后的第一个进程,内核会将/sbin/init启动为 PID 1,会一直运行,不会再进行根文件系统切换的特殊处理。
启动后的Qemu虚拟机:
[ 2.819865] devtmpfs: mounted
[ 2.903966] Freeing unused kernel image (initmem) memory: 1488K
[ 2.904265] Write protecting the kernel read-only data: 24576k
[ 2.907148] Freeing unused kernel image (text/rodata gap) memory: 2032K
[ 2.908811] Freeing unused kernel image (rodata/data gap) memory: 1292K
[ 2.909390] Run /sbin/init as init process
[ 3.046032] mount (79) used greatest stack depth: 14272 bytes left
[ 3.050250] rcS (78) used greatest stack depth: 14128 bytes left
Please press Enter to activate this console.
~ # poweroff
The system is going down NOW!
Sent SIGTERM to all processes
Sent SIGKILL to all processes
Requesting system poweroff
[ 305.271084] sh (80) used greatest stack depth: 13960 bytes left
[ 306.527946] ACPI: PM: Preparing to enter system sleep state S5
[ 306.534926] reboot: Power down
可以执行 poweroff 命令关机,如果要直接关闭 Qemu 虚拟机,可以按下组合键Ctrl+a,然后按 x 键。
启动后,可以查看文件系统挂载情况符合 /etc/fstab 的配置:
~ # df -a
Filesystem 1K-blocks Used Available Use% Mounted on
/dev/root 26596 2496 21812 10% /
devtmpfs 495408 0 495408 0% /dev
proc 0 0 0 0% /proc
tmpfs 498472 0 498472 0% /tmp
sysfs 0 0 0 0% /sys
但是,/dev 目录的自动挂载是Linux内核的内置机制,不需要在 /etc/fstab 中配置。还会发现,命令行参数配置的是root=/dev/ram,但是系统只有/dev/ram0设备,没有 /dev/ram 也没有 /dev/root:
~ # ls -l /dev/ram*
brw------- 1 0 0 1, 0 Sep 25 01:27 /dev/ram0
~ # ls /dev/root
ls: /dev/root: No such file or directory
这是因为内核历史问题和兼容性考虑,启动时,如果命令行参数中指定 root=/dev/ram ,内核会自动将其内部转换为 root=/dev/ram0 ,直接设置 root=/dev/ram0 也可以。而显示时,无论实际设备是什么,都会显示/dev/root,用于表示"当前的根文件系统设备"。内核解析过程:
用户指定: root=/dev/ram
↓
内核解析: /dev/ram → /dev/ram0 (主设备号1,次设备号0)
↓
内核挂载: 实际挂载 /dev/ram0 作为根文件系统
↓
显示名称: 在用户空间显示为 /dev/root
5. 文件共享 #
调试过程中,需要在主机和Qemu虚拟机之间进行文件传输,可选的方式很多,使用 9P virtio 较为方便,这是一种网络文件系统,可以在虚拟机里挂载主机的共享目录,实现文件共享。
首先要在内核配置中添加 9P virtio 支持:
./scripts/config --file ./build/.config --enable CONFIG_FUSE_FS
./scripts/config --file ./build/.config --enable CONFIG_VIRTIO_FS
./scripts/config --file ./build/.config --enable CONFIG_VIRTIO_PCI
./scripts/config --file ./build/.config --enable CONFIG_NET_9P
./scripts/config --file ./build/.config --enable CONFIG_NET_9P_VIRTIO
./scripts/config --file ./build/.config --enable CONFIG_9P_FS
./scripts/config --file ./build/.config --enable CONFIG_9P_FS_POSIX_ACL
启动 Qemu 虚拟机是添加相关参数:
-fsdev local,security_model=none,id=fsdev0,path=${HOST_SHARE_PATH} \
-device virtio-9p-pci,fsdev=fsdev0,mount_tag=hostshare
参数 path=${HOST_SHARE_PATH} 定义了主机的共享目录,可以自定义。这样启动系统后,可以将共享目录挂载到虚拟机的 /mnt 路径下:
~ # mount -t 9p -o trans=virtio,version=9p2000.L hostshare /mnt
~ # df
Filesystem 1K-blocks Used Available Use% Mounted on
/dev/root 27620 2736 22596 11% /
devtmpfs 495340 0 495340 0% /dev
tmpfs 498436 0 498436 0% /tmp
hostshare 1055762784 78147900 923911352 8% /mnt
6. 改用initramfs #
Linux内核支持的rootfs启动介质类似非常多,常用的包括:
- 传统块设备,例如硬盘
root=/dev/sda1,eMMC/SD卡root=/dev/mmcblk0p1。 - 网络文件系统,
root=/dev/nfs - RamDisk设备,
root=/dev/ram
我们用的RamDisk设备,属于传统的 initrd 镜像,需要块设备和文件系统(ext4)支持,制作方式比较复杂,且大小固定,需要分配固定大小的内存,使用时内核需要挂载整个块设备,启动流程也比较复杂:
内核启动 → 加载initrd到/dev/ram → 挂载/dev/ram → 执行/sbin/init → 读取/etc/inittab
从 Linux 2.6.13 开始,内核支持 initramfs 启动,它以ramfs技术为基础,rootfs存储在cpio格式的压缩包里,启动时直接解压到内存,没有文件系统开销,作为临时过渡的rootfs,启动过程简单直接,速度更快:
内核启动 → 解压initramfs到内存 → 直接执行/init
要使用initramfs,需要使能内核支持:
./scripts/config --file ./build/.config --enable CONFIG_INITRAMFS_SOURCE
./scripts/config --file ./build/.config --enable CONFIG_INITRAMFS_COMPRESSION_GZIP
# 可以关闭 RAMDisk 支持,不需要。
# ./scripts/config --file ./build/.config --disable CONFIG_BLK_DEV_RAM
make O=./build olddefconfig
然后做一个最简单的initramfs镜像:
# 新建一个HelloWord程序,编译为 init 可执行文件,因为内核对于initramfs会默认从/init启动
cat > hello.c << EOF
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
printf("Hello world!\n");
sleep(999999999);
}
EOF
gcc -static hello.c -o init
# 生成 cpio 格式的压缩包
echo init | cpio -o -H newc | gzip > test.cpio.gz
启动虚拟机:
qemu-system-x86_64 \
-name kardel \
-smp 2 -m 1024 -nographic \
-kernel ./bzImage \
-initrd ./test.cpio.gz \
-append "rw rootfstype=ramfs console=ttyS0"
因为内核默认使能了CONFIG_TMPFS,所以必须在命令行参数中定义rootfs的类型rootfstype=ramfs,否则会默认为tmpfs,导致无法启动。命令行参数没有设置 init,内核就会默认启动 /init 程序:
[ 2.375607] Freeing unused kernel image (initmem) memory: 1488K
[ 2.376463] Write protecting the kernel read-only data: 24576k
[ 2.380567] Freeing unused kernel image (text/rodata gap) memory: 2032K
[ 2.382223] Freeing unused kernel image (rodata/data gap) memory: 1292K
[ 2.382931] Run /init as init process
Hello world!
因为没有启动Shell,无法进行任何操作。下面用busybox制作一个initramfs镜像。
编译busybox的方式不变,还是在 _install/ 下制作基本的根文件系统,需要修改 etc/fstab 文件,加入devtmpfs挂载配置。这是因为initrd (Initial RAM Disk)是一个真正的文件系统镜像(通常是ext2/ext4/cramfs),内核会将其挂载为块设备,系统启动时,内核会自动创建基本的设备节点。而initramfs(Initial RAM File System)只是一个cpio 归档,直接解压到内存中运行,没有块设备概念,是纯内存文件系统,内核不会自动创建设备节点。
# 创建fstab文件
cat > etc/fstab << EOF
proc /proc proc defaults 0 0
tmpfs /tmp tmpfs defaults 0 0
sysfs /sys sysfs defaults 0 0
EOF
然后改变最后的打包方式:
cd _install/
find . | cpio -o -H newc | gzip > ../../rootfs.cpio.gz
启动虚拟机时,需要修改命令行参数。改用rdinit=指定 init 程序,这个选项专用指定initramfs/initrd中的 init 程序,而init=参数用于指定根文件系统挂载后的init程序,initramfs没有挂载的过程,会找不到程序:
qemu-system-x86_64 \
-name kardel \
-smp 2 -m 1024 -nographic \
-kernel ./bzImage \
-initrd ./rootfs.cpio.gz \
-append "rw rootfstype=ramfs console=ttyS0 rdinit=/sbin/init"
启动正常:
[ 2.539957] Run /sbin/init as init process
[ 2.608081] mount (76) used greatest stack depth: 14696 bytes left
[ 2.617947] rcS (75) used greatest stack depth: 14640 bytes left
Please press Enter to activate this console.
~ # df -a
Filesystem 1K-blocks Used Available Use% Mounted on
none 0 0 0 0% /
proc 0 0 0 0% /proc
tmpfs 498472 0 498472 0% /tmp
sysfs 0 0 0 0% /sys
devtmpfs 495428 0 495428 0% /dev
更多关于Linux启动方式的早期历史,可以参考initrd和LILO作者的文章:https://www.almesberger.net/cv/papers/ols2k-9.pdf
7. 启动u-boot #
假设已经生成了内核文件 bzImage 和初始化 RAMDisk 文件 rootfs.img.gz,下面介绍如何使用Qemu虚拟机启动 u-boot。
首先从 u-boot 的官网 https://u-boot.org/ 获取源码。我们下载了u-boot-v2024.04.tar.bz2,然后解压:
> tar xvf u-boot-v2024.04.tar.bz2
> cd u-boot-v2024.04
> ls
Kbuild Makefile board config.mk drivers fs post
Kconfig README boot configs dts include scripts
Licenses api cmd disk env lib test
MAINTAINERS arch common doc examples net tools
直接使用默认的适配Qemu-X86_64虚拟机的配置,然后编译:
make qemu-x86_64_defconfig
make
生成u-boot.rom文件用于Qemu启动,使用-bios选项指定u-boot文件:
> qemu-system-x86_64 -smp 2 -m 1024 -nographic -bios ./u-boot.rom
U-Boot SPL 2024.04 (Sep 26 2025 - 16:54:54 +0800)
Video: 1024x768x32
Trying to boot from SPI
Jumping to 64-bit U-Boot: Note many features are missing
U-Boot 2024.04 (Sep 26 2025 - 16:54:54 +0800)
CPU: QEMU Virtual CPU version 2.5+
DRAM: 1 GiB
Core: 20 devices, 13 uclasses, devicetree: separate
Loading Environment from nowhere... OK
Video: 1024x768x0
Model: QEMU x86 (I440FX)
Net: e1000: 52:54:00:12:34:56
eth0: e1000#0
Hit any key to stop autoboot: 0
此时已经可以启动u-boot并调试。如果要启动Linux系统,还要指定kernel和initrd文件:
> qemu-system-x86_64 -smp 2 -m 1024 -nographic -bios ./u-boot.rom -kernel ../bzImage -initrd ../rootfs.img.gz
启动后进入u-boot,查看变量:
=> env print
arch=x86
baudrate=115200
board=qemu-x86
board_name=qemu-x86
bootargs=root=/dev/sdb3 init=/sbin/init rootwait ro
bootcmd=bootflow scan -lb
bootdelay=2
bootp_arch=6
bootp_vci=PXEClient:Arch:00006:UNDI:003000
consoledev=ttyS0
dnsip=10.0.2.3
ethact=e1000#0
ethaddr=52:54:00:12:34:56
fdtcontroladdr=3ecf9df0
filesize=1497e7
gatewayip=10.0.2.2
hostname=x86
ipaddr=10.0.2.15
kernel_addr_r=0x1000000
loadaddr=0x02000000
netdev=eth0
netmask=255.255.255.0
pciconfighost=1
ramdisk_addr_r=0x4000000
ramdiskfile=initramfs.gz
rootpath=/opt/nfsroot
scriptaddr=0x7000000
serverip=10.0.2.2
soc=qemu
stderr=serial,vidconsole
stdin=serial,i8042-kbd,usbkbd
stdout=serial,vidconsole
vendor=emulation
Environment size: 680/262140 bytes
注意打印出的几个env变量:
- bootargs是传递给内核的命令行参数,用于内核启动rootfs。
- bootcmd是u-boot启动内核的命令。
- kernel_addr_r是u-boot加载内核文件的内存地址。
- ramdisk_addr_r是u-boot加载initrd文件的内存地址。
- filesize=1497e7是initrd文件的大小
先设置bootargs,内核启动时需要到的命令行参数:
=> setenv bootargs "root=/dev/ram rw rootfstype=ext4 console=ttyS0 init=/sbin/init"
=> env print bootargs
bootargs=root=/dev/ram rw rootfstype=ext4 console=ttyS0 init=/sbin/init
然后手动执行启动命令,先用qfw命令将kernle和initrd文件加载到内存,这是u-boot用于向QEMU虚拟机加载固件的接口:
=> qfw load ${kernel_addr_r} ${ramdisk_addr_r};
loading kernel to address 1000000 size a27980 initrd 4000000 size 1497e7
可以看到,qfw会自动计算并打印出加载的内存地址和大小,然后用zboot命令启动内核:
=> zboot ${kernel_addr_r} a27980 ${ramdisk_addr_r} 1497e7
Valid Boot Flag
Magic signature found
Linux kernel version 5.15.193 (lsc@Bob) #3 SMP Thu Sep 25 15:17:20 CST 2025
Building boot_params at 0x00090000
Loading bzImage at address 100000 (10647936 bytes)
Initial RAM disk at linear address 0x04000000, size 1349607 bytes
Kernel command line: "root=/dev/ram rw rootfstype=ext4 console=ttyS0 init=/sbin/init"
Kernel loaded at 00100000, setup_base=0000000000090000
Starting kernel ...
...
[ 3.194519] Run /sbin/init as init process
[ 3.320081] mount (80) used greatest stack depth: 14240 bytes left
[ 3.326269] rcS (79) used greatest stack depth: 13664 bytes left
Please press Enter to activate this console.
也可以在编译前配置bootargs和bootcmd两个变量,这样启动虚拟机后,u-boot会自动加载并启动内核:
> make menuconfig
Boot options --->
[*] Enable boot arguments
(root=/dev/ram rw rootfstype=ext4 console=ttyS0 init=/sbin/init) Boot arg
[*] Enable a default value for bootcmd
(qfw load ${kernel_addr_r} ${ramdisk_addr_r}; zboot ${kernel_addr_r} - ${ramdisk_addr_r} ${filesize})
> make
> qemu-system-x86_64 -smp 2 -m 1024 -nographic -bios ./u-boot.rom -kernel ../bzImage -initrd ../rootfs.img.gz
更多在x86平台使用u-boot的资料可以参考官方文档: