編碼的世界 / 優質文選 / 生物

Linux aarch64 編譯 & qemu 搭建實驗平台


2022年7月29日
-   

交叉編譯工具鏈
去 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/下會產生 Imagevmlinux 編出來大概有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」
出錯如下: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

注意:
  • 執行 make menuconfig 後需要修改配置,將 Build static binary (no shared libs) 選上。
  • 對於要在非 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 指定的文件是否存在。
  • 如果不存在的話,說明 ramdisk 中沒有什麼好執行的,使用 prepare_namespace() 准備根文件系統,因為要執行"init=xxx"指定的程序了。
  • 如果 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
    https://medicineyeh.wordpress.com/2016/03/29/buildup-your-arm-image-for-qemu/ ↩︎
    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/ ↩︎
    https://blog.csdn.net/LEON1741/article/details/54809347 ↩︎

    熱門文章