1. 安装 qemu
QEMU 用了很久了,之前一直没仔细想过它的全称是什么。这次要写文章,特意去查了一下,全称应该是 Quick Emulator。
QEMU(quick emulator)是由法布里斯·贝拉(Fabrice Bellard)等人编写的免费开源虚拟机(VMM),支持硬件虚拟化。
虚拟机大家应该都接触过。学 Linux 的时候,初学者一般都会先装一个虚拟机软件,比如 VMware 或 VirtualBox。不过这些虚拟机给我的感觉,更多是模拟 x86 架构的机器,也就是我们电脑上 Intel 芯片那套环境,而且都是带图形界面去启动一个完整系统。
而在嵌入式开发里,QEMU 能模拟很多嵌入式芯片和设备,支持的处理器架构和平台也相当丰富。
以 ubuntu 系统为例,安装 QEMU 可以通过下面两种方式:
-
使用 apt 命令安装
sudo apt-get install qemu-system-arm
-
从官网下载源码,自行编译安装
具体的安装步骤可以查看官网说明。
使用 apt 方式安装比较方便,但是一般该方式的对应版本比较低一点
使用源码安装可以自己选择任意版本,但是需要下载编译等,过程比较复杂
由于我们是要学习 arm64 架构的相关内容,所以需要保证安装了 arm64 架构支持的 QEMU。安装之后可以通过下面的命令验证安装结果:
qemu-system-aarch64 --version
QEMU emulator version 5.2.0
Copyright (c) 2003-2020 Fabrice Bellard and the QEMU Project developers
2. 编译 Linux 内核
2.1 下载内核源码
https://www.kernel.org/
这个网站可以下载到各个版本的内核源码。为了后续遇到问题容易查到资料,这里选一个 longterm 稳定版本 4.19.171。
从下面的链接下载一份完整的内核源码:
https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-4.19.171.tar.xz
下载完成之后解压源码到指定目录:
mkdir qemu_linux
cd ~/qemu_linux
tar -xvf ~/Downloads/linux-4.19.171.tar.xz
2.2 设置交叉编译工具链
我们是在 x86 平台下进行的开发,目标平台是 aarch64 架构,需要下载安装并配置一下交叉编译工具链。
工具链可以去 Arm 官网下载,需要下载 A 系列的工具链:
https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/gnu-a/downloads
x86_64 Linux hosted cross compilers
AArch64 GNU/Linux target (aarch64-none-linux-gnu)
下载 x86 平台 Linux 系统下的交叉编译工具,用于编译 Linux 内核的代码。
下载完成之后需要做一些步骤来完成交叉编译配置:
- 解压工具链到一个目录
- 设置 PATH 环境变量,将交叉编译工具链 bin 目录(该目录包含了工具链的可执行文件)加入到 PATH 环境变量
设置完成之后可以通过输入如下命令确认设置正确:
aarch64-none-linux-gnu-gcc -v
2.3 开始编译内核
内核源码和工具链就绪后,就可以开始编译了。第一次编译可能会提示缺少一些组件,根据报错信息自行搜索对应命令,安装依赖即可。
-
生成 arm64 平台默认配置文件
make ARCH=arm64 defconfig
-
开始编译源码
make ARCH=arm64
-
编译完成之后会生成一系列的文件,我们后面用到的主要在如下目录
arch/arm64/boot/
主要用到这里的 Image 文件,是内核编译生成的可执行文件。
3. 使用 qemu 模拟器执行启动内核
qemu-system-aarch64 -machine virt \
-cpu cortex-a53 \
-nographic \
-smp 1 \
-m 2048 \
-kernel arch/arm64/boot/Image
尝试执行上面的命令启动内核,真的启动起来了。
解释一下上面的参数含义:
-machine virt 指定 QEMU 模拟的设备,这里是指一个通用的 armv8 架构芯片。许多芯片厂商也尝试为自己的芯片和板卡做适配。
-cpu cortex-a53 指定具体的核心。A53 是早期一个经典核心,不同的核心可能在性能指标和部分配置上略有不同,不过对我们模拟来说差别不大。
-nographic 表示启动时不使用图形界面。
-smp 1 设置该设备只有 1 个核。
-m 2048 表示设置该设备有 2048M 内存。
-kernel arch/arm64/boot/Image 指定用于启动的内核文件。
启动画面如下,内核很顺利地跑起来了:
jhb@jhb-pc:~/qemu_linux$ ./boot_qemu.sh
[ 0.000000] Booting Linux on physical CPU 0x0000000000 [0x410fd034]
[ 0.000000] Linux version 4.19.171 (jhb@jhb-pc) (gcc version 10.3.1 20210621 (GNU Toolchain for the A-profile Architecture 10.3-2021.07 (arm-10.29))) #1 SMP PREEMPT Sun Jan 9 21:42:34 CST 2022
[ 0.000000] Machine model: linux,dummy-virt
[ 0.000000] efi: Getting EFI parameters from FDT:
[ 0.000000] efi: UEFI not found.
[ 0.000000] cma: Reserved 32 MiB at 0x00000000be000000
[ 0.000000] NUMA: No NUMA configuration found
[ 0.000000] NUMA: Faking a node at [mem 0x0000000040000000-0x00000000bfffffff]
[ 0.000000] NUMA: NODE_DATA [mem 0xbdfeaa40-0xbdfec1ff]
。。。。。。。
[ 0.951601] uart-pl011 9000000.pl011: no DMA platform data
[ 0.955718] VFS: Cannot open root device "(null)" or unknown-block(0,0): error -6
[ 0.956072] Please append a correct "root=" boot option; here are the available partitions:
[ 0.956682] Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0)
[ 0.957298] CPU: 0 PID: 1 Comm: swapper/0 Not tainted 4.19.171 #1
[ 0.957567] Hardware name: linux,dummy-virt (DT)
[ 0.957894] Call trace:
[ 0.958081] dump_backtrace+0x0/0x150
[ 0.958373] show_stack+0x14/0x20
[ 0.958606] dump_stack+0x98/0xc8
[ 0.958749] panic+0x12c/0x288
[ 0.958869] mount_block_root+0x1b4/0x26c
[ 0.959010] mount_root+0x11c/0x150
[ 0.959169] prepare_namespace+0x12c/0x17c
[ 0.959329] kernel_init_freeable+0x208/0x228
[ 0.959479] kernel_init+0x10/0x108
[ 0.959605] ret_from_fork+0x10/0x24
[ 0.960194] Kernel Offset: disabled
[ 0.960491] CPU features: 0x0,24002004
[ 0.960723] Memory Limit: none
[ 0.961271] ---[ end Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0) ]---
不过跑到最后出现了异常,原因是根文件系统还没挂载。我们需要先给它准备一个虚拟存储设备来存放文件系统,同时还要提供一个初始化进程交给内核去执行。
3.1 添加根文件系统支持
根文件系统本质上就是一个普通的文件系统,只不过常见发行版里内容比较复杂,需要准备大量文件。通常我们会用 BusyBox 或 Buildroot 来构建根文件系统,它们也附带了不少功能。不过今天我们自己手动制作一个极简版本。
- 建立一个本地的文件夹作为根文件系统目录,该目录我们可以存放为内核准备的文件
- 使用 mkinitramfs.sh 脚本将该目录转换成 ramfs 文件系统格式
- 使用 QEMU 启动内核,指定对应的根文件系统对应文件
3.1.1 配置内核选项 打开 ramfs 支持
make ARCH=arm64 menuconfig
Device Drivers
Block devices
BLK_DEV_RAM
按上面的菜单路径进入配置,选择使能 BLK_DEV_RAM 选项。
3.1.2 修改 qemu 启动参数 指定根文件系统的文件
内核支持 ramfs 设备后,我们通过给 QEMU 传递启动参数,把本机生成的文件作为 ramfs 文件系统交给内核。启动命令修改如下:
qemu-system-aarch64 -machine virt \
-cpu cortex-a53 \
-nographic \
-smp 1 \
-m 2048 \
-kernel linux-4.19.171/arch/arm64/boot/Image \
-append "root=/dev/ram0 rootfstype=ramfs rw init=/init" \
-initrd initramfs.cpio.gz
添加选项 -initrd initramfs.cpio.gz,表示指定使用这个文件作为 ramfs 的内容。
添加选项 -append "root=/dev/ram0 rootfstype=ramfs rw init=/init",表示传递给内核的参数。
root 表示根文件系统的设备为 /dev/ram0,除此之外还指定了文件系统的类型是 ramfs。
init 表示系统启动的第一个进程名称,内核会从文件系统中加载该进程去执行。
initramfs.cpio.gz 这个文件是使用脚本生成的根文件系统。
打包脚本(mkinitramfs.sh)内容:
#!/bin/sh
# Copyright 2006 Rob Landley <rob@landley.net> and TimeSys Corporation.
# Licensed under GPL version 2
if [ $# -ne 2 ]
then
echo "usage: mkinitramfs directory imagename.cpio.gz"
exit 1
fi
if [ -d "$1" ]
then
echo "creating $2 from $1"
(cd "$1"; find . | cpio -o -H newc | gzip) > "$2"
else
echo "First argument must be a directory"
exit 1
fi
打包命令:
./mkinitramfs.sh rootfs/ initramfs.cpio.gz

3.1.3 添加根文件系统后重新启动系统
有了文件系统之后再次尝试启动一下:
jhb@jhb-pc:~/qemu_linux$ ./mkinitramfs.sh rootfs/ initramfs.cpio.gz
creating initramfs.cpio.gz from rootfs/
1 块
jhb@jhb-pc:~/qemu_linux$ ./boot_qemu.sh
[ 0.000000] Booting Linux on physical CPU 0x0000000000 [0x410fd034]
[ 0.000000] Linux version 4.19.171 (jhb@jhb-pc) (gcc version 10.3.1 20210621 (GNU Toolchain for the A-profile Architecture 10.3-2021.07 (arm-10.29))) #3 SMP PREEMPT Mon Jan 10 00:05:42 CST 2022
。。。。
[ 0.974252] uart-pl011 9000000.pl011: no DMA platform data
[ 0.979358] VFS: Mounted root (ramfs filesystem) on device 0:15.
[ 0.979987] devtmpfs: error mounting -2
[ 1.000143] Freeing unused kernel memory: 1216K
[ 1.001108] Run /init as init process
[ 1.001660] Kernel panic - not syncing: Requested init /init failed (error -2).
[ 1.002203] CPU: 0 PID: 1 Comm: swapper/0 Not tainted 4.19.171 #3
[ 1.002601] Hardware name: linux,dummy-virt (DT)
[ 1.003037] Call trace:
[ 1.003178] dump_backtrace+0x0/0x150
[ 1.003507] show_stack+0x14/0x20
[ 1.003674] dump_stack+0x98/0xc8
[ 1.003799] panic+0x12c/0x288
[ 1.003914] kernel_init+0xa4/0x110
[ 1.004041] ret_from_fork+0x10/0x24
[ 1.004694] Kernel Offset: disabled
[ 1.005071] CPU features: 0x0,24002004
[ 1.005309] Memory Limit: none
[ 1.005855] ---[ end Kernel panic - not syncing: Requested init /init failed (error -2). ]---
QEMU: Terminated
可以看到其实已经成功挂载了文件系统,但是有错误提示:
[ 1.001108] Run /init as init process
[ 1.001660] Kernel panic - not syncing: Requested init /init failed (error -2).
3.1.4 实现一个 init 进程
执行初始化进程 init 失败了。这个 init 正是我们通过 init="/init" 参数传给内核的那个程序。
init 说白了就是一个普通进程,所以我们可以写一个最简单的 hello world 程序当作 init,放进根文件系统里试试能不能被正确执行。
init.c
#include <stdio.h>
int main()
{
printf("hello world!\n");
while(1);
return 0;
}
编译上面的程序:
aarch64-none-linux-gnu-gcc -o init init.c -static
把编译生成的 init 放入到根文件系统目录,重新生成根文件系统并启动内核:
./mkinitramfs.sh rootfs/ initramfs.cpio.gz
./boot_qemu.sh
可以看到已经按照我们的程序打印出了内容,系统成功启动:
[ 1.058241] Freeing unused kernel memory: 1216K
[ 1.059787] Run /init as init process
hello world!
到这里你会发现,Linux 从内核启动到运行应用程序的链路并没有想象中复杂。像 Ubuntu 这类发行版,不过是在初始化进程里做了大量工作,才最终呈现出图形界面。BusyBox 同样也是借助 Linux 提供的接口来实现各种命令。后续我们可以继续丰富这个 init,加入 shell 等基础功能和常用命令。