找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4359

积分

0

好友

565

主题
发表于 1 小时前 | 查看: 4| 回复: 0

1. 安装 qemu

QEMU 用了很久了,之前一直没仔细想过它的全称是什么。这次要写文章,特意去查了一下,全称应该是 Quick Emulator

QEMU(quick emulator)是由法布里斯·贝拉(Fabrice Bellard)等人编写的免费开源虚拟机(VMM),支持硬件虚拟化

虚拟机大家应该都接触过。学 Linux 的时候,初学者一般都会先装一个虚拟机软件,比如 VMware 或 VirtualBox。不过这些虚拟机给我的感觉,更多是模拟 x86 架构的机器,也就是我们电脑上 Intel 芯片那套环境,而且都是带图形界面去启动一个完整系统。

而在嵌入式开发里,QEMU 能模拟很多嵌入式芯片和设备,支持的处理器架构和平台也相当丰富。

以 ubuntu 系统为例,安装 QEMU 可以通过下面两种方式:

  1. 使用 apt 命令安装

    sudo apt-get install qemu-system-arm
  2. 从官网下载源码,自行编译安装

    具体的安装步骤可以查看官网说明。

使用 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 内核的代码。

下载完成之后需要做一些步骤来完成交叉编译配置:

  1. 解压工具链到一个目录
  2. 设置 PATH 环境变量,将交叉编译工具链 bin 目录(该目录包含了工具链的可执行文件)加入到 PATH 环境变量

设置完成之后可以通过输入如下命令确认设置正确:

aarch64-none-linux-gnu-gcc -v

2.3 开始编译内核

内核源码和工具链就绪后,就可以开始编译了。第一次编译可能会提示缺少一些组件,根据报错信息自行搜索对应命令,安装依赖即可。

  1. 生成 arm64 平台默认配置文件

    make ARCH=arm64 defconfig
  2. 开始编译源码

    make ARCH=arm64
  3. 编译完成之后会生成一系列的文件,我们后面用到的主要在如下目录

    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 来构建根文件系统,它们也附带了不少功能。不过今天我们自己手动制作一个极简版本。

  1. 建立一个本地的文件夹作为根文件系统目录,该目录我们可以存放为内核准备的文件
  2. 使用 mkinitramfs.sh 脚本将该目录转换成 ramfs 文件系统格式
  3. 使用 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

rootfs 目录经 mkinitramfs.sh 打包为 initramfs.cpio.gz 并映射到 /dev/ram0 的流程图

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 等基础功能和常用命令。




上一篇:DeepSeek Harness 刚发布:一切皆插件,Node 服务 + Web 界面的 Agent 框架初体验
下一篇:QEMU 模拟 Arm 开发板:Ubuntu 上跑通 U-Boot、Linux 与 Buildroot
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-8-14 04:33 , Processed in 0.844737 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表