编译qemu-arm
公司的系统中没有这个软件,设置外部源也下载不了,只能自己编译qenu-arm。
1. 安装编译依赖
sudo dnf install git gcc make ninja-build glib2-devel pixman-devel zlib-devel python3
2. 克隆并编译(仅构建 ARM 目标,大幅缩短编译时间)
gitclone https://gitlab.com/qemu-project/qemu.gitcdqemu# 1. 告诉系统优先去 Conda 环境里找库文件exportPKG_CONFIG_PATH=$CONDA_PREFIX/lib/pkgconfig:$PKG_CONFIG_PATH# 2. 回到 QEMU 源码目录,彻底清理旧的编译缓存cd~/code/gitlab/qemurm-rfbuildmkdirbuild&&cdbuild# 3. 强制指定编译器,并带上更新后的环境变量重新配置CC=$CONDA_PREFIX/bin/gccCXX=$CONDA_PREFIX/bin/g++../configure --target-list=aarch64-softmmu,arm-softmmu --enable-slirpmake-j$(nproc)3. 编译产物位于 build/ 目录,无需安装即可直接使用
./build/qemu-system-aarch64 --version
编译时是gcc版本太低。没有办法,无法安装源里的高版本gcc,只能再编译一个高版本的gcc
—2026-05-24 通过conda安装gcc的问题解决了。conda设置镜像源有坑,除了设置镜像源,还要修改默认通道。
conda里的gcc 包括arm和x86的,是15.x
下载编译gcc
下载 GCC 12.4.0(最新 12.x 稳定版)
清华 TUNA 镜像(仅当 GNU FTP 不通时使用)
wget https://mirrors.tuna.tsinghua.edu.cn/gnu/gcc/gcc-12.4.0/gcc-12.4.0.tar.xz
tar xf gcc-12.4.0.tar.xz
cd gcc-12.4.0
【关键】自动下载 gmp, mpfr, mpc 三大数学库依赖
该脚本会从 GNU 内部可信源拉取,通常不受企业代理限制
./contrib/download_prerequisites
创建独立构建目录(严禁在源码目录内直接 configure)
mkdir build && cd build
配置:安装到当前 conda 环境,仅启用 C/C++,禁用多架构以加速编译
../configure\--prefix=$CONDA_PREFIX\--enable-languages=c,c++\--disable-multilib\--disable-bootstrap并行编译(根据服务器核心数调整 -j 参数)
make -j$(nproc)
安装到 $CONDA_PREFIX
make install
$ gcc --version
gcc (GCC) 12.4.0
编写一个最简的OS
编写一个最简单的 ARM OS 是学习操作系统原理(如启动流程、内存管理、异常处理)的绝佳途径。对于初学者,强烈建议使用 QEMU 模拟 ARM64 (AArch64) 架构,而不是直接操作物理开发板。QEMU 提供了完美的调试环境,且 AArch64 的文档比老旧的 ARM32 更清晰、更现代化。
以下是一个“最小可运行”的 ARM64 OS 学习路线与调试指南。
🛠️ 1. 环境准备
你需要一套交叉编译工具链和 QEMU 模拟器:
# Ubuntu/Debiansudoaptinstallgcc-aarch64-linux-gnu binutils-aarch64-linux-gnu qemu-system-arm gdb-multiarchmake--这个armgcc,最后通过conda下载的。conda还有演进版 pixi,推荐用这个代替conda# macOS (Homebrew)brewinstallaarch64-elf-gcc aarch64-elf-binutils qemu gdb💻 2. 最小 OS 代码结构
一个能跑起来的最小 OS 只需要两个文件:一个汇编启动文件和一个 C 语言主函数。
boot.S- 引导与初始化
OS 启动时 CPU 处于特定状态,必须用汇编完成最基础的初始化才能跳转到 C 代码。
.section ".text.boot" .global _start _start: // 1. 检查处理器ID,确保只有核心0执行后续代码 mrs x0, mpidr_el1 and x0, x0, #0xFF cbz x0, primary_core // 非核心0进入死循环 b . primary_core: // 2. 设置栈指针 (SP),C语言运行必须有栈 // 假设我们将栈顶设置在内存 0x48000000 处 ldr x0, =0x48000000 mov sp, x0 // 3. 清零 BSS 段(未初始化全局变量) ldr x0, =__bss_start ldr x1, =__bss_end clear_bss: cmp x0, x1 b.ge jump_to_c stp xzr, xzr, [x0], #16 b clear_bss jump_to_c: // 4. 跳转到 C 语言入口 bl kernel_main // 如果 kernel_main 返回,进入死循环 b .kernel.c- 内核主逻辑
最简单的内核:向 UART 串口打印字符,证明 OS 已接管硬件。
// PL011 UART 基地址 (QEMU virt 机器默认)#defineUART_BASE0x09000000voiduart_putc(charc){volatileunsignedint*uart_dr=(unsignedint*)(UART_BASE+0x00);*uart_dr=c;}voiduart_puts(constchar*s){while(*s)uart_putc(*s++);}voidkernel_main(void){uart_puts("Hello, ARM64 OS!\n");// 简单的异常测试:触发一个未定义指令异常// asm volatile(".word 0x00000000");while(1){// 内核空闲循环asmvolatile("wfi");}}linker.ld- 链接脚本
告诉链接器如何将代码和数据放置到正确的内存地址。这是 OS 开发中最容易出错的地方。
ENTRY(_start) SECTIONS { /* QEMU virt 机器的 RAM 起始地址 */ . = 0x40080000; .text : { *(.text.boot) *(.text*) } .rodata : { *(.rodata*) } .data : { *(.data*) } . = ALIGN(16); __bss_start = .; .bss : { *(.bss*) *(COMMON) } __bss_end = .; }Makefile- 构建与运行
# ========================================== # 交叉编译工具链配置 # ========================================== CROSS_COMPILE = aarch64-linux-gnu- CC = $(CROSS_COMPILE)gcc LD = $(CROSS_COMPILE)ld OBJCOPY = $(CROSS_COMPILE)objcopy # ========================================== # 编译与链接选项 # ========================================== CFLAGS = -Wall -O2 -ffreestanding -nostdinc -nostdlib -mcpu=cortex-a72 LDFLAGS = -T linker.ld -nostdlib # ========================================== # 目标文件定义 # ========================================== TARGET = kernel.img SRCS = boot.S kernel.c # 自动将 .S 和 .c 后缀替换为 .o OBJS = $(SRCS:.S=.o) OBJS = $(OBJS:.c=.o) # ========================================== # 默认目标:执行 make 时默认构建 kernel.img # ========================================== all: $(TARGET) # ========================================== # 编译规则:将 .S 和 .c 文件编译为 .o 目标文件 # ========================================== %.o: %.S $(CC) $(CFLAGS) -c $*.S -o $*.o %.o: %.c $(CC) $(CFLAGS) -c $*.c -o $*.o # ========================================== # 链接规则:将所有 .o 目标文件链接成 kernel.bin # ========================================== kernel.bin: $(OBJS) $(LD) $(LDFLAGS) $(OBJS) -o kernel.bin # ========================================== # 格式转换:将 ELF 格式的 kernel.bin 转为纯二进制 kernel.img # ========================================== kernel.img: kernel.bin $(OBJCOPY) -O binary kernel.bin kernel.img # ========================================== # 运行与调试目标 # ========================================== run: $(TARGET) qemu-system-aarch64 -M virt -cpu cortex-a72 -nographic -kernel kernel.img debug: $(TARGET) qemu-system-aarch64 -M virt -cpu cortex-a72 -nographic -kernel kernel.img -s -S & gdb-multiarch -ex "target remote localhost:1234" -ex "symbol-file kernel.bin" # ========================================== # 辅助目标 # ========================================== clean: rm -f $(OBJS) kernel.bin $(TARGET) .PHONY: all run debug clean help help: @echo "可用的 make 命令:" @echo " make - 编译生成 kernel.img(默认)" @echo " make run - 使用 QEMU 运行内核" @echo " make debug - 启动 QEMU 并进入 GDB 调试模式" @echo " make clean - 清理所有编译生成的文件"🔍 3. 如何高效调试
OS 开发没有printf可用(在你自己实现之前),必须依赖调试器。
GDB + QEMU 联合调试
运行make debug后,GDB 会连接到 QEMU 的内置 GDB Server。你可以像调试普通程序一样调试 OS:
| GDB 命令 | 作用 | OS 调试特殊用途 |
|---|---|---|
b kernel_main | 在 C 入口设断点 | 验证汇编跳转是否成功 |
info registers | 查看所有寄存器 | 检查 SP、ELR_EL1、ESR_EL1 |
x/16xb 0x40080000 | 查看内存十六进制 | 验证代码是否正确加载到指定地址 |
set $sp=0x48000000 | 手动修改寄存器 | 修复因栈设置错误导致的崩溃 |
stepi/si | 单步执行一条汇编 | 跟踪启动阶段每一条指令 |
关键调试技巧
- 永远先调通串口输出:在写任何复杂功能前,先让
uart_putc工作。它是你后续所有调试信息的唯一出口。 - 善用 QEMU 的
-d参数:当 OS 莫名重启或挂起时,使用qemu-system-aarch64 -d int,unimp,guest_errors -D qemu.log ...,QEMU 会将中断、未实现指令、客户机错误记录到日志文件中,这比 GDB 更能定位底层问题。 - 检查异常级别 (EL):AArch64 有 EL0-EL3 四个特权级。QEMU virt 默认从 EL1 启动。如果你的代码试图访问 EL2/EL3 寄存器,会触发异常。用
mrs x0, CurrentEL确认当前特权级。
📚 4. 推荐学习路径与资源
不要试图从零发明一切,站在巨人肩膀上:
- 入门教程:搜索“Writing an Operating System for Raspberry Pi 3”(GitHub: isometimes/rpi3-os-tutorial)。虽然针对树莓派,但前几章完全适用于 QEMU ARM64,且讲解极其细致。
- 进阶参考:“Operating Systems: From 0 to 1”或 MIT 6.S081 (RISC-V,但概念完全相通)。
- 官方文档:ARM Architecture Reference Manual (DDI0487)。不需要全读,重点看System Registers和Exception Model章节。
- 下一步演进:在跑通上述最小 OS 后,按此顺序添加功能:
- 实现完整的异常向量表 (
vbar_el1) - 实现简单的内存分配器 (buddy system)
- 实现进程/线程上下文切换
- 实现一个简单的 Shell
- 实现完整的异常向量表 (
⚠️重要提醒
OS 开发中 90% 的 Bug 来自链接脚本地址错误和对齐问题。当你遇到无法解释的崩溃时,第一反应应该是检查linker.ld中的地址是否与 QEMU 的内存映射一致,以及数据结构是否满足 ARM 的对齐要求。
conda修复好了
什么意思,我们设置了镜像源,还要修改默认通道吗
● 是的,需要理解 conda 的通道机制:
Conda 通道的工作原理
当你设置 channels: 时,你只是添加了额外的通道,但 defaults 这个特殊通道仍然指向官方源。
你的配置:
channels:
- https://mirrors.ustc.edu.cn/anaconda/cloud/conda-forge/
- defaults # 这个是特殊的!
defaults 通道的实际含义:
- defaults 不是一个 URL,而是一个别名
- 它默认展开为:
- https://repo.anaconda.com/pkgs/main/
- https://repo.anaconda.com/pkgs/r/
所以即使你设置了镜像源,只要 defaults 还在,conda 仍会尝试访问 repo.anaconda.com。
解决方案
有两种方式:
方案 1:完全禁用 defaults(推荐)
channels:
- https://mirrors.ustc.edu.cn/anaconda/cloud/conda-forge/
只从 conda-forge 安装包。
方案 2:重写 defaults 指向镜像
default_channels:
- https://mirrors.ustc.edu.cn/anaconda/pkgs/main/
- https://mirrors.ustc.edu.cn/anaconda/pkgs/r/
channels:
- https://mirrors.ustc.edu.cn/anaconda/cloud/conda-forge/
- defaults
为什么会这样设计?
这是 Anaconda 的设计问题。defaults 是硬编码的特殊通道,不像其他通道可以直接替换 URL。很多用户都不知道这个坑。
简单记忆:用国内镜像时,要么去掉 defaults,要么必须显式重写 default_channels。