为什么 ARM 内核对 ELF 和二进制文件的行为不同

Why ARM cores behaving differently with an ELF and binary file

我正在 ARM 上进行裸机开发并在 QEMU 上模拟 Raspi 3。下面是我的最小汇编代码:

.section ".text.boot"

.global _start
_start:
1:  wfe
    b 1b

下面是我的链接描述文件:

SECTIONS
{
    . = 0x80000;
    .text : {*(.text.boot)}

    /DISCARD/ : { *(.comment) *(.gnu*) *(.note*) *(.eh_frame*) }
}

下面是我的 Makefile :

CC = /opt/gcc-arm-10.3-2021.07-x86_64-aarch64-none-elf/bin/aarch64-none-elf
CFLAGS = -Wall -O2 -ffreestanding -nostdinc -nostartfiles -nostdlib -g

all: clean kernel8.img

start.o: start.S
    ${CC}-gcc $(CFLAGS) -c start.S -o start.o

kernel8.img: start.o
    ${CC}-ld -nostdlib start.o -T link.ld -o kernel8.elf
    ${CC}-objcopy -O binary kernel8.elf kernel8.img

clean:
    rm kernel8.elf kernel8.img *.o >/dev/null 2>/dev/null || true

现在,我正在从一个终端加载我的 kernel8.elf,如下所示:

$ /opt/qemu-6.2.0/build/qemu-system-aarch64 -M raspi3b -kernel kernel8.elf -display none -S -s

我从另一个终端连接我的 gdb :

$ /opt/gcc-arm-10.3-2021.07-x86_64-aarch64-none-elf/bin/aarch64-none-elf-gdb ./kernel8.elf -ex 'target remote localhost:1234' -ex 'break *0x80000' -ex 'continue'
(gdb) info threads
  Id   Target Id                    Frame
  1    Thread 1.1 (CPU#0 [running]) _start () at start.S:6
  2    Thread 1.2 (CPU#1 [running]) _start () at start.S:5
* 3    Thread 1.3 (CPU#2 [running]) _start () at start.S:5
  4    Thread 1.4 (CPU#3 [running]) _start () at start.S:5

在这种情况下我的核心没问题,因为所有 4 个核心都是 运行 我的汇编代码。在 continue 时,核心随机遇到断点,这是完美的。

但是,如果我使用 kernel8.img(objcopy 二进制输出)而不是 kernel8.elf,我看到只有核心 1 是 运行 我的程序集,但其他 3 个核心似乎被卡住。 continue 只有 Core 1 每次都重复命中断点。

(gdb) info threads
  Id   Target Id                    Frame
* 1    Thread 1.1 (CPU#0 [running]) _start () at start.S:5
  2    Thread 1.2 (CPU#1 [running]) 0x0000000000000300 in ?? ()
  3    Thread 1.3 (CPU#2 [running]) 0x0000000000000300 in ?? ()
  4    Thread 1.4 (CPU#3 [running]) 0x0000000000000300 in ?? ()

我在其他3个核心上试过set scheduler-locking oncontinue,但它们似乎卡住了。

为什么 kernel8.img 不像 kernel8.elf 那样工作?我希望所有 ARM 内核在复位时都是 运行 相同的代码(正如 kernel8.elf 所发生的那样)但它不会发生在 kernel8.img.

QEMU -kernel 选项根据它是否是 ELF 文件来处理它加载的文件。

如果是ELF文件,则按照ELF文件说的加载方式加载,从ELF入口点开始执行。如果不是 ELF 文件,则假定为 Linux 内核,并以 Linux 内核的启动协议要求的方式启动。

特别是对于多核板,如果 -kernel 获取 ELF 文件,它会在入口点同时启动所有内核。如果它得到一个非 ELF 文件,那么它会做任何硬件应该做的事情来加载 Linux 内核。对于 raspi3b,这意味着模拟“辅助核心坐在一个循环中等待主核心通过写入 'mailbox' 地址来释放它们的固件行为。这就是您在 gdb 中看到的行为——0x300 地址核心 1-3 所在的位置在“循环等待”代码中。

一般来说,除非您的来宾代码是 Linux 内核或希望以与 Linux 内核相同的方式启动,否则不要使用 -kernel 选项加载它. -kernel 特别是“尝试做 Linux 内核想要的事情”,并且它也往往有很多遗留的“这对某些人来说似乎是有用的东西”的行为,这些行为在不同的董事会或不同的客户之间有所不同 CPU 架构。如果您想对“裸机”工作进行完全手动控制,“generic loader”是加载 ELF 文件的好方法。

有关用于加载访客代码的各种 QEMU 选项的详细信息,请参阅 this answer