Learning Linux Development for ARM64 Architecture Using QEMU

1. Installing QEMU

QEMU has been around for a long time, and I never thought about what its full name is. Today, while writing this article, I looked it up, and the full name should be quick emulator.

QEMU (quick emulator) is a free and open-source virtual machine (VMM) for hardware virtualization, written by Fabrice Bellard and others.

Everyone has probably come into contact with virtual machines. Generally, beginners learning Linux will first need to install a virtual machine software (VMware or VirtualBox). However, based on my experience with these virtual machines, they are used to simulate X86 architecture machines, which means the Intel chips in our computers. They all have graphical interfaces to start and boot a large system.

QEMU can simulate many embedded chips and devices during our embedded development process, supporting various processor architectures and platforms.

Taking the Ubuntu system as an example, QEMU can be installed in two ways:

  1. Install using the apt command

    sudo apt-get install qemu-system-arm
  2. Download the source code from the official website and compile it yourself

    For specific installation steps, refer to the official documentation.

Using the apt method is more convenient, but generally, the corresponding version is a bit lower.

Installing from source allows you to choose any version, but it requires downloading and compiling, which is a more complex process.

Since we are learning about the ARM64 architecture, we need to ensure that we have installed QEMU with ARM64 architecture support. After installation, you can verify the installation result with the following command:

qemu-system-aarch64 --version

QEMU emulator version 5.2.0
Copyright (c) 2003-2020 Fabrice Bellard and the QEMU Project developers

2. Compiling the Linux Kernel

2.1 Downloading Kernel Source Code

https://www.kernel.org/

This website allows you to download various versions of the kernel source code. To avoid issues, we choose to download a stable version, longterm: 4.19.171.

Download a complete kernel source from the link below:

https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-4.19.171.tar.xz

After downloading, extract the source code to a specified directory:

mkdir qemu_linux
cd ~/qemu_linux
tar -xvf ~/Downloads/linux-4.19.171.tar.xz

2.2 Setting Up the Cross Compilation Toolchain

We are developing on an X86 platform, and the target platform is AArch64 architecture. We need to download and configure the cross-compilation toolchain.

The toolchain can be downloaded from the ARM official website, and you need to download the A series of toolchains.

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)

Download the cross-compilation tool for the X86 platform Linux system, used for compiling Linux toolchains.

After downloading, follow some steps to complete the cross-compilation configuration:

  1. Extract the toolchain to a directory

  2. Set the PATH environment variable, adding the bin directory of the cross-compilation toolchain (which contains the executable files of the toolchain) to the PATH environment variable.

After setting up, you can confirm the configuration is correct by entering the following command:

aarch64-none-linux-gnu-gcc -v

2.3 Starting Kernel Compilation

Once you have the kernel source and toolchain ready, you can start compiling. If it’s your first time compiling, you may be missing some components; you can search for the corresponding commands based on the prompts and install the necessary plugins.

  1. Generate the default configuration file for the ARM64 platform

    make ARCH=arm64 defconfig
  2. Start compiling the source code

    make ARCH=arm64
  3. After compilation, a series of files will be generated. The main files we will use are in the following directory:

    arch/arm64/boot/

    The main file we will use here is the Image file, which is the executable file generated from the kernel compilation.

3. Using QEMU Emulator to Boot the Kernel

qemu-system-aarch64 -machine virt        \
                    -cpu cortex-a53     \
                    -nographic          \
                    -smp 1                 \
                    -m 2048             \
                    -kernel arch/arm64/boot/Image

Try executing the above command to boot the kernel, and it really started up.

Let’s explain the parameters used above:

-machine virt specifies the device that QEMU simulates, which refers to a general ARMv8 architecture chip. Many chips have attempted to implement their chip and board support.

-cpu cortex-a53 specifies the specific core. The A53 is an early classic core. Different cores may have slight differences in performance metrics and some configurations, but it does not make much difference for our simulation.

-nographic indicates that there is no graphical interface when starting.

-smp 1 sets the device to have only one core.

-m 2048 indicates that the device has 2048M of memory.

-kernel arch/arm64/boot/Image specifies the kernel file used for booting.

The boot screen is quite simple, and it started up easily:

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

At the end, an exception occurred because the filesystem was not mounted. We need to add a virtual storage device to store the filesystem.

We also need to add an initialization process for the kernel to execute.

3.1 Adding Root Filesystem Support

The root filesystem is essentially a regular filesystem, but the systems we usually see are quite complex and require various files. We can use busybox or buildroot to establish the root filesystem while providing a lot of functionality. Today, we will manually create a very simple root filesystem.

  1. Create a local folder as the root filesystem directory, where we can store files prepared for the kernel.

  2. Use the mkinitramfs.sh script to convert this directory into a ramfs filesystem format.

  3. Use QEMU to boot the kernel, specifying the corresponding root filesystem file.

3.1.1 Configuring Kernel Options to Enable ramfs Support

make ARCH=arm64 menuconfig

Device Drivers
    Block devices
        BLK_DEV_RAM

Follow the path above to configure and enable the BLK_DEV_RAM option.

3.1.2 Modifying QEMU Startup Parameters to Specify the Root Filesystem File

After the kernel supports ramfs devices, we can pass parameters to QEMU to specify the related files on the host as the ramfs filesystem passed to the kernel. Modify the QEMU startup command:

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

Adding the option -initrd initramfs.cpio.gz specifies that this file will be used as the content of ramfs.

Adding the option -append "root=/dev/ram0 rootfstype=ramfs rw init=/init" indicates the parameters passed to the kernel.

root indicates that the root filesystem device is /dev/ram0, and it specifies that the filesystem type is ramfs.

init indicates the name of the first process that the system will execute, and the kernel will load this process from the filesystem for execution.

initramfs.cpio.gz is the file generated by the script that contains the root filesystem.

The packaging script (mkinitramfs.sh) content:

#!/bin/sh

# Copyright 2006 Rob Landley <[email protected]> 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

Packaging command:

./mkinitramfs.sh  rootfs/ initramfs.cpio.gz

Learning Linux Development for ARM64 Architecture Using QEMU

rootfs

3.1.3 Restarting the System After Adding the Root Filesystem

After having the filesystem, try to boot again:

jhb@jhb-pc:~/qemu_linux$ ./mkinitramfs.sh  rootfs/ initramfs.cpio.gz
creating initramfs.cpio.gz from rootfs/
1 block
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

As we can see, the root filesystem has been successfully mounted, but there is an error message:

[    1.001108] Run /init as init process
[    1.001660] Kernel panic - not syncing: Requested init /init failed (error -2).

3.1.4 Implementing an Init Process

The initialization process init failed because it is the one we passed to the kernel through the init=”/init” parameter.

This is a regular process, so we can write a simple hello world program as our initialization process and place it in the root filesystem to see if it can execute correctly.

init.c

#include <stdio.h>

int main()
{
    printf("hello world!\n");
    while(1);

    return 0;
}

Compile the above program:

aarch64-none-linux-gnu-gcc -o init init.c -static

Place the compiled init into the root filesystem directory, regenerate the root filesystem, and boot the kernel:

./mkinitramfs.sh  rootfs/ initramfs.cpio.gz

./boot_qemu.sh

We can see that our program prints out the content correctly, and the system has successfully booted:

[    1.058241] Freeing unused kernel memory: 1216K
[    1.059787] Run /init as init process
hello world!

This shows that the process of the Linux kernel starting to the application is not very complicated. Operating systems like Ubuntu do a lot of complex work in the initialization process before we see the interface. Busybox also uses the interfaces provided by Linux to implement various commands. We can enrich our initialization process later to implement some basic functionalities and commonly used commands.

Learning Linux Development for ARM64 Architecture Using QEMU

Leave a Comment

×