news 2026/9/3 2:19:06

构建第一个ARM64裸机程序:从零实现入门案例

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
构建第一个ARM64裸机程序:从零实现入门案例

从零点亮第一行代码:手把手构建你的ARM64裸机程序

你有没有想过,当一块ARM64芯片上电的那一刻,它究竟是如何“醒”过来的?没有操作系统、没有C运行时库,甚至连栈都没有——它是怎么执行第一条指令的?

这正是裸机编程(Bare-metal Programming)的魅力所在。我们不依赖任何中间层,直接与硬件对话。这种能力,是开发Bootloader、固件、安全启动链乃至TrustZone可信执行环境的基础。

今天,我们就从零开始,亲手写出第一个能在ARM64平台上运行的裸机程序。不需要开发板也可以实践——用QEMU模拟器就能跑起来。整个过程只涉及三个核心文件:

  • startup.s:汇编写的启动代码
  • main.c:C语言主函数
  • linker.ld:链接脚本,决定内存布局

最终生成一个二进制镜像kernel.img,就像真正的内核一样被加载执行。


启动之前:ARM64是怎么“开机”的?

在写代码前,我们必须搞清楚一件事:CPU复位后,第一条指令从哪里来?

答案是:复位向量(Reset Vector)

ARM64处理器一上电,默认进入最高特权等级EL3(Exception Level 3),然后跳转到一个预设的物理地址去取第一条指令。这个地址由芯片厂商决定,常见的有:

  • 0x0000_0000
  • 0xFFFF_0000

比如树莓派3就使用0x0000_0000作为起始点。我们的目标就是确保在这个地址放上一条有效的跳转指令。

但注意:你不能指望系统自动帮你准备好一切。没有栈、没有初始化的.bss段、连中断都可能随机触发。所以,所有基础环境都得自己搭

这就引出了裸机程序的典型启动流程:

  1. 屏蔽中断(避免未准备就绪时被打断)
  2. 设置栈指针 SP
  3. 清零.bss段(保证全局变量初始为0)
  4. 跳转到 C 函数main()
  5. 死循环或后续引导操作

这些步骤必须用汇编语言完成,因为C语言需要运行时支持,而这些东西还没建立。


第一步:写下启动代码(startup.s)

这是整个程序的入口,也是唯一一段必须用汇编写的部分。

.section ".text.startup", "ax" .global _start _start: // 禁用中断和异常 msr daifset, #0xf // DAIF: Debug, SError, IRQ, FIQ 全部屏蔽 // 设置栈指针(假设RAM从0x80000开始) mov x0, #0x80000 mov sp, x0 // 清.bss段 mov x0, #0 // 要写入的值:0 ldr x1, =_bss_start // 获取.bss起始地址 ldr x2, =_bss_end // 获取.bss结束地址 sub x2, x2, x1 // 计算长度 cbz x2, skip_bss // 如果长度为0,跳过清零 mov x3, x1 // x3 指向当前要清零的位置 1: str x0, [x3], #8 // 存储0,并将x3增加8字节 subs x2, x2, #8 // 长度减8 b.ne 1b // 不为零则继续 skip_bss: // 调用C语言main函数 bl main // 防止返回——一旦main返回就陷入等待状态 hang: wfe // Wait For Event(节能指令) b hang // 循环等待

关键点解析:

  • daifset是什么?
    它是ARM64中控制中断屏蔽的寄存器位域:
  • D: Debug exceptions
  • A: SError (asynchronous abort)
  • I: IRQ interrupts
  • F: FIQ interrupts
    写入0xf表示全部关闭,防止早期中断导致崩溃。

  • 为什么不能直接调用main()
    因为C语言要求调用约定(calling convention)、栈已就位、静态存储区初始化完成。这些都要靠我们手动准备。

  • .bss段为什么要清零?
    C标准规定未初始化的全局变量默认为0。.bss就是用来存放这类变量的段,但它本身不占磁盘空间,只记录大小。所以我们必须在运行前手动将其内容置零。

  • wfevsnop
    wfe是“等待事件”指令,比空转更省电,适合嵌入式场景。如果平台不支持,可以换成nop


第二步:编写主逻辑(main.c)

现在环境准备好了,我们可以安心写C代码了。

// main.c void main(void) { // 这里可以做点有意义的事 // 比如:点亮LED、串口输出、内存测试等 while (1) { // 简单延时(实际项目应使用定时器) for (volatile int i = 0; i < 1000000; i++); // 假设GPIO控制寄存器地址为0x3F200000 // *(volatile unsigned*)0x3F200000 ^= (1 << 16); // 翻转LED } }

虽然目前只是个无限循环,但已经具备了扩展能力。只要你知道外设寄存器地址,就可以直接操作它们。

⚠️ 注意:volatile关键字不可少,否则编译器可能会优化掉空循环。


第三步:定义内存布局(linker.ld)

这是最容易被忽视却最关键的一环:链接脚本决定了你的代码放在哪、怎么加载、哪些符号可用

ENTRY(_start) MEMORY { RAM : ORIGIN = 0x80000, LENGTH = 64K } SECTIONS { . = ORIGIN(RAM); /* 代码段 */ .text : { *(.text.startup) /* 优先放置启动代码 */ *(.text) /* 其余函数 */ } > RAM /* 只读数据段 */ .rodata ALIGN(4) : { *(.rodata) } /* 可读写数据段(如果有初始化全局变量)*/ .data ALIGN(4) : { *(.data) } /* BSS段:未初始化全局变量 */ .bss ALIGN(4) : { _bss_start = .; *(.bss) _bss_end = .; } }

解读要点:

  • ENTRY(_start):告诉链接器程序入口是_start符号。
  • MEMORY:声明可用内存区域。这里我们假设有64KB RAM,起始于0x80000
  • . = ORIGIN(RAM):设置当前位置计数器,后续段从此处开始排布。
  • *(.text.startup)放在最前面:确保_start是整个.text段的第一个符号,这样烧录后CPU才能正确找到入口。
  • _bss_start_bss_end:这两个符号是在汇编中通过ldr x1, =_bss_start引用的,用于清零.bss段。

如果没有这个脚本,链接器会按默认规则分配地址,很可能导致程序无法运行。


编译与构建:Makefile自动化

接下来我们需要一个简单的构建流程,把源码变成可执行的二进制镜像。

# 工具链(推荐安装 aarch64-none-elf-gcc) CC = aarch64-none-elf-gcc AS = aarch64-none-elf-as LD = aarch64-none-elf-ld OBJCOPY = aarch64-none-elf-objcopy # 源文件 C_SOURCES = main.c ASM_SOURCES = startup.s # 输出目标 TARGET_ELF = kernel.elf TARGET_BIN = kernel.img # 编译选项 CFLAGS = -nostdlib -nostartfiles -ffreestanding -O2 LDFLAGS = -T linker.ld # 默认目标 all: $(TARGET_BIN) # 编译C文件 %.o: %.c $(CC) $(CFLAGS) -c $< -o $@ # 汇编.s文件 %.o: %.s $(AS) -g -o $@ $< # 链接成ELF $(TARGET_ELF): $(C_SOURCES:.c=.o) $(ASM_SOURCES:.s=.o) $(LD) $(LDFLAGS) -o $@ $^ # 转换为纯二进制镜像 $(TARGET_BIN): $(TARGET_ELF) $(OBJCOPY) -O binary $< $@ # 清理 clean: rm -f *.o $(TARGET_ELF) $(TARGET_BIN) .PHONY: all clean

运行make后,你会得到kernel.img—— 一个可以直接加载到内存中的原始二进制文件。


如何验证?用QEMU快速测试

别急着买开发板,先用模拟器试试!

安装 QEMU(Ubuntu/Debian):

sudo apt install qemu-system-aarch64

启动模拟(以virt机器为例):

qemu-system-aarch64 \ -machine virt \ -cpu cortex-a57 \ -nographic \ -semihosting \ -kernel kernel.img \ -append "console=ttyAMA0"

参数说明:

  • -machine virt:使用虚拟开发板,适合裸机实验
  • -cpu cortex-a57:指定ARM64 CPU型号
  • -nographic:禁用图形界面,使用终端输出
  • -semihosting:启用半主机模式,允许程序调用宿主机I/O(可用于调试打印)
  • -kernel:直接加载二进制镜像到默认加载地址

如果你在main()中加入了串口输出或半主机调用,就能看到效果了。


常见坑点与调试技巧

裸机开发最怕“程序跑飞”,下面是一些高频问题及应对方法:

问题现象可能原因解决方案
程序没反应栈未设置或地址错误检查sp是否指向有效RAM
全局变量非零.bss未清零确保汇编中正确引用_bss_start_bss_end
链接报错 undefined reference入口符号不一致检查ENTRY(_start).global _start是否匹配
中断频繁触发DAIF未屏蔽开头务必加msr daifset, #0xf
QEMU提示无法加载镜像格式不对必须是 raw binary,不能是 ELF

调试建议:

  1. 使用objdump查看反汇编
    bash aarch64-none-elf-objdump -D kernel.elf
    看看_start是否确实是第一条指令。

  2. 添加调试标记输出
    在关键位置写特定值到某个内存地址,用GDB观察:
    c *(volatile uint32_t*)0x800FF000 = 0xDEADBEEF;

  3. 结合 GDB 调试
    bash qemu-system-aarch64 ... -s -S # -S:暂停启动;-s:监听localhost:1234 aarch64-none-elf-gdb kernel.elf (gdb) target remote :1234 (gdb) continue


进阶方向:下一步你能做什么?

完成了第一个裸机程序,只是一个开始。你可以沿着以下路径深入探索:

✅ 移植到真实开发板

  • 树莓派3/4:配置GPIO点亮LED
  • NXP LS1028A:学习多阶段引导流程
  • Rockchip RK3399:尝试TrustZone安全世界切换

✅ 实现基本驱动

  • UART串口输出日志
  • 定时器实现精确延时
  • GPIO控制外设(按键、蜂鸣器)

✅ 构建小型操作系统雏形

  • 实现简易任务调度器
  • 添加中断处理框架(IRQ handler)
  • 初始化MMU开启虚拟内存

✅ 探索安全启动机制

  • 使用ARM TrustZone创建安全世界
  • 实现签名验证 + 安全固件加载
  • 结合TF-A(Trusted Firmware-A)理解BL系列引导流程

写在最后:为什么我们要学裸机编程?

你说,现在都有Linux、Zephyr、FreeRTOS了,还用得着从零写裸机吗?

当然需要。

就像飞行员必须学会滑翔伞才能驾驭喷气式飞机一样,只有理解最底层的启动机制,你才能真正掌控系统的行为

当你面对一个“启动失败”的开发板时,别人还在查资料,你已经能通过JTAG连接、查看EL级别、检查向量表偏移,迅速定位问题是出在电源管理还是异常降级配置。

这才是嵌入式工程师的核心竞争力。

而这一切,始于你亲手写出的那个kernel.img

现在,轮到你了:打开编辑器,新建startup.s,写下第一行.global _start—— 属于你的ARM64旅程,就此启航。

如果你在构建过程中遇到问题,欢迎留言交流。我们一起debug,一起点亮第一盏灯。

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

告别B站水印困扰!手把手教你用哔哩下载姬轻松下载纯净视频

告别B站水印困扰&#xff01;手把手教你用哔哩下载姬轻松下载纯净视频 【免费下载链接】downkyi 哔哩下载姬downkyi&#xff0c;哔哩哔哩网站视频下载工具&#xff0c;支持批量下载&#xff0c;支持8K、HDR、杜比视界&#xff0c;提供工具箱&#xff08;音视频提取、去水印等&a…

作者头像 李华
网站建设 2026/9/3 0:03:22

Unsloth实战教程:基于强化学习优化Qwen对话系统

Unsloth实战教程&#xff1a;基于强化学习优化Qwen对话系统 1. unsloth 简介 Unsloth 是一个开源的大型语言模型&#xff08;LLM&#xff09;微调与强化学习训练框架&#xff0c;旨在显著提升模型训练效率并降低资源消耗。其核心目标是让人工智能技术更加准确、高效且易于获取…

作者头像 李华
网站建设 2026/9/2 22:09:02

Wan2.2-T2V-A5B硬件选型指南:RTX 3060够用吗?实测告诉你

Wan2.2-T2V-A5B硬件选型指南&#xff1a;RTX 3060够用吗&#xff1f;实测告诉你 1. 背景与问题提出 随着AIGC技术的快速发展&#xff0c;文本生成视频&#xff08;Text-to-Video, T2V&#xff09;正逐步从实验室走向实际内容生产场景。通义万相推出的Wan2.2-T2V-A5B模型作为一…

作者头像 李华
网站建设 2026/9/2 22:09:07

Zprotect加壳工具汉化版

链接&#xff1a;https://pan.quark.cn/s/d5fd83c7ac41Zprotect 是一款优秀的应用程序加壳软件&#xff0c;拥有良好的稳定性和兼容&#xff0c;全面保护您的软件不被破解&#xff01;不被反编译&#xff0c;目前发布的为中文版本&#xff0c;这里就不多做介绍了&#xff0c;大…

作者头像 李华
网站建设 2026/9/3 1:09:14

RexUniNLU案例分享:新闻事件监测系统

RexUniNLU案例分享&#xff1a;新闻事件监测系统 1. 引言 在当今信息爆炸的时代&#xff0c;新闻媒体每天产生海量文本数据。如何从这些非结构化内容中快速识别关键事件、提取核心要素并进行语义理解&#xff0c;成为舆情监控、金融情报、公共安全等领域的共性需求。传统方法…

作者头像 李华
网站建设 2026/8/28 21:12:46

Hunyuan-MT1.8B如何监控?GPU利用率观测部署教程

Hunyuan-MT1.8B如何监控&#xff1f;GPU利用率观测部署教程 1. 引言 1.1 业务场景描述 随着企业级机器翻译需求的不断增长&#xff0c;高效、稳定且可监控的大模型部署成为关键。Tencent-Hunyuan/HY-MT1.5-1.8B 是一款基于 Transformer 架构构建的高性能翻译模型&#xff0c…

作者头像 李华