news 2026/5/25 3:05:01

qemu和gcc编译

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
qemu和gcc编译

编译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单步执行一条汇编跟踪启动阶段每一条指令
关键调试技巧
  1. 永远先调通串口输出:在写任何复杂功能前,先让uart_putc工作。它是你后续所有调试信息的唯一出口。
  2. 善用 QEMU 的-d参数:当 OS 莫名重启或挂起时,使用qemu-system-aarch64 -d int,unimp,guest_errors -D qemu.log ...,QEMU 会将中断、未实现指令、客户机错误记录到日志文件中,这比 GDB 更能定位底层问题。
  3. 检查异常级别 (EL):AArch64 有 EL0-EL3 四个特权级。QEMU virt 默认从 EL1 启动。如果你的代码试图访问 EL2/EL3 寄存器,会触发异常。用mrs x0, CurrentEL确认当前特权级。

📚 4. 推荐学习路径与资源

不要试图从零发明一切,站在巨人肩膀上:

  1. 入门教程:搜索“Writing an Operating System for Raspberry Pi 3”(GitHub: isometimes/rpi3-os-tutorial)。虽然针对树莓派,但前几章完全适用于 QEMU ARM64,且讲解极其细致。
  2. 进阶参考“Operating Systems: From 0 to 1”或 MIT 6.S081 (RISC-V,但概念完全相通)。
  3. 官方文档:ARM Architecture Reference Manual (DDI0487)。不需要全读,重点看System RegistersException Model章节。
  4. 下一步演进:在跑通上述最小 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。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/25 2:57:22

Titanic数据集分析避坑指南:新手常犯的3个错误及如何修正

Titanic数据集分析避坑指南:新手常犯的3个错误及如何修正泰坦尼克号数据集是机器学习领域的"Hello World",但看似简单的数据背后藏着无数陷阱。许多初学者在Kaggle等平台提交分析时,常常陷入三个典型误区:用均值粗暴填充…

作者头像 李华
网站建设 2026/5/25 2:47:07

保姆级教程:用Python从零实现Occupancy Grid Map,附ROS实战避坑指南

从零构建Occupancy Grid Map:Python实现与ROS避坑实战移动机器人感知环境的核心工具之一便是占用栅格地图(Occupancy Grid Map)。这种将环境划分为均匀栅格并计算每个栅格被障碍物占据概率的方法,已成为机器人导航与路径规划的基石…

作者头像 李华