利用 qemu 模拟嵌入式系统制作全过程

2019-07-12 16:39发布

这篇文章,将介绍如何用qemu来搭建一个基于ARM的嵌入式linux系统。通过该文章,你可以学习到如何配置kernel,如何交叉编译kernel,如何配置busybox并编译,如何制作initramfs,如何制作根文件系统,如何定制自己的uboot,如何通过uboot向kernel传递参数等。开始干活!

1 环境搭建

在实现我们的目标之前,我们需要搭建自己的工作环境。在这里,假设你的主机上已经有 gcc 本地编译环境,并运行 Ubuntu 12.10 。但是这并不影响在其他的 linux 平台上进行,只要修改一下对应的命令就可以了。首先,我们需要下载一个 ARM 交叉工具链。你可以在网上下载源码自己编译,也可以下载已经编译好的工具链。在工具链中有基本的 ARM 编译工具,比如: gcc, gdb, addr2line, nm, objcopy, objdump 等。可能你会问,这些工具本机不是已经有了么?如果不出意外,我想你的主机应该是 x86 架构的。不同的架构,有不同的指令集,你不能拿一个 x86 的执行文件放到一个 ARM 机器上执行。所以我们需要一个能够在 x86 架构上生成 ARM 可执行程序的 GCC 编译器。有很多预先编译好的 ARM 工具链,这里使用的是 CodeSourcery 。更多关于 toolchain 的信息可以在 elinux.org 找到。下载下来后,直接解压,放到某个目录,然后配置一下 PATH 环境变量,这里是这样配置的:
  1. export PATH=~/arm-2013.05/bin:$PATH
配置完 ARM 交叉工具链后,我们需要下载一些源码,并安装一些软件。 命令如下:
  1. # install qemu
  2. sudo apt-get install qemu qemu-kvm qemu-kvm-extras qemu-user qemu-system
  3. # install mkimage tool
  4. sudo apt-get install uboot-mkimage
  5. # install git
  6. sudo apt-get install git
  7. # prepare related directory
  8. mkdir -pv ~/armsource/{kernel,uboot,ramfs,busybox}
  9. # download latest kernel stable code to kernel dir
  10. git clone http://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git ~/armsource/kernel
  11. # download latest u-boot code to uboot dir
  12. git clone git://git.denx.de/u-boot.git ~/armsource/uboot
  13. # download latest busybox code to busybox dir
  14. git clone git://busybox.net/busybox.git ~/armsource/busybox

2 配置 kernel

环境搭建完后,我们就正式进入主题了。现在我们需要配置 kernel 源码,编译,并用 qemu 运行我们自己编译的 kernel 。这样我们就能够对我们的 kernel 进行测试,并做出对应的修改。进入 kernel 源码目录,我们需要找最新的 kernel 稳定版本。在写这篇文章的时候,最新的稳定版本是 3.10.10 。我们可以通过 git 切换到 3.10.10 。由于我们编译的内核需要运行在 ARM 上,所以我们应该到 arch/arm/configs 下找到对应我们设备的 kernel 配置文件。但是我们没有实际意义上的设备,而是用 qemu 模拟的设备,所以我们应该选择 qemu 能够模拟的设备的配置文件。这里我们选择常用的 versatile_defconfig 。 对应的命令如下:
  1. cd ~/armsource/kernel
  2. # checkout a tag and create a branch
  3. git checkout v3.10.10 -b linux-3.10.10
  4. # create .config file
  5. make versatile_defconfig ARCH=arm
配置完了,我们就可以编译了。编译的时候,我们可以用多个线程来加速编译,具体用多少个就要看你主机的配置了。这里我们用 12 个线程编译,命令如下:
  1. make -j12 ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabi-
注意,如果交叉编译环境没有配置好,这个地方会提示找不到对应的 gcc 编译器。这里 -j12 是指定编译线程为 12 个, ARCH 是指定目标架构为 arm ,所用的交叉编译器 arm-none-linux-gnueabi- 。OK , kernel 已经编译好了,那么我们需要用 qemu 把它跑起来。关于 qemu 的具体使用,请看 qemu 的官方文档 ,这里直接给出命令:
  1. qemu-system-arm -M versatilepb -kernel arch/arm/boot/zImage -nographic
这里 -M 是指定模拟的具体设备型号, versatile 系列的 pb 版本, -kernel 指定的是对应的内核, -nographic 是把 qemu 输出直接导向到当前终端。好,命令成功执行了。但是,好像没有任何有效输出。我们通过 C-a x 来退出 qemu 。编译的 kernel 好像不怎么好使,配置文件肯定有问题。打开 .config 配置文件,发现传递给 kernel 的参数没有指定 console ,难怪没有输出。我们定位到 CMDLINE ,并加入 console 参数:
  1. CONFIG_CMDLINE="console=ttyAMA0 root=/dev/ram0"
保存 .config ,重新编译 kernel ,并用 qemu 加载。现在终于有输出了。如果不出意外, kernel 应该会停在找不到根文件系统,并跳出一个 panic 。为什么会找不到根文件系统?因为我们压根就没有给它传递过,当然找不到。那现在是不是应该制作我们自己的根文件系统了。先别急,为了让后面的路好走一点,我们这里还需对内核进行一些配置。首先,我们需要用 ARM EABI 去编译 kernel ,这样我们才能让 kernel 运行我们交叉编译的用户态程序,因为我们所有的程序都是用 gnueabi 的编译器编译的。具体可以看 wikipedia 相关页面 4 ,你也可以简单的理解为嵌入式的 ABI 。其次,我们需要把对 kernel module 的支持去掉,这样可以把相关的驱动都编译到一个文件里,方便我们之后的加载。当然,你可以使能 kernel 的 debug 选项,这样就可以调试内核了,并打印很多调试信息。这里就不再说了,如果感兴趣,可以看我之前写的关于 kernel 调试的文章 5 。总结起来,这一次我们对 .config 做了如下修改:
  1. # CONFIG_MODULES is not set
  2. CONFIG_AEABI=y
  3. CONFIG_OABI_COMPAT=y
  4. CONFIG_PRINTK_TIME=y
  5. CONFIG_EARLY_PRINTK=y
  6. CONFIG_CMDLINE="earlyprintk console=ttyAMA0 root=/dev/ram0"

3 通过 busybox 制作 initramfs 镜像

如果你注意到了之前传递给 kernel 的参数,你会发现有一个 root=/dev/ram0 的参数。没错,这就是给 kernel 指定的根文件系统, kernel 检查到这个参数的时候,会到指定的地方加载根文件系统,并执行其中的 init 程序。这样就不会出现刚才那种情况,找不到根文件系统了。我们的目标就是让 kernel 挂载我们的 ramfs 根文件系统,并且在执行 init 程序的时候,调用 busybox 中的一个 shell ,这样我们就有一个可用的 shell 来和系统进行交互了。整个 ramfs 中的核心就是一个 busybox 可执行文件。 busybox 就像是一把瑞士军刀,可以把很多 linux 下的命令 ( 比如: cp, rm, whoami 等 ) 全部集成到一个可执行文件。这为制作嵌入式根文件系统提供了很大的便利,开发者不用单独编译每一个要支持的命令,还不用考虑库的依赖关系。基本上每一个制作嵌入式系统的开发者的首选就是 busybox 。busybox 也是采用 Kconfig 来管理配置选项,所以配置和编译 busybox 和 kernel 没有多大区别。 busybox 很灵活,你可以自由取舍你想要支持的命令,并且还可以添加你自己写的程序。在编译 busybox 的时候,为了简单省事,我们这里采用静态编译,这样就不用为 busybox 准备其他 libc , ld 等依赖库了。具体命令如下:
  1. cd ~/armsource/busybox
  2. # using stable version 1.21
  3. git checkout origin/1_21_stable -b busybox-1.21
  4. # using default configure
  5. make defconfig ARCH=arm
  6. # compile busybox in static
  7. make menuconfig
  8. make -j12 ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabi-
编译完后,我们就得到一个 busybox 静态链接的文件。接下来,我们需要一个 init 程序。这个程序将是 kernel 执行的第一个用户态的程序,我们需要它来产生一个可交互的 shell 。在桌面级别的 linux 发行版本,使用的 init 程序一般是 System V init( 传统的 init) , upstart(ubuntu) , systemd(fedora) 等。 busybox 也带有一个 init 程序,但是我们想自己写一个。既然自己写,那有两种实现方式,用 C 和 libc 实现,或者写一个 shell 脚本。为了简单,这里选择后者,具体脚本如下:
  1. #!/bin/sh
  2. echo
  3. echo "###########################################################"