news 2026/6/10 2:04:32

构建第一个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/6/9 20:11:06

为什么推荐gpt-oss-20b-WEBUI做角色微调?答案在这

为什么推荐gpt-oss-20b-WEBUI做角色微调&#xff1f;答案在这 1. 背景与需求&#xff1a;从单向消费到沉浸式互动 在当前数字内容高速发展的背景下&#xff0c;影视、动漫和游戏产业不断产出具有鲜明个性的虚拟角色。用户不再满足于被动观看或体验剧情&#xff0c;而是渴望与…

作者头像 李华
网站建设 2026/6/9 20:03:28

通义千问2.5-7B-Instruct算法设计:AI辅助编程实践

通义千问2.5-7B-Instruct算法设计&#xff1a;AI辅助编程实践 1. 引言 1.1 技术背景与行业需求 随着大模型在自然语言理解和代码生成领域的持续突破&#xff0c;AI辅助编程已成为软件开发效率提升的关键路径。从GitHub Copilot的广泛应用到各类本地化代码助手的兴起&#xf…

作者头像 李华
网站建设 2026/6/9 22:04:56

AT89C51控制蜂鸣器:proteus仿真实战案例

AT89C51驱动蜂鸣器实战&#xff1a;从代码到声音的Proteus全流程仿真你有没有遇到过这样的情况——写好了单片机程序&#xff0c;烧进去却发现蜂鸣器不响&#xff1f;是硬件接错了&#xff1f;还是延时算偏了&#xff1f;又或者频率根本不对&#xff1f;反复下载、调试、换芯片…

作者头像 李华
网站建设 2026/6/9 22:06:17

不会代码怎么用ASR模型?Seaco Paraformer图形化界面1小时上手

不会代码怎么用ASR模型&#xff1f;Seaco Paraformer图形化界面1小时上手 你是不是也遇到过这样的情况&#xff1a;作为市场专员&#xff0c;手头有一堆用户访谈录音&#xff0c;想快速转成文字做分析&#xff0c;但网上搜到的语音识别工具不是要写代码就是操作复杂&#xff0…

作者头像 李华
网站建设 2026/6/9 21:00:43

Z-Image-Turbo快速上手:8步生成真实感图像保姆级教程

Z-Image-Turbo快速上手&#xff1a;8步生成真实感图像保姆级教程 Z-Image-Turbo是阿里巴巴通义实验室开源的高效AI图像生成模型&#xff0c;作为Z-Image的蒸馏版本&#xff0c;它在保持高质量图像输出的同时大幅提升了推理速度。该模型仅需8个去噪步骤即可生成具备照片级真实感…

作者头像 李华
网站建设 2026/6/9 22:06:59

Speech Seaco Paraformer ASR GPU配置推荐:最具性价比算力方案

Speech Seaco Paraformer ASR GPU配置推荐&#xff1a;最具性价比算力方案 1. 背景与技术选型动机 随着语音识别技术在会议记录、访谈转写、智能客服等场景的广泛应用&#xff0c;本地化部署高性能中文ASR系统的需求日益增长。Speech Seaco Paraformer 是基于阿里云FunASR项目…

作者头像 李华