跳转至

Lab 01:QEMU ARM64 裸机启动与 UART

目标是在 QEMU virt 机器的 RAM 起始地址 0x40000000 运行最小 AArch64 镜像,通过 PL011 UART0(0x09000000)打印字符。QEMU -kernel 会把裸机镜像放到约定入口,本实验不模拟真实 BootROM。

依赖 aarch64-none-elf-gcc 或兼容裸机工具链、qemu-system-aarch64

make CROSS_COMPILE=aarch64-none-elf-
qemu-system-aarch64 -M virt -cpu cortex-a53 -nographic -kernel build/hello.bin

预期输出为 hello from qemu arm64。随后可修改 main.c,读取 PL011 Flag Register,解释 TXFF 为何必须轮询。

文件结构

lab01-qemu-arm64-baremetal-boot/
├── Makefile       # 交叉编译并生成 ELF/裸二进制镜像
├── linker.ld      # 指定入口地址与各段布局
├── start.S        # 设置栈并进入 C 入口
└── main.c         # PL011 UART 轮询发送

以下内容与目录中的独立源码文件保持一致;源码文件继续用于实际构建,README 中的副本用于讲解和在线阅读。

完整源码

Makefile

CROSS_COMPILE ?= aarch64-none-elf-
CC := $(CROSS_COMPILE)gcc
OBJCOPY := $(CROSS_COMPILE)objcopy
CFLAGS := -ffreestanding -fno-stack-protector -nostdlib -nostartfiles -Wall -Wextra -O2

all: build/hello.bin

build/hello.elf: start.S main.c linker.ld
    mkdir -p build
    $(CC) $(CFLAGS) -T linker.ld start.S main.c -o $@

build/hello.bin: build/hello.elf
    $(OBJCOPY) -O binary $< $@

clean:
    rm -rf build

.PHONY: all clean

构建首先生成带符号和段信息的 hello.elf,随后通过 objcopy 提取 QEMU 可以直接加载的裸二进制 hello.bin

linker.ld

ENTRY(_start)
SECTIONS {
    . = 0x40000000;
    .text : { KEEP(*(.text.boot)) *(.text*) }
    .rodata : { *(.rodata*) }
    .data : { *(.data*) }
    .bss (NOLOAD) : { *(.bss*) *(COMMON) }
}

ENTRY(_start) 指定程序入口;KEEP(*(.text.boot)) 防止启动段被链接器垃圾回收;.bss (NOLOAD) 表示该段只占运行地址空间,不需要写入裸镜像。

start.S

.section .text.boot
.global _start
_start:
    ldr x0, =stack_top
    mov sp, x0
    bl kernel_main
1:  wfe
    b 1b

.section .bss.stack, "aw", %nobits
.align 12
stack_bottom:
    .skip 16384
stack_top:

启动代码把栈指针设置到预留栈顶,然后调用 kernel_main()。如果 C 入口意外返回,CPU 会进入 WFE 等待循环,不再执行未知地址。

main.c

#include <stdint.h>

#define UART_BASE 0x09000000UL
#define UART_DR   (*(volatile uint32_t *)(UART_BASE + 0x00))
#define UART_FR   (*(volatile uint32_t *)(UART_BASE + 0x18))
#define UART_FR_TXFF (1u << 5)

static void uart_putc(char c)
{
    while (UART_FR & UART_FR_TXFF) { }
    UART_DR = (uint32_t)c;
}

void kernel_main(void)
{
    const char *s = "hello from qemu arm64\n";
    while (*s)
        uart_putc(*s++);
}

volatile 防止编译器缓存或消除 MMIO 访问。UART_FR_TXFF 为发送 FIFO 满标志;只有该位清零后,软件才能向 UART_DR 写入下一个字符。