← 返回信息流
AI 资讯Hacker News·3 天前

编写可移植的ARM64汇编代码指南

原标题:Writing Portable ARM64 Assembly

速览

本文介绍了编写可移植ARM64汇编代码的关键技术与最佳实践。重点在于处理不同实现间的差异,确保代码在多种硬件上稳定运行。这对于底层系统开发和跨平台软件移植具有重要意义。

AI 深度解读

编写可移植的 ARM64 汇编:跨越 Apple 与 Linux/BSD 的鸿沟

背景

随着 Apple 基于 ARM 架构的电脑(如 M 系列芯片 Mac)日益普及,一个不太理想的副作用也随之显现:针对 64 位 ARM 指令集架构(ISA)的汇编代码变得不再具备可移植性。

许多开发者为了在 Apple 的 ARM 设备上加速程序运行,专门编写了针对该平台优化的汇编代码片段,却忽略了其他运行 Linux 或 BSD 系统的 64 位 ARM 设备(如单板计算机 SBC 和服务器)。这导致代码被锁定在 Apple 的工具链环境中,无法在其他 UNIX-like 系统中复用。

好消息是,编写既能运行在 Apple 计算机上,又能兼容其他非 Darwin 操作系统(如 Linux、BSD)的 64 位 ARM 汇编代码其实非常容易。关键在于理解 Mach-O 与 ELF 二进制格式 ABI(应用程序二进制接口)之间的差异,并避免使用 Apple 特有的语法扩展。遵循本文的指导,你可以编写出在 Apple 工具链、官方 ARM 汇编工具链以及 GNU 工具链之间均可移植的汇编代码。

核心内容

ELF 与 Mach-O ABI 的主要差异

现代 UNIX 系统(包括基于 Linux 的系统)主要使用 ELF(Executable and Linkable Format)二进制格式。出于历史原因,Apple 在 Darwin 操作系统中使用了 Mach-O 格式。需要注意的是,这并非 Apple 强制要求的技术限制;事实上,Darwin、MkLinux 和 OSF/1 所基于的内核 OSFMK 完全支持 ELF 二进制文件,Apple 只是选择了 Mach-O 作为其默认格式。

在编写针对 Darwin 的汇编代码(或更广泛地说,进行代码链接)时,开发者需要关注以下两个主要差异:

  1. 符号前缀下划线: 在 Darwin 上,所有符号(函数、变量等)名称前都需要添加一个下划线 _

    • 示例:如果你在 C 语言中声明了一个函数:
      extern void unmask(const char *payload, const char *mask, size_t len);
      
    • 在你的汇编代码中,该函数必须定义为 _unmask,而不是 unmask
  2. 符号类型指令的差异: ELF 定义了不同的数据类别,例如 STT_FUNC(函数)和 STT_OBJECT(对象)。Mach-O 中没有对应的等价概念,因此你在为 ELF 目标编写汇编时常用的 .type 指令在 Darwin/Mach-O 环境下是不受支持的。

平台 ABI 的细微差别

除了上述主要差异外,还需要注意 Darwin ABI 与其他平台 ABI 之间的细微区别。一个显著的例子是寄存器 x18 的使用:

  • Darwinx18 寄存器被保留,并且在某些上下文切换场景中会被显式清零。
  • Android:同样保留 x18
  • GNU/Linux / Alpine:未保留该寄存器。

这意味着在编写跨平台汇编时,应避免假设 x18 的状态或用途在所有平台上保持一致。

Apple 特有的向量指令助记符

另一个需要警惕的陷阱是 Apple 为 NEON 指令集引入的自定义助记符。为了简化 NEON 代码的编写,Apple 引入了一套允许省略寄存器内存布局说明的助记符扩展。

  • Apple 特有写法

    eor.16b v2, v2, v0
    

    这种写法是 Apple 对 ARM 汇编语法的特定扩展,它允许开发者省略每个操作数的 .16b 后缀。

  • 官方 ARM 标准写法: 根据官方 ARM 汇编手册,必须为每个寄存器指定内存布局:

    eor v2.16b, v2.16b, v0.16b
    

如果仅针对 Apple 设备,前者更简洁;但为了可移植性,应坚持使用官方手册规定的标准写法。

使用宏抽象 ABI 细节

ABI 的细节差异可以通过简单的宏定义轻松抽象出来。对于 NEON 函数的使用,原则很简单:遵循 ARM 手册的标准写法,而非 Apple 的简写。

以下是两个关键的宏定义,建议放置在头文件中以便复用:

  1. 处理 Darwin 的下划线要求

    #ifdef __APPLE__
    # define PROC_NAME(__proc) _ ## __proc
    #else
    # define PROC_NAME(__proc) __proc
    #endif
    

    该宏在 Apple 环境下自动为函数名添加下划线,在其他环境下则保持原样。

  2. 可选:处理 ELF 符号类型

    #ifdef __clang__
    # define TYPE(__proc, __typ)
    #else
    # define TYPE(__proc, __typ) .type __proc, __typ
    #endif
    

    该宏在非 Apple 工具链(通常使用 GNU 汇编器)中生成 .type 指令,而在 Apple 的 Clang 工具链中则忽略该指令,从而避免语法错误。

最终使用示例: 编写汇编时,只需正常书写但使用上述宏:

.global PROC_NAME(unmask)
.align 2
TYPE(unmask, @function)
PROC_NAME(unmask):
    ...

只要遵循这些指南,你的汇编代码即可在任何 64 位 ARM 的 UNIX-like 环境中保持可移植性。

关键要点

  • 符号命名规则:在 Darwin (Mach-O) 上,所有全局符号需加前导下划线(如 _func),而在 ELF (Linux/BSD) 上则不需要。
  • 指令兼容性:Mach-O 不支持 ELF 的 .type 指令,需通过宏定义在非 Apple 平台下启用,在 Apple 平台下禁用。
  • 寄存器保留:注意 x18 寄存器在 Darwin 和 Android 中被保留并可能清零,但在 GNU/Linux 中未保留,跨平台代码应避免依赖其状态。
  • NEON 语法标准:避免使用 Apple 特有的 NEON 简写助记符(如 eor.16b v2, v2, v0),应严格遵循 ARM 官方手册的标准语法(如 eor v2.16b, v2.16b, v0.16b)以确保可移植性。
  • 宏抽象策略:通过 PROC_NAMETYPE 两个宏,可以无缝处理 Apple 与非 Apple 工具链之间的 ABI 差异,实现“一次编写,多处运行”。

意义与影响

这篇文章为 ARM64 生态系统的开发者提供了一套实用的标准化方案。随着 ARM 架构在服务器、嵌入式设备(SBC)以及个人电脑(Apple Silicon)中的全面渗透,代码的可移植性变得至关重要。

通过明确区分 Mach-O 和 ELF 的 ABI 差异,并摒弃平台特定的语法糖,开发者可以构建出真正通用的底层优化代码。这不仅有助于提升软件在异构 ARM 环境中的兼容性,也降低了维护多平台汇编代码的成本,促进了开源社区中高性能计算库(如加密库、图像处理库等)的广泛复用。对于希望深入系统级编程或进行跨平台性能优化的工程师而言,这是一份极具价值的实践指南。

查看原文 →ariadne.space