Linux aarch64 编译 & qemu 搭建实验平台

交叉编译工具链

去 linaro 官网下载即可。https://www.linaro.org/downloads/
https://releases.linaro.org/components/toolchain/binaries/latest-7/aarch64-linux-gnu/

解压以后在环境变量 PATH 里加上工具链的路径。
export PATH=/path/to/bin/linaro-xxx/bin:$PATH
验证一下:aarch64-linux-gnu-gcc -v

内核编译

下载内核源码:

wget https://mirrors.edge.kernel.org/pub/linux/kernel/v4.x/linux-4.10.tar.xz

在根目录下会产生 vmlinux*,arch/arm64/boot/下会产生 Image
vmlinux 编出来大概有190+M,Image 去除了很多调试信息,大概是15M。

cd ~/linux-4.10/

# cp ./arch/arm64/configs/defconfig  .config

mkdir build

# stay in the current dir, do not cd to ./build
# 如果需要调整配置选项,则使用 menuconfig 进行配置
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- O=./build defconfig
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- O=./build menuconfig

# 注意这里 menuconfig 需要选中以下两个选项,这里给 ramdisk 64M的空间,应当比后面生成的 rootfs 大。
	General setup  --->
	    [*] Initial RAM filesystem and RAM disk (initramfs/initrd) support
	 
	Device Drivers  --->
	    [*] Block devices  --->
	        <*>   RAM block device support
	        (65536) Default RAM disk size (kbytes)
        
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- O=./build Image -j16

qemu 安装

请参考 qemu-4.x.x 安装

$ sudo apt-get install qemu-system-arm

qemu-system-aarch64 运行

Linux 要如何启动?1

Linux kernel 在自身初始化完成之后,需要能够找到并运行第一个用户程序(这个程序通常叫做 “init” 程序)。用户程序存在于文件系统之中,因此,内核必须找到并挂载一个文件系统才可以成功完成系统的引导过程。在 grub 中提供了一个选项 “root=” 用来指定第一个文件系统,但随着硬件的发展,很多情况下这个文件系统也许是存放在USB设备,SCSI设备等等多种多样的设备之上,如果需要正确引导,USB或者SCSI驱动模块首先需要运行起来,可是不巧的是,这些驱动程序也是存放在文件系统里,因此会形成循环依赖的情况。

为解决这个问题,Linux kernel 提出了一个 RAM disk 的解决方案,把一些启动所必须的用户程序和驱动模块放在 RAM disk中,这个 RAM disk 看上去和普通的 disk 一样,有文件系统,有cache,内核启动时,首先把RAM disk挂载起来,等到init 程序和一些必要模块运行起来之后,再切到真正的文件系统之中。

上面提到的 RAM disk 的方案实际上就是 initrd。 如果仔细考虑一下,initrd 虽然解决了问题但并不完美。 比如,disk 有cache 机制,对于 RAM disk 来说,这个cache机制就显得很多余且浪费空间;disk 需要文件系统,那文件系统(如ext2等)必须被编译进 kernel 而不能作为模块来使用。

Linux 2.6 kernel 提出了一种新的实现机制,即 initramfs。顾名思义,initramfs 只是一种 RAM filesystem 而不是 diskinitramfs 实际是一个 cpio 归档,启动所需的用户程序和驱动模块被归档成一个文件。因此,不需要 cache,也不需要文件系统。

QEMU has a command argument called “-kernel”. It is a very handy function!! Because of this feature, we don’t need to bother the complicated boot sequence and problems on locating Kernel Image. QEMU will uncompress the kernel image to a proper memory location and start to run the kernel code.

很显然指定 -kernel /path/to/kernel_image 即可。但是这样是无法正常启动 Linux 的。

qemu-system-aarch64 -kernel build/arch/arm64/boot/Image -append "console=ttyAMA0" -m 2048M -smp 4  -M virt -cpu cortex-a57 -nographic

qemu-system-aarch64 \
    -kernel build/arch/arm64/boot/Image \
    -append "console=ttyAMA0" \
    -m 2048M -smp 4  \
    -M virt -cpu cortex-a57 \
    -nographic

-m 指定内存大小
-M 指定虚拟机器「machine」的类型,virt 是 qemu 提供的一种机器类型,可以理解成一种单板。
-cpu 指定虚拟CPU的型号
-smp 指定对称多处理的核心数
-append 指定内核启动时使用的命令行参数「cmdline」

在这里插入图片描述
ctrl+a + c 进入 qemu-monitor,输入 q 退出,或者按 ctrl+a + x 退出 qemu。
在这里插入图片描述
出错如下:kernel panic ... unable to mount root fs ...。正确地启动需要一个根文件系统。

创建 rootfs

Kernel modules are mostly drivers, both hardware drivers and software drivers. For example, the Ethernet! If the driver is a kernel module stored in root file system, Linux kernel will not be able to access the Internet before mounting the root file system. Another example is ext3, ext4 driver, Linux Kernel must contain these basic file system driver in order to execute init procedure because the init files are located in root file system. It’s somehow a very common problem which was very popular in early years. That’s why we have so-called initramfs or rootfs. They are minimal file system images containing all kernel modules(.ko files), init procedure scripts, and necessary binaries to boot a full system.

busybox2

wget https://busybox.net/downloads/busybox-1.30.1.tar.bz2
tar -xjf busybox-1.30.1.tar.bz2
cd busybox-1.30.1
make defconfig
make menuconfig
make -j16
make install

注意:

  1. 执行 make menuconfig 后需要修改配置,将 Build static binary (no shared libs) 选上。
  2. 对于要在非 host 平台运行的情况,其交叉编译工具链也要配好!
Build Options  --->
    [*] Build BusyBox as a static binary (no shared libs)

交叉编译选项别忘了
(/path/to/aarch64-linux-gnu-) Cross Compiler prefix

上面的命令执行完后,可以看到源码目录 _install 目录下生成了一些目录。

$ ls
bin  linuxrc  sbin  usr

除了上面的文件目录外,我们还需要打包更多的目录用于生成根文件系统,下面有一个简要的脚本去完成打包过程,里面完成的事情是创建了一个 img 镜像,将其挂载到 rootfs 目录,拷贝 busybox 的编译产物到 rootfs 目录,然后在 rootfs 里创建了更多的目录,另外在 rootfs 目录里创建了一些设备节点,增加了配置文件,最后将 img 从 rootfs 上解除挂载,如此一来 rootfs 的内容就被写到 img 中了,后面 img 将供 Linux 启动使用。

BUSYBOX_VERSION=1.30.1

dd if=/dev/zero of=busybox-${BUSYBOX_VERSION}-rootfs_ext4.img bs=1M count=64 oflag=direct
mkfs.ext4 busybox-${BUSYBOX_VERSION}-rootfs_ext4.img

mkdir rootfs
sudo mount busybox-${BUSYBOX_VERSION}-rootfs_ext4.img rootfs/
sudo cp -raf busybox-${BUSYBOX_VERSION}/_install/* rootfs/

cd rootfs
sudo mkdir -p proc sys tmp root var mnt dev
sudo mknod dev/tty1 c 4 1
sudo mknod dev/tty2 c 4 2
sudo mknod dev/tty3 c 4 3
sudo mknod dev/tty4 c 4 4
sudo mknod dev/console c 5 1
sudo mknod dev/null c 1 3
sudo cp -r ../busybox-${BUSYBOX_VERSION}/examples/bootfloppy/etc/ .

cd ..
sudo umount rootfs
运行,起飞!

这里 Image 的路径被我移动过了,大家不要误会 =.=
注意这里启动设备是 -hda/-hdb file use 'file' as IDE hard disk 0/1 image

qemu-system-aarch64 \
        -kernel ../linux-4.10/build/Image \
        -nographic \
        -append "root=/dev/vda console=ttyAMA0 rootfstype=ext4 init=/linuxrc rw" \
        -m 2048M \
        -smp 4  \
        -M virt \
        -cpu cortex-a57 \
        -hda busybox-1.30.1-rootfs_ext4.img

-M: Specify the machine type. Use “-M help” to list all the supported boards
-kernel: Specify the kernel image (bzimage)
-dtb: Specify the hardware description file (Device Tree Blob)
-nographic: Run QEMU without GUI. It’s much more convenient.
-append: Specify Linux kernel arguments. Here we set default console to ttyAMA0 which is one of QEMU’s console when Guest OS/Applications wants to print something on host’s terminal.
-drive: Specify a drive for the image. It can be SD card, flash, etc. It’s the lowest level of drive API. We use if(interface) SD card with write back cache policy to save image access time.
-sd: It is a higher level API to specify a drive. It’s equivalent to “-drive if=sd,file=”
-net nic,macaddr=$macaddr: Specify the mac address
-net tap,vlan=0,ifname=tap0: Use tap device for internet access
-snapshot: Don’t write back to the original disk image.

如下图所示,运行成功!可以用 ls 命令看下目录情况,由于未作任何系统配置,比如 /etc/passwd/etc/group/etc/shadow/etc/hostname 等文件,所以系统的操作和易用性还有待改进。
在这里插入图片描述

制作成 initramfs

前面提过 initramfs 实际是一个 cpio 归档,直接用 cpio 打包压缩即可。

# cd rootfs
# find . -print0 | cpio --null -ov --format=newc  | gzip -9  > ../initramfs.cpio.gz

# or use this way

cd ../linux-4.10/build
# should cd to build, because the script use the related path,
# and gen_init_cpio is under build/usr/

sh ../scripts/gen_initramfs_list.sh \
        -o ../../run_linux/initramfs.cpio.gz ../../run_linux/rootfs2/

注意这里启动设备换成了 -initrd,use 'file' as initial ram disk

qemu-system-aarch64 -kernel build/arch/arm64/boot/Image -initrd initramfs.cpio.gz -append "console=ttyAMA0 rdinit=/linuxrc" -M virt -cpu cortex-a57 -nographic -smp 4 -m 2048M

qemu-system-aarch64 \
	-kernel build/arch/arm64/boot/Image \
	-initrd initramfs.cpio.gz \
	-append "console=ttyAMA0 rdinit=/linuxrc" \
	-M virt \
	-cpu cortex-a57 \
	-nographic \
	-smp 4 \
	-m 2048M

注意:通常引导内核时向 command line 传递的参数都是 init=xxx ,而对于 initrd 则是传递 rdinit=xxx 。内核的处理代码位于 init/main.c。rdinit=xxx 在内核中被 ramdisk_execute_command 变量接收,如果没有 rdinit 参数,ramdisk_execute_command 默认为 “/init”。sys_access() 检查 ramdisk_execute_command 指定的文件是否存在。

  1. 如果不存在的话,说明 ramdisk 中没有什么好执行的,使用 prepare_namespace() 准备根文件系统,因为要执行"init=xxx"指定的程序了。
  2. 如果 ramdisk_execute_command 指定的文件存在,则接下来全部由其接管。

如果使用 "init=/linuxrc" 参数,此时没有 rdinit 参数,则内核中默认去找 /init,没找到,则尝试挂载rootfs。rootfs挂载失败,则一直报错。

  • 解压和制作ramdisk:
    解压 gunzip -c …/initrd-cpio.gz | cpio -i
    制作 find . | cpio -o -H newc | gzip > …/ramdisk.cpio.gz
其他方法

记录一下网上看到一个方法,可以参考,实际上是把交叉编译选项放在命令行里带进去了。
如果是x86_64 的话,不需要 ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-

sudo make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- defconfig
sudo make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- install

制作根文件系统。

sudo mkdir rootfs
sudo cp _install/* -r rootfs/
sudo mkdir rootfs/lib
sudo cp -P /usr/arm-linux-gnueabi/lib/* rootfs/lib/

sudo mknod rootfs/dev/tty1 c 4 1
sudo mknod rootfs/dev/tty2 c 4 2
sudo mknod rootfs/dev/tty3 c 4 3
sudo mknod rootfs/dev/tty4 c 4 4

dd if=/dev/zero of=a9rootfs.ext3 bs=1M count=32
mkfs.ext3 a9rootfs.ext3

sudo mkdir tmpfs
sudo mount -t ext3 a9rootfs.ext3 tmpfs/ -o loop
sudo cp -r rootfs/*  tmpfs/
sudo umount tmpfs

qemu-system-arm -M vexpress-a9 -m 512M -kernel /home/peter/work/src/linux/linux/arch/arm/boot/zImage -nographic -append "root=/dev/mmcblk0 console=ttyAMA0" -sd a9rootfs.ext3

使用 Buildroot 来创建根文件系统

推荐使用 Buildroot 的方法来创建 rootfs,功能强大,便于定制。这里就不详述了。

支持网络

首先在 host 机器上创建一个 tap 网络设备。

$ sudo ip tuntap add dev tap_ci mod tap
$ sudo ifconfig tap_ci up
$ sudo ifconfig tap_ci 192.168.110.10 netmask 255.255.255.0

qemu 启动参数增加如下参数即可。

-netdev tap,id=mynet,script=no,downscript=no,ifname=tap_ci \
-device virtio-net-pci,netdev=mynet,mrg_rxbuf=off,csum=off,guest_csum=off,gso=off,guest_tso4=off,guest_tso6=off,guest_ecn=off,guest_ufo=off,ctrl_vlan=off,ctrl_rx=off \

支持 9p 文件共享

# 首先在 host 机器上创建一个共享目录。
$ mkdir kmodules

# qemu 启动参数增加,上面的共享目录由 path 指定
--fsdev local,id=kmod_dev,path=kmodules,security_model=passthrough \
-device virtio-9p-pci,id=fs0,fsdev=kmod_dev,mount_tag=hostshare \

# qemu 启动 Linux 后,执行命令,把共享目录 kmodules 和 Linux 中的 /mnt 目录关联起来
# 当然也可以在 Linux 中创建一个新目录关联这个共享目录
$ mount -t 9p -o trans=virtio,version=9p2000.L hostshare /mnt

x86_64

x86_64的同理,不过命令有些差别,这里也记录一下。根文件系统可以使用系统带的命令进行生成。

mkinitramfs -o ramdisk.img

qemu-system-x86_64 -kernel ./bzImage -nographic -append "console=tty0" -initrd ramdisk.img -m 512

qemu-system-x86_64 \
    -kernel ./bzImage \
    -nographic \
    -append "console=tty0" \
    -initrd ramdisk.img \
    -m 512

gdb 调试

在上述启动命令后加上 -s-S。前者是 -gdb tcp::1234 的缩写,后者表示 freeze CPU at startup (use 'c' to start execution)。运行后打开 gdb,加载内核调试文件,运行target remote :1234 attach 到 qemu 里的调试端口,使用 hbreak start_kernelstart_kernel 打个断点。然后 c 让内核继续运行。
在这里插入图片描述

Image zImage uImage3

1、首先来解释一下前面2个命令的区别。Image为普通的内核映像文件,而zImage为压缩过的内核映像文件(其中的z字母就是压缩的意思)。一般情况下,编译出来的Image大约为4M,而zImage不到2M。

2、然后来解释一下第3个命令uImage。它是uboot专用的映像文件,它是在zImage之前加上一个长度为64字节的“头”,说明这个内核的版本、加载位置、生成时间、大小等信息;其0x40之后与zImage没区别。换句话说,如果直接从uImage的0x40位置开始执行,那么zImage和uImage没有任何区别。

为什么要用uboot 的mkimage工具处理内核映像zImage呢?

因为uboot在用bootm命令引导内核的时候,bootm需要读取一个64字节的文件头,来获取这个内核映象所针对的CPU体系结构、OS、加载到内存中的位置、在内存中入口点的位置以及映象名等等信息。这样bootm才能为OS设置好启动环境,并跳入内核映象的入口点。而mkimage就是添加这个文件头的专用工具。具体的实现请看uboot中bootm的源码和mkimage的源码。

下面介绍下mkimage这个工具的用法:

参数说明:

- A:指定 CPU 的体系结构,有:alpha、arm 、x86、ia64、mips、mips64、 ppc 、s390、sh、sparc 、sparc64、m68k 等;
- O:指定操作系统类型,有:openbsd、netbsd、freebsd、4_4bsd、linux、 svr4、esix、solaris、irix、sco、dell、ncr、lynxos、vxworks、psos、qnx、u-boot、rtems、artos;
- T:指定映象类型,有:standalone、kernel、ramdisk、multi、firmware、script、filesystem;
- C:指定映象压缩方式,有:
:none 不压缩(一般使用这个,因为 zImage 是已经被 bzip2 压缩过的自解压内核);
:zip 用 gzip 的压缩方式;
:bzip2 用 bzip2 的压缩方式;
- a:指定映象在内存中的加载地址,映象下载到内存中时,要按照用 mkimage 制作映象时该参数所指定的地址值来下载;
- e:指定映象运行入口点地址,这个地址就是 -a 参数指定值加上 0x40(因为前面有个 mkimage 添加的 0x40 个字节的头);
- n:指定映象名;
- d:指定制作映象的源文件;

例如:下面命令的作用就是,将目录下的 zImage 文件制作成符合uboot引导要求的uImage.img文件,使得uboot能够正确的引导和启动linux内核。-e 的地址特别要小心,在 -a 指定的地址基础上加 0x40

mkimage -n ‘mykernel’ -A arm -O linux -T kernel -C none -a 0x30008000 -e 0x30008040 -d zImage uImage.img


  1. https://medicineyeh.wordpress.com/2016/03/29/buildup-your-arm-image-for-qemu/ ↩︎

  2. https://chasinglulu.github.io/2019/07/27/%E5%88%A9%E7%94%A8Qemu-4-0%E8%99%9A%E6%8B%9FARM64%E5%AE%9E%E9%AA%8C%E5%B9%B3%E5%8F%B0/ ↩︎

  3. https://blog.csdn.net/LEON1741/article/details/54809347 ↩︎

  • 7
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值