在QEMU中启动U-Boot和内核

作为Linux内核工程师,QEMU是日常工作中经常接触的一个虚拟化工具,通过其CPU以及外设的模拟能力,我们很容易搭建出一套用于测试或开发的虚拟环境。以前在使用时通常都是直接去引导内核,但其实QEMU并不只是能够引导内核,任何拥有硬件初始化和管理能力的二进制程序它都可以引导运行。今天我们就使用qemu引导uboot,并在uboot中启动内核。

为什么要使用U-Boot

既然QEMU本身就具备引导Linux内核的功能,我们为什么还要先引导U-Boot呢?我想主要有一下几点好处:

  • U-Boot能够提供更灵活的内核引导方式,它不仅可以从本地获取内核文件,还能通过网络获取
  • U-Boot可以提供更完整的调试环境,如果直接使用QEMU,我们就很难看到bootloader做了哪些事情
  • 对于嵌入式系统的学习,U-Boot也是重要的一环,因此了解其工作原理有时也很重要
  • 对于跨平台模拟,如Arm,当涉及到DTB加载和引导压缩内核镜像的时候,QEMU有时并不能正确处理

安装QEMU

QEMU在主流的发行版中都可以直接从源上下载安装。

  • Arch: pacman -S qemu
  • Debian/Ubuntu: apt-get install qemu
  • Fedora: dnf install @virtualization
  • Gentoo: emerge --ask app-emulation/qemu
  • RHEL/CentOS: yum install qemu-kvm
  • SUSE: zypper install qemu

当然也可以从源码编译安装,参考QEMU网站的Build instructions

QEMU为每一种Arch生成一个对应的可执行程序,我们主要关注以下两个程序:

  • qemu-system-arm: 模拟32位Arm CPU,如Arm9/Arm11、Cortex-A7/A9/A15

  • qemu-system-aarch64: 模拟64位Arm CPU,如Cortex-A53/A57

可以通过qemu-system-arm -machine help来查看支持哪些开发板的模拟。

这里我们使用 vexpress-a9 这款开发板。vexpress-a9 是 Arm 公司自己设计的一款 4 核 Cortex-A9 开发板,U-Boot、Linux Kernel 和 QEMU 对这款开发板都做了完整的支持。

安装交叉编译器

请参照这篇文章安装所需的交叉编译工具链:

《安装Arm交叉编译工具链》

编译U-Boot

下载源码

1
git clone https://gitlab.denx.de/u-boot/u-boot.git

编译

1
2
make vexpress_ca9x4_defconfig
make CROSS_COMPILE=arm-linux-gnueabihf- all

最终编译生成ELF格式的可执行文件 u-boot 和纯二进制文件u-boot.bin,其中 QEMU 可以启动的为ELF格式的可执行文件 u-boot。

编译文件系统

这里我们使用Buildroot来快速构建一个我们需要的文件系统。

下载源码

1
git clone git://git.buildroot.net/buildroot

配置

首先进入menuconfig:

1
make menuconfig
  1. Target options

    • Target Architecture: ARM (little endian)

      大部分 Arm 都是小端模式,所以选上little endian

    • Target Architecture Variant: cortex-A9

      这款开发板的 CPU 是 cortex-A9。

    • Enable VFP extension support: enable

    • Target ABI: EABIhf

      我们将使用Linaro GCC进行编译,Linaro的GCC默认都打开了hardfloat的支持,所以选上VFP extension和EABIhf

  2. Build options

    • Location to save buildroot config: ca9_mini_defconfig

      该选项是设置最后生成的配置文件的保存路径,buildroot可以针对不同的板子生成特定的defconfig文件,默认保存在configs目录下。自己修改各项配置后,执行make savedefconfig命令,就会生成新的defconfig文件。下次编译之前,可以直接执行make ca9_mini_defconfig命令来加载已有的配置。

    • Download dir

      该选项设置 buildroot 下载的各种第三方包的存储路径,默认在 dl 目录下。

  3. Toolchain

    • Toolchain type: External toolchain

    • Toolchain: Custom toolchain

      因为这里使用电脑上自己安装的toolchain,所以我们这里选External toolchainCustom toolchain

    • Toolchain path: /usr/local/toolchain/gcc-arm-10.2-2020.11-x86_64-arm-none-linux-gnueabihf

      然后在Toolchain path中填写toolchian在电脑上安装的位置。

    • Toolchain prefix: $(ARCH)-none-linux-gnueabihf

      另外要注意Toolchain prefix这个前缀别写错。

    • External toolchain gcc version: 10.x

      设置toolchain的版本。

    • External toolchain kernel headers series: 4.20.x

      设置用来编译这个toolchain的内核头文件的内核的版本。这个版本可以在toolchain里面的version.h这个文件查到,打开这个文件:

      1
      2
      3
      ${toolchain_location}/arm-linux-gnueabihf/libc/usr/include/linux/version.h

      #define LINUX_VERSION_CODE 267277

      267277对应的16进制为0x4140D,如果版本号为M.m.p,那么

      1
      2
      3
      M = ( LINUX_VERSION_CODE >> 16 ) & 0xFF /* 0x04 = 4 */
      m = ( LINUX_VERSION_CODE >> 8 ) & 0xFF /* 0x14 = 20 */
      p = ( LINUX_VERSION_CODE >> 0 ) & 0xFF /* 0x0D = 13 */

      所以我们设置为4.20.x。

    • External toolchain C library: glibc/eglibc

  4. System configuration

    • Run a getty (login prompt) after boot

      • TTY port: ttyAMA0

        vexpress_a9内核启动的控制台的名字叫做ttyAMA0。

  5. Filesystem images

    • cpio the root filesystem: enable

      • Compression method: lz4

      我们把编译的rootfs以initramfs的形式和Linux Kernel链接在一起,为了让根文件系统镜像尽量小,可以对文件系统采用lz4压缩。

编译

1
make

编译内核

下载源码

1
git clone https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git

配置

把前面buildroot编译的rootfs.cpio.lz4拷贝到Linux Kernel根目录下:

1
cp {buildroot_dir}/output/images/rootfs.cpio.lz4 ./

然后加载vexpress_a9这块开发板的默认配置,并进入menuconfig做进一步配置:

1
2
make ARCH=arm vexpress_defconfig
make ARCH=arm menuconfig
  1. General setup

    • Initial RAM filesystem and RAM disk (initramfs/initrd) support: enable
      • Initramfs source file(s): rootfs.cpio.lz4
  2. Kernel hacking

    • printk and dmesg options

      • Show timing information on printks: enable

        这样打印的内核 log 前面会附带有时间戳信息。

编译

1
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j8

启动QEMU

QEMU可以模拟sd卡等外设。我们就把编译好的固件放在一个模拟的sd卡上,让QEMU从这张模拟的sd卡上启动Linux系统。

  1. 制作sd卡镜像,并将它格式化成fat格式

    1
    2
    dd if=/dev/zero of=sd.img bs=4096 count=4096
    mkfs.vfat sd.img
  2. 把编译好的kernel zImage和DTB文件拷贝到sd.img中

    1
    2
    3
    4
    sudo mount sd.img /mnt/ -o loop,rw
    sudo cp arch/arm/boot/zImage /mnt/
    sudo cp arch/arm/boot/dts/vexpress-v2p-ca9.dtb /mnt/
    sudo umount /mnt
  3. 在QEMU中启动U-Boot

    1
    qemu-system-arm -M vexpress-a9 -m 512M -kernel ${u-boot_dir}/u-boot -nographic  -sd sd.img
  4. 从sd卡中加载Linux Kernel和DTB

    1
    2
    fatload mmc 0:0 0x62008000 zImage
    fatload mmc 0:0 0x64008000 vexpress-v2p-ca9.dtb

    这里面的0x62008000和0x64008000分别对应zImage和dtb文件在内存中的加载地址。我们可以在arch/arm/Makefile里面搜索textofs:

    1
    141 textofs-y       := 0x00008000

    这个textofs定义的就是Linux Kernel zImage执行地址对应的内存偏移地址,默认偏移为0x8000。

    在U-Boot命令行中输入bdinfo命令,可以查到这块开发板内存的起始地址:

    1
    2
    3
    4
    5
    => bdinfo
    boot_params = 0x60002000
    DRAM bank = 0x00000000
    -> start = 0x60000000
    -> size = 0x20000000

    可以看到这块开发板的内存其实地址为0x60000000,所以对应内核的起始地址为:0x62008000。DTB的加载地址没有特别的要求,一般注意和 Linux Kernel Image 避开,不要重叠即可。

  5. 通过bootz命令启动Linux Kernel

    1
    bootz 0x62008000 - 0x64008000