这里就重新在开源的QEMU平台上搭建一个Trusted Firmware-A + U-boot + Linux kernel的开发环境,
如下所有的步骤最终都汇聚在我的github boot-aarch64-linux-qemu ,如果仅仅是使用,不需要知道它的具体细节的,建议直接使用github仓库提供的Makefile。
编译代码
- 编译qemu代码
编译qemu代码要使用到python virtualenv的环境,这里首先使用如下命令创建python的环境:
接下来就可以使用如下命令来配置和编译qemu:
source $(workspace)/tools/venv/bin/activate && cd $(workspace)/src/qemu/ && ./configure --target-list=aarch64-softmmu --enable-virtfs
source $(workspace)/tools/venv/bin/activate && make -C $(workspace)/src/qemu
这里还有几个关键的信息,在后面编译u-boot的时候需要使用到,准备好所有的image之后,会使用如下命令来运行qemu,当然参数可以根据自己的情况进行调整:
src/qemu/build/qemu-system-aarch64 \
-M virt,gic-version=3,virtualization=on,type=virt,mte=on,secure=on \
-nographic \
-cpu max -nographic -m 16G \
-smp 16 \
-bios src/tf-a/build/qemu/debug/flash.bin \
-device loader,file=src/linux/arch/arm64/boot/Image,addr=0x40400000 \
-drive file=rootfs/rootfs.img,if=virtio,format=raw \
所以在qemu平台上,有
- 编译u-boot代码
在编译u-boot代码之前,需要根据Linux kernel和rootfs的信息来配置u-boot的BOOTARGS和BOOTCOMMAND。 这里有几个比较关键的参数:
这里使用了 root=/dev/vda1, 这是因为后面做rootfs的时候,使用了qemu的virtio这个参数:
-drive file=rootfs/grub-busybox.img,if=virtio,format=raw
而制作rootfs 的时候,rootfs会放到第一个分区,所以就是vda1。
- BOOTCOMMAND中kernel Image 和device tree的地址
这里使用了 booti 0x40400000 - 0x40000000,这个是因为在启动qemu的时候,有如下参数:
-device loader,file=src/linux/arch/arm64/boot/Image,addr=0x40400000
根据上面的描述,使用如下命令来配置u-boot:
cd $(workspace)/src/u-boot
echo "CONFIG_USE_BOOTARGS=y" > qemu.cfg
echo "CONFIG_BOOTARGS=\"console=ttyAMA0 earlycon=pl011,0x9000000 root=/dev/vda1 rw debug user_debug=31 nokaslr loglevel=9\"" >> qemu.cfg
echo "CONFIG_BOOTCOMMAND=\"booti 0x40400000 - 0x40000000\"" >> qemu.cfg
export ARCH=aarch64 ;
export CROSS_COMPILE=$(CROSS_COMPILE) ;
make qemu_arm64_defconfig;
scripts/kconfig/merge_config.sh -m -O ./ .config qemu.cfg ;
这里的CROSS_COMPILE 可以根据前面下载的gcc来决定,这里可以把他设置成:
export CROSS_COMPILE=$(workspace)/tools/arm-gnu-toolchain-13.2.Rel1-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-
配置完成之后,可以打开u-boot目录下的.config 来确保CONFIG_USE_BOOTARGS,CONFIG_BOOTARGS 和 CONFIG_BOOTCOMMAND已经设置成期望的值。
CONFIG_BOOTARGS="console=ttyAMA0 earlycon=pl011,0x9000000 root=/dev/vda1 rw debug user_debug=31 nokaslr loglevel=9"
CONFIG_BOOTCOMMAND="booti 0x40400000 - 0x40000000"
接下来使用如下命令来编译u-boot:
cd $(workspace)/src/u-boot
export ARCH=aarch64 ;
export CROSS_COMPILE=$(CROSS_COMPILE) ;
make
这一步做完,最终生成 src/u-boot/u-boot.bin.
- 编译tf-a代码
编译在tf-a的过程中,u-boot是作为tf-a的BL33的image,所以需要先编译前面的u-boot后,才能编译tf-a,使用如下命令:
export CROSS_COMPILE=$(CROSS_COMPILE)
cd $(workspace)/src/tf-a
make PLAT=qemu DEBUG=1 BL33=$(workspace)/src/u-boot/u-boot.bin all fip V=1 ENABLE_FEAT_MTE2=1 QEMU_USE_GIC_DRIVER=QEMU_GICV3
dd if=build/qemu/debug/bl1.bin of=build/qemu/debug/flash.bin bs=4096 conv=notrunc
dd if=build/qemu/debug/fip.bin of=build/qemu/debug/flash.bin seek=64 bs=4096 conv=notrunc
这里面仅仅是使用了tf-a的默认配置,如TTBR等feature 都有没有使能。 如果需要使能更多feature,可以根据自己的需求添加。
从上面的命令可以看出, u-boot 是被打包到flash.bin里面,所以如果更改了u-boot,必须重新运行tf-a的编译过程,把u-boot重新打包到flash.bin里面。
更多关于QEMU的TF-A, 参考 QEMU virt Armv8-A .
- 编译Linux kernel代码
编译Linux kernel,这里使用kernel中的默认配置,即defconfig。即使用如下命令来配置kernel:
cd $(workspace)/src/linux
make -C $(SRC_DIR)/linux ARCH=arm64 defconfig CROSS_COMPILE=$(CROSS_COMPILE)
make -C $(SRC_DIR)/linux ARCH=arm64 olddefconfig CROSS_COMPILE=$(CROSS_COMPILE)
如果想要对kernel 进行额外的配置,使用如下命令:
cd $(workspace)/src/linux
make -C $(SRC_DIR)/linux ARCH=arm64 menuconfig CROSS_COMPILE=$(CROSS_COMPILE)
配置完成之后,如下命令可以用来进行编译:
cd $(workspace)/src/linux
make -C $(SRC_DIR)/linux ARCH=arm64 Image CROSS_COMPILE=$(CROSS_COMPILE) Image dtbs scripts_gdb
编译buildroot
第一步要配置buildroot,这里根据toolchain的情况,在buildroot目录下面的configs建立如下配置文件 configs/arm_qemu_defconfig:
cd $(workspace)/src/buildroot
$cat configs/arm_qemu_defconfig
BR2_aarch64=y
BR2_TOOLCHAIN_EXTERNAL=y
BR2_TOOLCHAIN_EXTERNAL_PREINSTALLED=y
BR2_TOOLCHAIN_EXTERNAL_PATH="../../tools/arm-gnu-toolchain-13.2.Rel1-x86_64-aarch64-none-linux-gnu/"
# BR2_STRIP_strip is not set
BR2_OPTIMIZE_0=y
BR2_TARGET_GENERIC_HOSTNAME="Qemu"
BR2_TARGET_GENERIC_ISSUE="Welcome to Qemu"
BR2_PACKAGE_GDB=y
BR2_PACKAGE_GDB_SERVER=y
BR2_PACKAGE_GDB_DEBUGGER=y
BR2_PACKAGE_KEXEC=y
BR2_PACKAGE_KEXEC_ZLIB=y
BR2_PACKAGE_BINUTILS=y
BR2_PACKAGE_BINUTILS_TARGET=y
BR2_PACKAGE_TREE=y
BR2_PACKAGE_E2FSPROGS=y
BR2_PACKAGE_E2FSPROGS_DEBUGFS=y
BR2_PACKAGE_KVMTOOL=y
BR2_PACKAGE_MAKEDUMPFILE=y
BR2_TARGET_ROOTFS_EXT2=y
BR2_TARGET_ROOTFS_EXT2_3=y
BR2_TARGET_ROOTFS_EXT2_SIZE="256M"
第二步根据需求修改buildroot的配置,如下命令:
make -C $(SRC_DIR)/buildroot arm_qemu_defconfig
make -C $(SRC_DIR)/buildroot menuconfig # 这步可以修改配置文件
make -C $(SRC_DIR)/buildroot savedefconfig # 这步会把.config 复制到configs/arm_qemu_defconfig里面
第三步编译buildroot,如下命令:
make -C $(SRC_DIR)/buildroot arm_qemu_defconfig
make -C $(SRC_DIR)/buildroot
最终buildroot会生成$(workspace)/src/buildroot/output/images/rootfs.tar
重制rootfs
buildroot本身其实会生成ext2/3的rootfs格式,参考文件 $(workspace)/src/buildroot/output/images/,QEMU也是可以直接使用这些格式的。
但是有些时候可能需要往文件系统里面加一些文件来进行调试,所以下面的步骤就记录一下如何把rootfs重新打包成gdisk文件。
- 解压rootfs.tar
mkdir -p $(workspace)/rootfs/tmp/rootfs/ -p
cd rootfs/tmp/rootfs
tar -xvf $(workspace)/src/buildroot/output/images/rootfs.tar
- 修改rootfs
在目录 $(workspace)/src/rootfs/tmp/rootfs ,可以根据自己的需求来增加或者删减文件。
这一步不是必须的,是根据需求来决定的,如果没有改动需求,这一步可以跳过。
- 生成rootfs的partition文件
cd $(workspace)/src/rootfs/tmp
export BLOCK_SIZE=512
export SEC_PER_MB=$((1024*2))
export EXT3_SIZE_MB=512
export PART_START=$((1*SEC_PER_MB))
export EXT3_SIZE=$((EXT3_SIZE_MB*SEC_PER_MB))
dd if=/dev/zero of=ext3_part bs=$BLOCK_SIZE count=$EXT3_SIZE
mkdir -p mnt
mkfs.ext3 -F ext3_part
fuse-ext2 ext3_part mnt -o rw+
cp -rf rootfs/* mnt/
sync
fusermount -u mnt
rm -rf mnt
这里rootfs的最大大小是512M,如果需要调整大小,可以调整EXT3_SIZE_MB=512的值。
完成这一步,就生成了一个ext3的rootfs partition文件ext3_part,下一步会把这个分区文件放在磁盘映像文件的第一个分区。
- 使用gdisk生成rootfs的磁盘映像文件
cd $(workspace)/src/rootfs/tmp
export BLOCK_SIZE=512
export SEC_PER_MB=$((1024*2))
export EXT3_SIZE_MB=512
export PART_START=$((1*SEC_PER_MB))
export EXT3_SIZE=$((EXT3_SIZE_MB*SEC_PER_MB))
export IMG_BB=../rootfs.img
dd if=/dev/zero of=part_table bs=$BLOCK_SIZE count=$PART_START
cat part_table > $IMG_BB
cat ext3_part >> $IMG_BB
cat part_table >> $IMG_BB
(echo n; echo 1; echo $PART_START; echo +$((EXT3_SIZE)); echo 8300; echo w; echo y) | gdisk $IMG_BB
这里就完成了把上一步生成的ext3_part放在$(workspace)/src/rootfs/rootfs.img 的第一个分区。
文件$(workspace)/src/rootfs/rootfs.img就是最终的要传给QEMU的rootfs。
在GDB上调试Linux
如果要启动GDB调试整个software stack,在启动QEMU的时候需要加上: -S -s 这些参数,这样QEMU就会停在那里等待GDB开始调试。
启动脚本:
cd $(workspace)
src/qemu/build/qemu-system-aarch64 \
-M virt,gic-version=3,virtualization=on,type=virt,mte=on,secure=on \
-nographic \
-cpu max -nographic -m 16G \
-smp 16 \
-bios src/tf-a/build/qemu/debug/flash.bin \
-device loader,file=src/linux/arch/arm64/boot/Image,addr=0x40400000 \
-drive file=rootfs/rootfs.img,if=virtio,format=raw \
-S -s
新开一个窗口,使用aarch64-linux-gdb 或者gdb-multiarch 就可以开始调试啦:
src/linux$ cat gdb.ds
target remote :1234
add-symbol-file ../../src/tf-a/build/qemu/debug/bl1/bl1.elf
add-symbol-file ../../src/tf-a/build/qemu/debug/bl2/bl2.elf
add-symbol-file ../../src/tf-a/build/qemu/debug/bl31/bl31.elf
add-symbol-file vmlinux -o 0x7fffc0400000
add-symbol-file vmlinux
src/linux$ gdb-multiarch vmlinux
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04.2) 9.2
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from vmlinux...
(gdb) source gdb.ds
0x0000000000000000 in ?? ()
add symbol table from file "../../src/tf-a/build/qemu/debug/bl1/bl1.elf"
add symbol table from file "../../src/tf-a/build/qemu/debug/bl2/bl2.elf"
add symbol table from file "../../src/tf-a/build/qemu/debug/bl31/bl31.elf"
add symbol table from file "vmlinux" with all sections offset by 0x7fffc0400000
add symbol table from file "vmlinux"
(gdb) break bl31_main
Breakpoint 1 at 0xe0a23d0: file bl31/bl31_main.c, line 130.
(gdb) break _text
Breakpoint 2 at 0x40400000: _text. (2 locations)
(gdb) break start_kernel
Breakpoint 3 at 0x42010928: start_kernel. (2 locations)
(gdb) c
Continuing.
Thread 1 hit Breakpoint 1, bl31_main () at bl31/bl31_main.c:130
130 cm_manage_extensions_el3();
(gdb) bt
#0 bl31_main () at bl31/bl31_main.c:130
#1 0x000000000e0a00c0 in bl31_entrypoint () at bl31/aarch64/bl31_entrypoint.S:93
Backtrace stopped: previous frame identical to this frame (corrupt stack?)
(gdb) c
Continuing.
Thread 1 hit Breakpoint 2, _text () at arch/arm64/kernel/head.S:60
60 efi_signature_nop // special NOP to identity as PE/COFF executable
(gdb) x/4i $pc
=> 0x40400000 <_text>: ccmp x18, #0x0, #0xd, pl // pl = nfrst
0x40400004 <_text+4>: b 0x420000e0 <primary_entry>
0x40400008 <_text+8>: .inst 0x00000000 ; undefined
0x4040000c <_text+12>: .inst 0x00000000 ; undefined
(gdb) c
Continuing.
Thread 1 hit Breakpoint 3, start_kernel () at init/main.c:908
908 set_task_stack_end_magic(&init_task);
(gdb) l
903 void start_kernel(void)
904 {
905 char *command_line;
906 char *after_dashes;
907
908 set_task_stack_end_magic(&init_task);
909 smp_setup_processor_id();
910 debug_objects_early_init();
911 init_vmlinux_build_id();
至于为什么vmlinux 要被加两遍符号表,因为如果要调试在MMU没有enable的代码,就需要加:
add-symbol-file vmlinux -o 0x7fffc0400000
计算方法:
0x40400000 - 0xffff800080000000 = 0x7fffc0400000
0x40400000 为u-boot kernel加载的地址,而0xffff800080000000 是 _text在linux kernel符号表的位置:
grep " _text" src/linux/System.map
ffff800080000000 T _text
更多可以参考