嵌入式开发离不开硬件设备:开发板、外设等,但如果只是想研究Linux内核的架构/工作模式,修改一些代码然后烧写到开发板中验证,这样未必有些复杂。然而qemu可以避免频繁在开发板上烧写版本,如果仅仅是内核方面的调试,qemu完全可以完美地胜任。仿真能解决以下痛点:
真实单板难以获取时,可以快速上板,无需轮候
源码级的GDB(这真是一个超级强大的功能,有了它,开发效率会直线上升)
快速单元测试、开发者测试
业务代码无需打桩(桩还是会有条件存在的,但是转移到了qemu侧)
使用qemu运行自己编译的Linux系统,并能够进行简单调试。本文不对qemu做过多分析,着重于如何快速搭建环境。
qemu可运行在多个平台上,如Linux、windows、mac等。通常嵌入式开发是基于开源Linux的,因此我们也基于Linux环境开展实验。
windows下安装VMware,VMware创建Ubuntu 20.04 LTS版本的虚拟机。Ubuntu版本下载:
https://ubuntu.com/download/desktop
在Ubuntu安装qemu软件
用qemu模拟运行arm64 Linux系统
qemu安装
qemu安装方式有两种:Linux软件包安装、源码编译安装。
软件包安装
Ubuntu的软件包安装:qemu软件包越来越大,因此被拆分为了多个软件包。不同软件包有不同的功能,比如qemu-system-ARCH提供全系统模拟(ARCH替换为arm/mips等架构名),qemu-utils提供了一些工具。
sudo apt install qemu-system-arm
查看版本号,如果版本号太老,考虑使用源码编译
lv@ubuntu:~$ qemu-system-aarch64 --version
qemu emulator version 4.2.1 (Debian 1:4.2-3ubuntu6.16)
qemu官网给出了安装步骤:
https://www.qemu.org/download/
qemu版本号会持续更新。make之后,编译的可执行二进制文件在
./build目录下
wget https://download.qemu.org/qemu-7.0.0-rc0.tar.xz
tar xvJf qemu-7.0.0-rc0.tar.xz
cd qemu-7.0.0-rc0
./configure
内核编译步骤
下载并解压kernel源码
下载最新的kernel源码,kernel官网:https://www.kernel.org
tar -xf linux-5.12xx.tar
安装编译工具
gcc交叉编译工具链安装
我们要在x86_64 Ubuntu系统下编译arm64镜像,因此需要交叉编译工具链。
sudo apt install gcc-aarch64-linux-gnu
初装的Ubuntu缺少很多编译工具,可不急于全部安装以下工具,如果make menuconfig在哪出错了,再安装对应软件包即可。
sudo apt-get install git fakeroot build-essential ncurses-dev xz-utils libssl-dev bc flex libelf-dev bison
配置环境变量
要在x86_64的宿主机编译arm64的镜像,需要arm64的gcc工具。每次重新打开shell都需要配置。
export ARCH=arm64
export CROSS_COMPILE=<path>/aarch64-linux-gnu-
查看环境变量是否设置成功:export
生成.config文件
在顶层目录生成arm64的默认配置.config,其实是把arch/arm64/configs/defconfig复制到kernel源码顶层目录。
make defconfig
在.config的基础上配置其他特性开关
make menuconfig
编译镜像,并且10个job运行,加快编译速度。生成的内核镜像在:arch/arm64/boot/Image
make -j 10
启动裸内核
qemu相关使用
可执行程序
在./build目录可以找到下qemu-system-aarch64
可执行文件,同时目录下面还有其他cpu架构的可执行程序,命名都是qemu-system-
作为前缀。
查看qemu帮助信息
qemu-system-aarch64 -h // 查看全部帮助信息
qemu-system-aarch64 -machine help //查看支持的machine
qemu-system-aarch64 -cpu help //查看machine支持的cpu类型
During emulation, press 'ctrl-a h' to get some help
启动裸内核
执行如下命令尝试启动内核,如果一切顺利,可看到Linux的启动log,但是大概率会运行到根文件系统初始化时挂死。哈哈,不过到这里证明我们成功一半了。
qemu-system-aarch64 -M virt -cpu cortex-a57 -m 1024 -nographic -kernel path-to-Image-file
-M 指定开发板
-m 指定内存ram大小,单位MB
-smp 指定core num
-nographic 指定不需要图形界面
-kernel 指定内核文件
-dtb 指定dtb文件
-hda 硬盘0,Use file as hard disk
-hdb 硬盘1
-append 指定启动参数command line
$ ./build/qemu-system-aarch64 -M virt -cpu cortex-a57 -m 1024 -nographic -kernel /home/xxxx/linux/linux-5.12.4/arch/arm64/boot/Image
[ 0.000000] Booting Linux on physical CPU 0x0000000000 [0x411fd070]
[ 0.000000] Linux version 5.12.4 (xxxx) (aarch64-linux-gnu-gcc (Ubuntu/Linaro 7.5.0-3ubuntu1~18.04) 7.5.0, GNU ld (GNU Binutils for Ubuntu) 2.30) #1 SMP PREEMPT Mon May 17 14:36:21 CST 2021
[ 0.000000] Machine model: linux,dummy-virt
[ 0.000000] efi: UEFI not found.
[ 1.080779] Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0)
[ 1.081468] CPU: 0 PID: 1 Comm: swapper/0 Not tainted 5.12.4 #1
[ 1.081886] Hardware name: linux,dummy-virt (DT)
上文提到裸内核在初始化文件时出错而停止运行,接下来制作一个根文件系统并传递给内核,启动一个完整的内核程序。
文件系统和内核是完全独立的两个部分,文件是用户与内核交互的主要工具。
根文件系统是内核启动时所mount的第一个文件系统,是加载其它文件系统的”根“。
一套linux体系,只有内核本身是不能工作的,必须要rootfs(etc目录下的配置文件、/bin /sbin等目录下的shell命令,还有/lib目录下的库文件等···)相配合才能工作。
文件系统配置
打开ext4支持
不一定非得是ext4,这里勾选ext4仅仅是因为我们要制作的文件系统是ext4格式
File systems
----> {*} The Extended 4 (ext4) filesystem
制作根文件系统
制作一个简易的根文件系统,该文件系统包含的功能极其简陋,仅为了验证qemu启动Linux内核后挂载根文件系统的过程。以后会进一步完善该文件系统。
下载编译busybox
从官网下载最新的busybox源码,https://busybox.net/downloads/
tar -xf busybox-1.29.3.tar.bz2
busybox的编译和Linux kernel类似都是通过kconfig管理
export ARCH=arm64
export CROSS_COMPILE=<path>/aarch64-linux-gnu-
make menuconfig
进入menuconfig后,配置为静态编译
setting
----> Build Options
----> [*] Build static binary (no shared libs)
编译完成之后,在busybox根目录下会有_install
目录,该目录是编译好的一些命令集合。
make install
创建文件系统EXT4格式
创建空文件
if= / of=,分别指定输入输出文件。/dev/zero是输出一直为零的设备。下面命令创建内容为空的文件rootfs.ext4。
dd if=/dev/zero of=rootfs.ext4 bs=1M count=32
将文件格式转为ext4文件系统
mkfs.ext4 rootfs.ext4
将busybox编译生成的_install目录下的文件全部拷贝到initrd。至此,简易版根文件系统就制作完成,该根文件系统只含有最基本的功能,一些其他功能在以后的操作中会进行添加。
mkdir mnt
sudo mount rootfs.ext4 mnt/
cd mnt
sudo cp -rf busybox-xxx/_install/* .
cd ../
sudo umount mnt
rm -rf mnt
非嵌入式启动
qemu-system-aarch64 -M virt -cpu cortex-a57 -m 1G -nographic -kernel path-to-Image-file -append "root=/dev/vda" -hda ~/rootfs.ext4
嵌入式启动方式
文件系统配置
内核打开initramfs支持
ramfs是将文件系统直接编译进内核镜像中。
General setup
----> [*] Initial RAM filesystem and RAM disk (initramfs/initrd) support
内核打开ramdisk支持
ramdisk是在内存中模拟磁盘,使用ramdisk比Initramfs灵活一些,不需要每次都去编译内核。不过在qemu中,都支持通过-initrd指定文件镜像。注意ramdisk大小一定要足够大。
Device Drivers
----> [*] Block devices
----> <*> RAM block device support
----> (65535) Default RAM disk size (kbytes)
制作initramfs
cp busybox/_install/ rootfs
cd rootfs
find . | cpio -o -H newc > rootfs.cpio
gzip -c rootfs1.cpio > rootfs.cpio.gz
initramfs启动
qemu-system-aarch64 -M virt -cpu cortex-a57 -m 1G -nographic -kernel path-to-Image-file -initrd ~/rootfs.cpio.gz -append "rdinit=/linuxrc"
ramdisk启动
qemu-system-aarch64 -M virt -cpu cortex-a57 -m 1G -nographic -kernel path-to-Image-file -append "root=/dev/ram0 ramdisk_size=65535" -initrd ~/rootfs.ext4
initramfs与initrd区别
Linux内核只认cpio格式的initramfs文件(因为unpack_to_rootfs只能解析cpio格式文件),非cpio格式的initramfs文件包将被系统抛弃,而initrd可以是cpio包也可以是传统的镜像文件,实际使用中initrd都是传统镜像文件如ext4格式。
使用initramfs,命令行参数将不需要"root="命令。
如下,kernel会首先尝试解析initramfs,然后尝试initrd(ramdisk)。
[ 0.548161] Trying to unpack rootfs image as initramfs...
[ 0.550507] rootfs image is not initramfs (invalid magic at start of compressed archive); looks like an initrd
ramdisk大小
如果RAM disk size这个大小和你做的ramdisk不匹配,则启动时仍然会出现 kernel panic内核恐慌,提示ramdisk格式不正确,挂载不上ramdisk。也可通过设置启动参数修改ramdisk大小。“ramdisk_size=65536”
Device Drivers
----> [*] Block devices
----> <*> RAM block device support
----> (65535) Default RAM disk size (kbytes)
挂载失败打印:
RAMDISK: ext2 filesystem found at block 0
RAMDISK: image too big! (32768KiB/4096KiB)
VFS: Cannot open root device "(null)" or unknown-block(0,0): error -6
串口打印, 不能打开tty文件
文件系统添加dev目录
sudo mkdir dev
找不到rcS文件
can't run '/etc/init.d/rcS': No such file or directory,
/etc/init.d/rcS allows you to run additional programs at boot time,
在文件系统中新建rcs解决此问题
sudo mkdir -p etc/init.d/
cd etc/init.d/
sudo chmod 777 rcS
kernel启动参数
不知怎的,最新版本的Linux没有这个文档了,看看之前版本的吧
https://elixir.bootlin.com/linux/v2.6.37.5/source/Documentation/kernel-parameters.txt
常用的主要有
initrd [BOOT] Specify the location of the initial ramdisk
loglevel
ramdisk_size
gdb调试
arm不能使用普通的gdb,要用gdb-multiarch
sudo apt install gdb-multiarch
确保编译的内核包含调试信息
Kernel hacking
----> Compile-time checks and compiler options
----> [*] compile the kernel with debug info
----> [*] Provide GDB scripts for kernel debugging
qemu命令需要添加以下选项
qemu-system-aarch64 -S -s
-S:表示qemu虚拟机会冻结CPU,直到远程的gdb输入相应控制命令
-s:表示在1234端口接受gdb的调试连接
在另一终端输入命令
gdb-multiarch --tui
(gdb) file vmlinux //加载kernel符号表
(gdb) target remote localhost:1234 //通过1234端口连接qemu平台
(gdb) b start_kernel //在内核start_kernel设置断点
(gdb) c //continue 运行
Linux kernel使用了大量的static函数,配合-O2编译选项,这些static函数被自动inline,这样可以达到优异的速度优化效果,但是也让gdb的时候带来一些小困扰。
这个问题似乎没有更好的解决办法,因为-O2是不能关闭的,因此一般都是在函数名前加上这样一个定义来临时规避这个问题:
__attribute__((optimize("O0")))
gdb调试,在start_kernel断点停不住
关闭kaslr
qemu-system-aarch64 -append nokaslr
启动打印时间戳
Kernel hacking
----> printk and dmesg options
----> [*] Show timing information on printk
earlyprintk
因为printk依赖于console,而console驱动此时还未加载,这个阶段printk的内存都被存储到log_buf中,待console驱动加载后才会一次性地打印出来。在console被初始化之前,earlyprintk将printk的所有内容直接输出到串口,console初始化完毕后,由console驱动接管printk的内容。打开earlyprintk有以下两种方式:
1、打开config 宏
2、增加启动参数:bootargs还要增加earlyprintk
buildroot
可借助buildroot,生成kernel、rootfs等
riscv linux启动
qemu启动命令和arm64一样,只不过需要编译出riscv的linux和busybox镜像。
https://risc-v-getting-started-guide.readthedocs.io/en/latest/linux-qemu.html
~/workspace/qemu/qemu-7.0.0-rc0/build/qemu-system-riscv64 \
-M virt \
-smp 1 \
-m 1024 \
-nographic \
-kernel ~/workspace/buildroot/buildroot-2022.02.3/output/images/Image \
-initrd ~/workspace/buildroot/buildroot-2022.02.3/output/images/rootfs.cpio \
-append "root=/dev/ram0 ramdisk_size=65535 nokaslr"