QEMU搭建Linux实验环境


嵌入式开发离不开硬件设备比如:开发板、外设等,但是如果只是想研究Linux内核,想学习Linux内核的架构/工作模式,修改一些代码,重新编译并烧写到开发板中进行验证,这样未必有些复杂。然而qemu的使用可以避免频繁在开发板上烧写版本,如果进行与外设无关,仅仅是内核方面的调试,qemu模拟ARM开发环境完全可以完美地胜任。仿真能解决以下痛点:

  • 真实单板难以获取时,可以快速上板,无需轮候
  • 源码级的GDB(这真是一个超级强大的功能,有了它,开发效率会直线上升)
  • 快速单元测试、开发者测试
  • 业务代码无需打桩(桩还是会有条件存在的,但是转移到了qemu侧)

目标

使用qemu运行自己编译的Linux系统,并能够进行简单调试。本文不对qemu做过多分析,着重于如何快速搭建环境。

环境准备

qemu可运行在多个平台上,如Linux、windows、mac等。通常嵌入式开发是基于开源Linux的,因此我们也基于Linux环境开展实验。

  1. windows下安装VMware,VMware创建Ubuntu 20.04 LTS版本的虚拟机。Ubuntu版本下载:https://ubuntu.com/download/desktop
  2. 在Ubuntu安装qemu软件
  3. 用qemu模拟运行arm64 Linux系统

qemu安装

qemu安装方式有两种:Linux软件包安装、源码编译安装。

软件包安装

Ubuntu的软件包安装:qemu软件包越来越大,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)
Copyright (c) 2003-2019 Fabrice Bellard and the qemu Project developers

源码安装

qemu官网给出了下载安装步骤:https://www.qemu.org/download/
qemu版本号会持续更新。make之后,编译的二进制文件在./build目录下

wget https://download.qemu.org/qemu-6.0.0.tar.xz
tar xvJf qemu-6.0.0.tar.xz
cd qemu-6.0.0
./configure
make

gcc交叉编译工具链安装

我们要在x86_64 Ubuntu系统下编译arm64镜像,因此需要交叉编译工具链。
sudo apt install gcc-aarch64-linux-gn

其他工具

初装的Ubuntu缺少很多编译工具,可不急于全部安装以下工具,如果make menuconfig在哪出错了,再安装对应软件包即可。

make: sudo apt install make
gcc: sudo apt install gcc
ncurses: sudo apt install libncurses-dev
flex: sudo apt install flex
bison: sudo apt install bison
openssl : sudo apt install libssl-dev

内核编译

下载并解压kernel源码

下载最新的kernel源码,kernel官网:https://www.kernel.org

tar -xf linux-5.12xx.tar

配置环境变量

要在x86_64的宿主机编译arm64的镜像,需要arm64的gcc工具。每次重新打开shell都需要配置。

export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-

查看环境变量是否设置成功

export

内核配置

在顶层目录生成arm64的默认配置.config,其实是把arch/arm64/configs/defconfig复制到顶层目录。

make defconfig

配置其他特性开关

make menuconfig

  1. 内核打开initramfs支持
    ram fs是将最小文件系统直接编译进内核镜像中。

General setup --->
----> [*] Initial RAM filesystem and RAM disk (initramfs/initrd) support
---->() Initramfs source file(s)

  1. 内核打开ramdisk支持
    ram disk是在内存中模拟磁盘,使用ramdisk比Initramfs灵活一些,不需要每次都去编译内核。不过在qemu中,都支持通过-initrd指定文件镜像。如果RAM disk size这个大小和你做的ramdisk不匹配,则启动时仍然会出现 kernel panic内核恐慌,提示ramdisk格式不正确,挂载不上ramdisk。也可通过设置启动参数修改ramdisk大小。“ramdisk_size=65536”

Device Drivers --->
[] Block devices --->
<
> RAM block device support
(4096) 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

  1. 核打开ext4支持
    不一定非得是ext4,这里勾选ext4仅仅是因为我们要制作的ramdisk是ext4格式

File systems --->
{*} The Extended 4 (ext4) filesystem

编译镜像

编译,并且10个job运行,加快编译速度。生成的内核镜像在:arch/arm64/boot/Image

make -j 10

启动裸内核

qemu相关使用

qemu有很多帮助信息。

  • qemu-system-aarch64 -h // 查看全部帮助信息
  • qemu-system-aarch64 -machine help //查看支持的machine
  • qemu-system-aarch64 -cpu help //查看machine支持的cpu类型
  • When using -nographic, press 'ctrl-a h' to get some help

启动裸内核

执行如下命令尝试启动内核,如果一切顺利,可看到Linux的启动log,但是大概率会运行到根文件系统初始化时挂死。哈哈,不过到这里证明我们成功一半了。

qemu-system-aarch64 -M virt -cpu cortex-a57 -m 1024 -nographic -kernel Image

-M 指定开发板
-m 指定内存大小
-kernel 指定内核文件
-dtb 指定dtb文件
-nographic 指定不需要图形界面
-append 指定启动参数

$ ./build/qemu-system-aarch64 -machine 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)

如果内核没启动,反而有如下错误打印,大概率是qemu版本问题。可考虑下载qemu源码,本地编译qemu二进制。

rom: requested regions overlap (rom bootloader. free=0x0000000041e99200, addr=0x0000000040000000)
qemu-system-aarch64: rom check and register reset failed

制作根文件系统

制作一个简易的根文件系统,该文件系统包含的功能极其简陋,仅为了验证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=aarch64-linux-gnu-
make menuconfig

进入menuconfig后,配置为静态编译

--- Build Options
[*] Build static binary (no shared libs)

make
make install

编译完成之后,在busybox根目录下会有_install目录,该目录是编译好的一些命令集合。

根文件系统格式

创建initrd文件系统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/* .
umount mnt

至此,简易版根文件系统就制作完成,该根文件系统只含有最基本的功能,一些其他功能在以后的操作中会进行添加。

指定ramdisk文件,并启动kernel

qemu-system-aarch64 -M virt -cpu cortex-a57 -m 1G -nographic -kernel Image -append "root=/dev/ram0" -initrd ~/rootfs.ext4

制作initramfs

cd busybox/_install/ rootfs
cd rootfs
find . | cpio -o -H newc > rootfs.cpio
gzip -c rootfs1.cpio > rootfs.cpio.gz
qemu-system-aarch64 -M virt -cpu cortex-a57 -m 1G -nographic -kernel Image -initrd ~/rootfs.cpio.gz -append "rdinit=/linuxrc"

initramfs与initrd区别

Linux内核只认cpio格式的initramfs文件包(因为unpack_to_rootfs只能解析cpio格式文件),非cpio格式的initramfs文件包将被系统抛弃,而initrd可以是cpio包也可以是传统的镜像(image)文件,实际使用中initrd都是传统镜像文件如ext4格式。
使用initramfs,命令行参数将不需要"initrd="和"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

常见问题

串口打印

在dev目录下创建如下设备节点,解决找不到tty的问题,此处需要管理员权限。

mkdir -p dev
mknod dev/console c 5 1
mknod dev/null c 1 3

找不到rcS文件

can't run '/etc/init.d/rcS': No such file or directory

kernel启动参数

不知怎的,最新版本的Linux没有这个文档了,看看之前版本的吧

https://elixir.bootlin.com/linux/v2.6.37.5/source/Documentation/kernel-parameters.txt

常用的主要有

root=    [KNL] Root filesystem
ro      [KNL] Mount root device read-only on boot
rw     [KNL] Mount root device read-write on boot
rdinit=   [KNL] Format: <full_path>
      Run specified binary instead of /init from the ramdisk, used for early userspace startup. See initrd.
loglevel=   All Kernel Messages with a loglevel smaller than the console loglevel will be printed to the console. It can also be changed with klogd or other programs. Theloglevels are defined as follows:

ramdisk_size= [RAM] Sizes of RAM disks in kilobytes

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命令需要添加以下选项

-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")))

启动打印时间戳

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


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM