QEMU,搞嵌入式开发的一定不陌生,最近各大群里都讨论疯了,说它是Linux利器一点也不夸张。它是一款知名的而且开源的模拟器,能够模拟 Arm、MIPS、RISC-V 等各种 CPU 和开发板,以及 网卡、声卡、键盘、sdcard、emmc、u***等各种外设。
你可以把它当作一块召之即来的开发板,在上面运行 U-Boot、Linux Kernel、甚至 Ubuntu 等各种软件和操作系统。
有时候我们想体验一下 mainline 上最新的 U-Boot 或者 Linux Kernel,可是却发现手边没有合适的板子,或者手边的板子搭载的 U-Boot 和 Linux Kernel 版本都比较低,这时候 QEMU 可以帮你迅速实现这一愿望。
QEMU安装
我们安装的是 Arm 版本的 QEMU,如果直接在 Ubuntu 上用sudo apt install qemu-system-arm命令安装的话,得到的 QEMU 版本比较旧,最好直接通过源码去编译。
我在 Ubuntu 18.04 系统上发现系统默认安装的 QEMU 在图形模式(不带 -nographic 参数)下无法启动。

参考上面步骤编译成功后,得到:qemu-system-arm和qemu-system-aarch64。
前者用来模拟 32 位的 Arm cpu,比如 Arm9 /Arm11、 Cortex-A7/A9/A15 。
后者用来模拟 64 位的 Arm cpu,比如 Arm Cortex A53,A57。
可以用qemu-system-arm -machine help命令来查看所支持的开发板:

是不是有很多熟悉的开发板都在里面,i.MX、EXYNOS 这些知名的芯片都有包含。
这里我们使用 vexpress-a9 这款开发板。vexpress-a9 是 Arm 公司自己设计的一款 4 核 Cortex-A9 开发板,U-Boot、Linux Kernel 和 QEMU 对这款开发板都做了完整的支持。
编译U-Boot
第一步:U-Boot 代码下载:
git clone https://gitlab.denx.de/u-boot/u-boot.git 下载完后,可以看到 configs 目录下有针对这款开发板的配置文件:
vexpress_ca9x4_defconfig

第二步:编译
make vexpress_ca9x4_defconfig make CROSS_COMPILE=arm-linux-gnueabihf- all 最终编译生成 elf 格式的可执行文件 u-boot 和纯二进制文件u-boot.bin,其中 QEMU 可以启动的为 elf 格式的可执行文件 u-boot。

编译 Buildroot
启动一个 Arm Linux 系统,一般都要必须的三件套:Bootloader、Linux Kernel、rootfs(根文件系统)。
在很久以前,制作 rootfs 是一件很麻烦的事情:交叉编译 busybox,然后手动建立标准的 Linux 系统目录,再把编译 busybox 生成的各种文件和库拷贝过来。如果还需要其他的模块,再交叉编译。如果交叉编译的某个模块依赖其他的库,还想要办法解决这个依赖关系。最后还要手动建立设备节点,设置对应的权限。一步一步做下去,任意一个环节都不能出错。否则启动的时候不知道会遇到什么莫名其妙的问题。
Buildroot 项目出现之后,如同它的 Slogan:MakingEmbedded Linux Easy,构建 rootfs 就变得轻松了许多,用一个群友的话说:
“自从用了buildroot,我就告别了刀耕火种的野蛮生活。那种文件系统自己定制,需要任何工具都要自己下源码交叉编译然后被各种库问题搞的焦头烂额的时代一去不复返了。”
◆ 代码下载:
git clone git://git.buildroot.net/buildroot Buildroot 代码仓库默认只包含一个编译框架,所以代码量很小,下载起来很快。真正构建 rootfs需要的各种代码包是根据你的配置选项,在编译的时候才开始下载的。
◆ 配置:
Buildroot 提供了和 U-Boot、Linux Kernel 等主流开源项目一样的 menucoinfig 配置接口,可以通过make help来查询所支持的各种命令:

开发者只要执行make menuconfig命令,就能通过这个熟悉的界面去选择自己需要的各种组件,定制自己的 rootfs:
首先要配置的是 Target options 选项:

大部分 Arm 都是小端模式,所以选上 little endian 。
这款开发板的 CPU 是 cortex-A9。我们将使用 Linaro GCC 进行编译,Linaro 的 GCC 默认都打开了 hardfloat 的支持,所以选上 VFP extension 和 EABIhf。
交叉编译工具 Linaro GCC 的安装可以参考前面的文章:一次搞定 Arm Linux 交叉编译。
Build options选项:

第一个选项是设置最后生成的配置文件的保存路径,buildroot 可以针对不同的板子生成特定的 defconfig 文件,默认保存在 configs 目录下。
自己修改各项配置后,执行make savedefconfig命令,就会生成新的 defconfig 文件:

下次编译之前,可以直接执行make ca9_mini_defconfig命令来加载已有的配置。
第二个选项设置 buildroot 下载的各种第三方包的存储路径,默认在 dl 目录下:

设置 Toolchain

因为这里使用电脑上自己安装的 toolchain,所以我们这里选 External toolchain 和 Custom toolchain
然后在 Toolchain path 中填写 toolchian 在电脑上安装的位置,如果不知道具体位置,用which命令查看:

另外要注意 Toolchain prefix 这个前缀别写错。
设置 toolchain 的版本和用来编译这个 toolchain 的内核头文件的内核的版本:
Toolchain 的版本我们根据 Toolchain 的名字或者通过arm-linux-gnueabihf-gcc -v 命令就可以查到。
编译 Toolchain 的内核头文件对应的内核的版本是什么呢?
我们在这个选项上敲 h 键,会看到下面的帮助选项:

原来这个版本可以在 toolchain 里面的 version.h 这个文件查到:
打开这个文件:
arm-linux-gnueabihf/libc/usr/include/linux/version.h

263680 对应的十六进制为 0x40600,右移 16 位,得到的版本号为 4。这就是上面 4.0.x 的由来。
这里也教给了大家一个小窍门:当我们在 make menuconfig 做配置的时候,如果遇到了看不懂的选项,直接在这个选项上敲 h 键,会看到一些有用的帮助信息,对这个选项做进一步解释。
因为 vexpress_a9 内核启动的控制台的名字叫做 ttyAMA0,所以我们还要在 :System configuration-》Run a getty(login prompt) after boot选项中配置 TTY Port 为 ttyAMA0。否则文件系统挂载后无法进入控制台。

我们把编译的 rootfs 以 initramfs 的形式和 Linux Kernel 链接在一起,为了让根文件系统镜像尽量小,可以对文件系统采用 lz4 压缩,所以 Filesystem images 还要做如下配置:

到这里一个最精简的 buildroot 已经配置完成。
如果还需要其他的命令或者工具,可以在 Target Packages 下面开启:

如果你需要的某个模块,buildroot 里面没有,还可以自己添加:

比如加入上面这个补丁,就可以让 buildroot 在编译的时候自动下载 https://github.com/rockchip-linux/io.git 并编译。
退出执行make命令开始编译。
Buildroot 编译的过程中会自动通过网络下载需要的各种包,所以要保证网络畅通。
编译完成后输入信息如下图:

编译Linux Kernel
1)代码下载
git clone https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git Linux mainline 已经有了对 vexpress_a9 这块板子的支持:

这里面的 ca 就是 Cortex-a 的简写,所以 ca9 就是 Cortex-A9,ca15 就是 Cortex-A15.
2)配置
把前面 buildroot 编译的 rootfs.cpio.lz4 拷贝到 linux kernel 根目录下:
cp.。/buildroot/output/images/rootfs.cpio.lz4 。 make ARCH=arm vexpress_defconfig 执行make ARCH=arm menuconfig命令,我们修改一些基本的配置:

在 General setup-》Initramfs source file处填写 rootfs.cpio.lz4, 就是我们前面从 Buildroot 拷贝过来的 rootfs,这里面我们把它和内核编译在一起,当然,rootfs也可以单独作为一个文件,放在独立的分区去加载,这种方式我们可以留在以后去尝试。

在 Kernel hacking-》printk and dmesg options选项中选中第一项,这样打印的内核 log 前面会附带有时间戳信息,比较好看。
退出,保存,然后编译:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j8

编译完成。
如果在编译 Linux kernel 或者 u-boot 的过程中,遇到什么错误,可以参考我之前的文章:LinuxKernel 和 U-Boot 编译的那些事。
启动QEMU
前面说了,QEMU 可以模拟 sd 卡等外设。我们就把编译好的固件放在一个模块的 sdcard 上,让 QEMU 从这张模拟的 sd 卡上启动 Linux 系统:
◆ 制作 sd 卡镜像,并将它格式化成 fat 格式:
dd if=/dev/zero of=sd.img bs=4096 count=4096 mkfs.vfat sd.img ◆ 把编译好的 kernel zImage 和 dtb 文件拷贝到 sd.img 中
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 ◆ 启动 QEMU
sudo qemu-system-arm -M vexpress-a9 -m 512M -kernel 。。/uboot-imx/u-boot-nographic -sd sd.img

-M 参数指定启动的板子为 vexpress-a9。
-m 指定这块板子的内存为 512MB。
-kernel 指定 QEMU 启动时首先执行的程序,我们这里指定为前面编译好的 u-boot 可以执行文件。
-sd 参数指定前面制作的 sd.img。
因为我们这里是从命令行启动,所以加了 nographic 参数。
可以看到 u-boot 已经顺利启动并进入命令,下面我们来启动 Linux Kernel。
◆ 首先通过 fatload 命令把 sd.img 里面的 zImage 和 dtb 文件读到开发板的内存中:
fatload mmc 0:0 0x62008000 zImage fatload mmc 0:0 0x64008000 vexpress-v2p-ca9.dtb 这里面的 0x62008000 和 0x64008000 分别对应 zImage 和 dtb 文件在内存中的加载地址,这两个地址是怎么来的呢:
在 Linux Kernel 中,有如下定义:

这个 textofs 定义的就是 Linux kernel zImage 执行地址对应的内存偏移地址,默认偏移为 0x8000。
在 u-boot 命令行中输入bdinfo命令,可以查到这块开发板内存的起始地址:

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

启动到最后输入root就可以进入命令行控制台。
到这里,我们就顺利的在 QEMU 上把 Arm linux 运行起来了。
QEMU 能否完全替代开发板?
QEMU 是一个模拟器,它和真正的开发板还是有一定的区别:
它无法完全模拟真实的硬件行为,也很难模拟一个 gpio 让你去拉高拉低或去点一个 led 灯,又或者去模拟 dram,一个 LCD 接口,让你去接一个显示屏…。看起来它不能模拟的东西太多了,那它到底有什么用呢?
它的强项是模拟实现那些不涉及具体硬件外设的场景,比如:
你想快速体验一下最新的 u-boot 和 linux kernel,它拿过来就能跑。
你想在 Arm 上运行 Ubuntu、Debian 这些时髦的 Linux 发行版,用它就行。
你想研究u-boot 或者 linux kernel 的启动流程,它也很合适。我有时候在具体的开发板上移植 Linux 内核的时候,发现某个流程跑的很异常,我又不确定正常的流程是什么样的时候,我就直接拿 QEMU 来跑一下做对比。
你想了解文件系统的挂载过程,它很合适。
你想学习 Arm 汇编,完全可以在 QEMU 上跑,配合 GDB 就能单步调试啦。
你想学习 Linux 设备树,它可以拿来做实验…。
再或者你想编译某个开源的项目在 Arm 开发板上运行,而交叉编译比较困难,就可以考虑直接在 QEMU 上运行一个 Arm Ubuntu 来编译,也许会简单很多。
QEMU,搞嵌入式开发的一定不陌生,最近各大群里都讨论疯了,说它是Linux利器一点也不夸张。它是一款知名的而且开源的模拟器,能够模拟 Arm、MIPS、RISC-V 等各种 CPU 和开发板,以及 网卡、声卡、键盘、sdcard、emmc、u***等各种外设。
你可以把它当作一块召之即来的开发板,在上面运行 U-Boot、Linux Kernel、甚至 Ubuntu 等各种软件和操作系统。
有时候我们想体验一下 mainline 上最新的 U-Boot 或者 Linux Kernel,可是却发现手边没有合适的板子,或者手边的板子搭载的 U-Boot 和 Linux Kernel 版本都比较低,这时候 QEMU 可以帮你迅速实现这一愿望。
QEMU安装
我们安装的是 Arm 版本的 QEMU,如果直接在 Ubuntu 上用sudo apt install qemu-system-arm命令安装的话,得到的 QEMU 版本比较旧,最好直接通过源码去编译。
我在 Ubuntu 18.04 系统上发现系统默认安装的 QEMU 在图形模式(不带 -nographic 参数)下无法启动。

参考上面步骤编译成功后,得到:qemu-system-arm和qemu-system-aarch64。
前者用来模拟 32 位的 Arm cpu,比如 Arm9 /Arm11、 Cortex-A7/A9/A15 。
后者用来模拟 64 位的 Arm cpu,比如 Arm Cortex A53,A57。
可以用qemu-system-arm -machine help命令来查看所支持的开发板:

是不是有很多熟悉的开发板都在里面,i.MX、EXYNOS 这些知名的芯片都有包含。
这里我们使用 vexpress-a9 这款开发板。vexpress-a9 是 Arm 公司自己设计的一款 4 核 Cortex-A9 开发板,U-Boot、Linux Kernel 和 QEMU 对这款开发板都做了完整的支持。
编译U-Boot
第一步:U-Boot 代码下载:
git clone https://gitlab.denx.de/u-boot/u-boot.git 下载完后,可以看到 configs 目录下有针对这款开发板的配置文件:
vexpress_ca9x4_defconfig

第二步:编译
make vexpress_ca9x4_defconfig make CROSS_COMPILE=arm-linux-gnueabihf- all 最终编译生成 elf 格式的可执行文件 u-boot 和纯二进制文件u-boot.bin,其中 QEMU 可以启动的为 elf 格式的可执行文件 u-boot。

编译 Buildroot
启动一个 Arm Linux 系统,一般都要必须的三件套:Bootloader、Linux Kernel、rootfs(根文件系统)。
在很久以前,制作 rootfs 是一件很麻烦的事情:交叉编译 busybox,然后手动建立标准的 Linux 系统目录,再把编译 busybox 生成的各种文件和库拷贝过来。如果还需要其他的模块,再交叉编译。如果交叉编译的某个模块依赖其他的库,还想要办法解决这个依赖关系。最后还要手动建立设备节点,设置对应的权限。一步一步做下去,任意一个环节都不能出错。否则启动的时候不知道会遇到什么莫名其妙的问题。
Buildroot 项目出现之后,如同它的 Slogan:MakingEmbedded Linux Easy,构建 rootfs 就变得轻松了许多,用一个群友的话说:
“自从用了buildroot,我就告别了刀耕火种的野蛮生活。那种文件系统自己定制,需要任何工具都要自己下源码交叉编译然后被各种库问题搞的焦头烂额的时代一去不复返了。”
◆ 代码下载:
git clone git://git.buildroot.net/buildroot Buildroot 代码仓库默认只包含一个编译框架,所以代码量很小,下载起来很快。真正构建 rootfs需要的各种代码包是根据你的配置选项,在编译的时候才开始下载的。
◆ 配置:
Buildroot 提供了和 U-Boot、Linux Kernel 等主流开源项目一样的 menucoinfig 配置接口,可以通过make help来查询所支持的各种命令:

开发者只要执行make menuconfig命令,就能通过这个熟悉的界面去选择自己需要的各种组件,定制自己的 rootfs:
首先要配置的是 Target options 选项:

大部分 Arm 都是小端模式,所以选上 little endian 。
这款开发板的 CPU 是 cortex-A9。我们将使用 Linaro GCC 进行编译,Linaro 的 GCC 默认都打开了 hardfloat 的支持,所以选上 VFP extension 和 EABIhf。
交叉编译工具 Linaro GCC 的安装可以参考前面的文章:一次搞定 Arm Linux 交叉编译。
Build options选项:

第一个选项是设置最后生成的配置文件的保存路径,buildroot 可以针对不同的板子生成特定的 defconfig 文件,默认保存在 configs 目录下。
自己修改各项配置后,执行make savedefconfig命令,就会生成新的 defconfig 文件:

下次编译之前,可以直接执行make ca9_mini_defconfig命令来加载已有的配置。
第二个选项设置 buildroot 下载的各种第三方包的存储路径,默认在 dl 目录下:

设置 Toolchain

因为这里使用电脑上自己安装的 toolchain,所以我们这里选 External toolchain 和 Custom toolchain
然后在 Toolchain path 中填写 toolchian 在电脑上安装的位置,如果不知道具体位置,用which命令查看:

另外要注意 Toolchain prefix 这个前缀别写错。
设置 toolchain 的版本和用来编译这个 toolchain 的内核头文件的内核的版本:
Toolchain 的版本我们根据 Toolchain 的名字或者通过arm-linux-gnueabihf-gcc -v 命令就可以查到。
编译 Toolchain 的内核头文件对应的内核的版本是什么呢?
我们在这个选项上敲 h 键,会看到下面的帮助选项:

原来这个版本可以在 toolchain 里面的 version.h 这个文件查到:
打开这个文件:
arm-linux-gnueabihf/libc/usr/include/linux/version.h

263680 对应的十六进制为 0x40600,右移 16 位,得到的版本号为 4。这就是上面 4.0.x 的由来。
这里也教给了大家一个小窍门:当我们在 make menuconfig 做配置的时候,如果遇到了看不懂的选项,直接在这个选项上敲 h 键,会看到一些有用的帮助信息,对这个选项做进一步解释。
因为 vexpress_a9 内核启动的控制台的名字叫做 ttyAMA0,所以我们还要在 :System configuration-》Run a getty(login prompt) after boot选项中配置 TTY Port 为 ttyAMA0。否则文件系统挂载后无法进入控制台。

我们把编译的 rootfs 以 initramfs 的形式和 Linux Kernel 链接在一起,为了让根文件系统镜像尽量小,可以对文件系统采用 lz4 压缩,所以 Filesystem images 还要做如下配置:

到这里一个最精简的 buildroot 已经配置完成。
如果还需要其他的命令或者工具,可以在 Target Packages 下面开启:

如果你需要的某个模块,buildroot 里面没有,还可以自己添加:

比如加入上面这个补丁,就可以让 buildroot 在编译的时候自动下载 https://github.com/rockchip-linux/io.git 并编译。
退出执行make命令开始编译。
Buildroot 编译的过程中会自动通过网络下载需要的各种包,所以要保证网络畅通。
编译完成后输入信息如下图:

编译Linux Kernel
1)代码下载
git clone https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git Linux mainline 已经有了对 vexpress_a9 这块板子的支持:

这里面的 ca 就是 Cortex-a 的简写,所以 ca9 就是 Cortex-A9,ca15 就是 Cortex-A15.
2)配置
把前面 buildroot 编译的 rootfs.cpio.lz4 拷贝到 linux kernel 根目录下:
cp.。/buildroot/output/images/rootfs.cpio.lz4 。 make ARCH=arm vexpress_defconfig 执行make ARCH=arm menuconfig命令,我们修改一些基本的配置:

在 General setup-》Initramfs source file处填写 rootfs.cpio.lz4, 就是我们前面从 Buildroot 拷贝过来的 rootfs,这里面我们把它和内核编译在一起,当然,rootfs也可以单独作为一个文件,放在独立的分区去加载,这种方式我们可以留在以后去尝试。

在 Kernel hacking-》printk and dmesg options选项中选中第一项,这样打印的内核 log 前面会附带有时间戳信息,比较好看。
退出,保存,然后编译:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j8

编译完成。
如果在编译 Linux kernel 或者 u-boot 的过程中,遇到什么错误,可以参考我之前的文章:LinuxKernel 和 U-Boot 编译的那些事。
启动QEMU
前面说了,QEMU 可以模拟 sd 卡等外设。我们就把编译好的固件放在一个模块的 sdcard 上,让 QEMU 从这张模拟的 sd 卡上启动 Linux 系统:
◆ 制作 sd 卡镜像,并将它格式化成 fat 格式:
dd if=/dev/zero of=sd.img bs=4096 count=4096 mkfs.vfat sd.img ◆ 把编译好的 kernel zImage 和 dtb 文件拷贝到 sd.img 中
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 ◆ 启动 QEMU
sudo qemu-system-arm -M vexpress-a9 -m 512M -kernel 。。/uboot-imx/u-boot-nographic -sd sd.img

-M 参数指定启动的板子为 vexpress-a9。
-m 指定这块板子的内存为 512MB。
-kernel 指定 QEMU 启动时首先执行的程序,我们这里指定为前面编译好的 u-boot 可以执行文件。
-sd 参数指定前面制作的 sd.img。
因为我们这里是从命令行启动,所以加了 nographic 参数。
可以看到 u-boot 已经顺利启动并进入命令,下面我们来启动 Linux Kernel。
◆ 首先通过 fatload 命令把 sd.img 里面的 zImage 和 dtb 文件读到开发板的内存中:
fatload mmc 0:0 0x62008000 zImage fatload mmc 0:0 0x64008000 vexpress-v2p-ca9.dtb 这里面的 0x62008000 和 0x64008000 分别对应 zImage 和 dtb 文件在内存中的加载地址,这两个地址是怎么来的呢:
在 Linux Kernel 中,有如下定义:

这个 textofs 定义的就是 Linux kernel zImage 执行地址对应的内存偏移地址,默认偏移为 0x8000。
在 u-boot 命令行中输入bdinfo命令,可以查到这块开发板内存的起始地址:

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

启动到最后输入root就可以进入命令行控制台。
到这里,我们就顺利的在 QEMU 上把 Arm linux 运行起来了。
QEMU 能否完全替代开发板?
QEMU 是一个模拟器,它和真正的开发板还是有一定的区别:
它无法完全模拟真实的硬件行为,也很难模拟一个 gpio 让你去拉高拉低或去点一个 led 灯,又或者去模拟 dram,一个 LCD 接口,让你去接一个显示屏…。看起来它不能模拟的东西太多了,那它到底有什么用呢?
它的强项是模拟实现那些不涉及具体硬件外设的场景,比如:
你想快速体验一下最新的 u-boot 和 linux kernel,它拿过来就能跑。
你想在 Arm 上运行 Ubuntu、Debian 这些时髦的 Linux 发行版,用它就行。
你想研究u-boot 或者 linux kernel 的启动流程,它也很合适。我有时候在具体的开发板上移植 Linux 内核的时候,发现某个流程跑的很异常,我又不确定正常的流程是什么样的时候,我就直接拿 QEMU 来跑一下做对比。
你想了解文件系统的挂载过程,它很合适。
你想学习 Arm 汇编,完全可以在 QEMU 上跑,配合 GDB 就能单步调试啦。
你想学习 Linux 设备树,它可以拿来做实验…。
再或者你想编译某个开源的项目在 Arm 开发板上运行,而交叉编译比较困难,就可以考虑直接在 QEMU 上运行一个 Arm Ubuntu 来编译,也许会简单很多。
举报