below/HelloSilicon

GitHub: below/HelloSilicon

将 ARM64 汇编教材中的 Linux 示例代码适配为 Apple Silicon 版本的入门教程,详细记录 Darwin 与 Linux 底层差异。

Stars: 4974 | Forks: 329

# HelloSilicon 关于 Apple silicon Mac 汇编语言的入门介绍。 ## 简介 在这个代码仓库中,我将跟着《[Programming with 64-Bit ARM Assembly Language](https://link.springer.com/book/10.1007/978-1-4842-5881-1?source=shoppingads&locale=de&cjsku=9781484258804)》这本书一起编写代码,并将所有示例代码调整为适用于 Apple ARM64 系列计算机的版本。尽管 Apple 的营销材料似乎避免为该平台命名,而只谈论 M1 处理器,但开发者文档使用了“Apple silicon”一词。在下文中,我将使用这个术语。 原始源代码可以在[这里](https://github.com/Apress/programming-with-64-bit-ARM-assembly-language)找到。 ## 前置条件 虽然我非常假设能来到这里的人已经满足了大部分(如果不是全部)所需的前置条件,但将它们列出来也无妨。 * 你需要 [Xcode 12.2](https://developer.apple.com/xcode/) 或更高版本,并且为了让操作更简单,应该安装命令行工具。这可以确保工具能在默认位置(即 `/usr/bin`)被找到。如果你不确定是否安装了这些工具,请在 Xcode 中检查 _Preferences → Locations_,或者运行 `xcode-select --install`。 * 所有应用程序示例还需要至少 [macOS Big Sur](https://developer.apple.com/macos/)、[iOS 14](https://developer.apple.com/ios/) 或对应的 watchOS 或 tvOS 版本。特别是对于后三个系统,这本身并不是必须的(Xcode 12.2 也不是),但这会让事情变得简单得多。 * 最后,虽然所有示例都可以进行调整以在 iPhone 和 Apple 的所有其他 ARM64 设备上运行,但为了获得最佳效果,你应该拥有一台 [Apple silicon Mac](https://www.apple.com/newsroom/2020/11/introducing-the-next-generation-of-mac/)。 ## 相对于原书的改动 除了现有的 iOS 示例外,本书是基于 Linux 操作系统的。Apple 的操作系统(macOS、iOS、watchOS 和 tvOS)实际上只是 [Darwin](https://en.wikipedia.org/wiki/Darwin_(operating_system) 操作系统的不同分支,因此它们共享一组公共的核心组件。 Linux 和 Darwin 都受到了 [AT&T Unix System V](http://www.unix.org/what_is_unix/history_timeline.html) 的启发,但在我们所关注的底层级别上,它们存在显著差异。对于书中的代码清单,这主要涉及系统调用(即当我们希望内核为我们做某事时),以及 Darwin 访问内存的方式。 本文档的组织方式使你可以一边阅读本书,一边了解 Apple silicon 的相关差异。本文档中的标题与书中的标题保持一致。 ## 第 1 章:入门 ### 计算机与数字 macOS 上的默认“计算器.app”也有一个“程序员模式”。你可以通过 _View → Programmer_ (⌘3) 来启用它。 ### CPU 寄存器 Apple 对寄存器做出了某些特定于平台的选择: * Apple 保留 **X18** 供自己使用。不要使用这个寄存器。 * 帧指针寄存器(**FP**, **X29**)必须始终指向一个有效的帧记录。 ### 关于 GCC 汇编器 本书使用 Linux GNU 工具,例如 GNU `as` 汇编器。虽然 macOS 上也有 `as` 命令,但默认情况下它会调用集成的 [LLVM Clang](https://clang.llvm.org) 汇编器。即使有使用基于 GNU 的汇编器的 `-Q` 选项,这也仅适用于 x86_64 —— 并且在撰写本文时已被弃用。 ``` % as -Q -arch arm64 /usr/bin/as: can't specifiy -Q with -arch arm64 ``` 因此,GNU 汇编器语法不是一个可选项,必须针对 Clang 汇编器语法调整代码。 同样,虽然 macOS 上有 `gcc` 命令,但它只是调用了 Clang C 编译器。为了保持透明,所有对 `gcc` 的调用都将替换为 `clang`。 ``` % gcc --version Configured with: --prefix=/Applications/Xcode-beta.app/Contents/Developer/usr --with-gxx-include-dir=/Applications/Xcode-beta.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/4.2.1 Apple clang version 12.0.0 (clang-1200.0.32.27) Target: arm64-apple-darwin20.1.0 Thread model: posix InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin ``` ### Hello World 如果你正在阅读本文,我假设你已经知道可以在 _Applications → Utilities → Terminal.app_ 中找到 macOS 的终端。但如果你不知道,我很荣幸能告诉你,祝你在这次旅程中玩得开心!不要害怕提问。 为了让“Hello World”在 Apple silicon 上运行,首先必须应用第 78 页(第 3 章)中的更改,以解决 Darwin 和 Linux 内核之间的差异。 为了消除警告,我插入了 `.align 4`(或 `.p2align 2`),因为 Darwin 希望事物在偶数边界上对齐。原书在第 5 章第 114 页的“数据对齐”中提到了这一点。 由于每个系统的独特约定,Linux 和 macOS 中的系统调用有几个不同之处。以下是一些关键区别: * 函数编号:两个系统的函数编号不同,Linux 使用 64,而 macOS 使用 4。Darwin (Apple) 系统调用的表格可以在此链接找到:[Darwin System Calls](https://github.com/apple-oss-distributions/xnu/blob/main/bsd/kern/syscalls.master)。 * 存储函数编号的地址:用于存储函数编号的地址也有所不同。在 Linux 中,它位于 X8,而在 macOS 中,它位于 X16。 * 中断调用:Linux 中的中断调用为 0,而在 Apple Silicon 上则为 0x80。 为了让链接器正常工作,还需要做一些事情,其中大部分对于 Mac/iOS 开发者来说应该很熟悉。这些更改需要应用到 `makefile` 和 `build` 文件中。调用链接器的完整命令如下所示: ``` ld -o HelloWorld HelloWorld.o \ -lSystem \ -syslibroot `xcrun -sdk macosx --show-sdk-path` \ -e _start \ -arch arm64 ``` 我们都知道 `-o` 开关,让我们看看其他的: * `-lSystem` 告诉链接器将我们的可执行文件与 `libSystem.dylib` 链接。我们这样做是为了将 `LC_MAIN` 加载命令添加到可执行文件中。通常,Darwin 不支持[静态链接的可执行文件](https://developer.apple.com/library/archive/qa/qa1118/_index.html)。不使用 `libSystem.dylib` 来创建可执行文件是[可能](https://stackoverflow.com/questions/32453849/minimal-mach-o-64-binary/32659692#32659692)的,但并不特别优雅。时间允许的话,我会深入探讨这个话题。对于阅读 _Mac OS X Internals_ 的读者,我只想补充一点,从 MacOS X 10.7 开始,这取代了 `LC_UNIXTHREAD`。 * `-sysroot`:为了找到 `libSystem.dylib`,必须告诉我们的链接器去哪里找它。在 macOS 10.15 上似乎没有必要这样做,因为 _“macOS Big Sur 11 beta 中的新增功能,系统附带了一个内置的动态链接器缓存,其中包含所有系统提供的库。作为此更改的一部分,文件系统中不再存在动态库的副本。”_。我们使用 `xcrun -sdk macosx --show-sdk-path` 来动态使用当前激活的 Xcode 版本。 * `-e _start`:Darwin 期望入口点为 `_main`。为了既让示例尽可能贴近原书,又允许它在第 3 章的 C 示例中使用,我选择保留 `_start` 并告诉链接器这是我们要使用的入口点。 * `-arch arm64`:为了以防万一,我们加入这个选项,以便可以从 Intel Mac 交叉编译它。在 Apple silicon 上运行时,你可以忽略它。 ### 逆向工程我们的程序 虽然 `objdump` 命令行程序在 Darwin 上同样有效并产生预期的输出,但也可以尝试 `--macho`(或 `-m`)选项,这会让 objdump 使用特定于 Mach-O 的目标文件解析器。 ## 第 2 章:加载与相加 必须应用[第 1 章](https://github.com/below/HelloSilicon#chapter-1)中的更改(makefile、对齐、系统调用)。 ### 寄存器与移位 gcc 汇编器接受 `MOV X1, X2, LSL #1`,但这在 [ARM Compiler User Guide](https://developer.arm.com/documentation/dui0801/g/A64-General-Instructions/MOV--register-?lang=en) 中并未定义。相反,应使用 `LSL X1, X2, #1`(等等)。 ### 寄存器与扩展 Clang 要求源寄存器为 32 位。这是有道理的,因为在这些扩展下,64 位寄存器的高 32 位永远不会被触及: ``` ADD X2, X1, W0, SXTB ``` GNU 汇编器似乎忽略了这一点,并允许你指定 64 位源寄存器。 ## 第 3 章:工具准备 ### GDB 入门 在 macOS 上,`gdb` 已被 LLVM 项目的 [LLDB Debugger](https://lldb.llvm.org) `lldb` 取代。其语法并不总是与 gdb 相同,因此我将在此处指出差异。 要开始调试我们的 **movexamps** 程序,请输入命令 ``` lldb movexamps ``` 这会产生简略的输出: ``` (lldb) target create "movexamps" Current executable set to 'movexamps' (arm64). (lldb) ``` 像 `run` 或 `list` 这样的命令是一样的,并且有一个很好的 [GDB to LLDB command map](https://lldb.llvm.org/use/map.html)。 为了反汇编我们的程序,lldb 使用了稍微不同的语法: ``` disassemble --name start ``` 请注意,因为我们链接的是动态可执行文件,所以清单会很长,并包含其他的 `start` 函数。我们的代码将在 ``movexamps`start`` 行下列出。 同样,lldb 需要不带下划线的断点名称:`b start` 要在 lldb 中获取寄存器,我们使用 **register read**(或 **re r**)。不带参数时,此命令将打印所有寄存器,或者你也可以只指定你想查看的寄存器,比如 `re r SP X0 X1`。 我们可以使用 **breakpoint list**(或 **br l**)查看所有断点。我们可以使用 **breakpoint delete**(或 **br de**)指定要删除的断点编号来删除断点。 **lldb** 拥有更强大的内存显示机制。主要命令是 **memory read**(或 **m read**)。首先,以下是本书使用的参数: ``` memory read -fx -c4 -s4 $address ``` 其中 * **-f** 是显示格式 * **-s** 数据大小 * **-c** 计数 ### 清单 3-1 作为练习,我添加了在 macOS 上查找默认 Xcode 工具链的代码。在书中,他们使用它稍后从 Linux 工具链切换到 Android 工具链。这个过程在 macOS 和 iOS 上有很大不同:它通常不涉及不同的工具链,而是涉及不同的软件开发套件 (SDK)。你可以在[清单 1-1](https://github.com/below/HelloSilicon#listing-1-1) 中看到这一点,其中设置了 `-sysroot`。 也就是说,虽然可以使用命令行构建 iOS 可执行文件,但这并不是一个简单的过程。因此,对于构建应用程序,我将坚持使用 Xcode。 ### Apple Xcode 由于[第 10 章](https://github.com/below/HelloSilicon#chapter-10)侧重于构建将在 iOS 上运行的应用程序,因此我选择在这里简单地创建一个命令行工具,它现在使用了相同的 `HelloWorld.s` 文件。 请注意,函数编号不仅不同,而且在 Darwin 上,它们被视为私有的,随时可能更改。 ## 第 4 章:控制程序流 除了常见的更改之外,我们还面临一个新问题,该问题在原书的第 5 章中有所描述:Darwin 不喜欢 `LDR X1, =symbol`,它会产生错误 `ld: Absolute addressing not allowed in arm64 code`。如果我们按照书第 3 章中的建议使用 `ADR X1, symbol`,我们的数据必须位于只读的 `.text` 段中。然而,在这个示例中,我们需要可写的数据。 [Apple 文档](https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/MachOTopics/1-Articles/x86_64_code.html#//apple_ref/doc/uid/TP40005044-SW1)告诉我们,在 Darwin 上: 默认情况下,在 Darwin 上,包含在 `.data` 段(数据可写)中的所有数据都是“可能是非局部的”。 完整的答案可以在这[里](https://reverseengineering.stackexchange.com/a/15324)找到: 所以这个: ``` LDR X1, =outstr // address of output string ``` 变成了这个: ``` ADRP X1, outstr@PAGE // address of output string 4k page ADD X1, X1, outstr@PAGEOFF // offset to outstr within the page ``` ### 练习 有人问我如何读取命令行,我很乐意[回答](https://github.com/below/HelloSilicon/issues/22#issuecomment-682205151)这个问题。 示例代码可以在第 4 章的文件 [`case.s`](Chapter%2004/case.s) 中找到。 ## 第 5 章:感谢内存 上面已经解决了 Darwin 在内存寻址方面的重要区别。 总的来说,关于 **WORD** 是什么可能会引起一些混淆。书中说 `LDR` 指令从 PC 中获取偏移量,以“字”为单位,通常被理解为 32 位。但在“双寄存器”部分,128 位长的数据被称为 *octaword*,这意味着 **WORD** 是 16 位。 我向本书作者 Stephen Smith 询问了这个问题,他的回复是: 我唯一的补充是,clang 的 `as` 也保持了 `.octa` 指令不变。 ### 清单 5-1 对于 llvm 汇编器,`quad`、`octa` 和 `fill` 关键字必须小写。(见本文档底部) ### 清单 5-10 更改与第 4 章类似。 ## 第 6 章:函数与堆栈 正如我们在第 5 章中学到的,所有的汇编器指令(如 `.equ`)都必须小写。 ## 第 7 章:Linux 操作系统服务 Apple SDK 中不存在 `asm/unistd.h`,而是可以使用 `sys/syscalls.h`。 **警告:** 请注意,Darwin 中的系统调用编号在官方上被认为是私有的,并且可能会发生变化。此处提供它们仅出于教育目的。 还需要注意的是,虽然调用和定义看起来相似,但 Linux 和 Darwin 并不相同:`AT_FDCWD` 在 Linux 上是 -100,但在 Darwin 上必须是 -2。 与 Linux 不同,错误是通过设置进位标志来表示的,并且错误代码是非负的。因此,我们将结果 `MOV` 到所需的寄存器中,而不是 `ADDS`(我们不需要检查负数,并且需要保留条件标志)并使用 BC 跳转到成功路径。 ## 第 8 章:对 GPIO 引脚进行编程 本章专门针对 Raspberry Pi 4,因此在此无需赘述。 ## 第 9 章:与 C 和 Python 交互 出于透明的原因,我用 `clang` 替换了 `gcc`。 ### 清单 9-1 除了通常的更改之外,对于可变参数函数,Apple 偏离了 ARM64 标准 ABI(即函数如何被调用的约定)。可变参数函数是接受可变数量参数的函数,而 `printf` 就是其中之一。Linux 接受在寄存器中传递的参数,而对于 Darwin,我们必须将它们压入堆栈传递。 ``` str X1, [SP, #-32]! // Move the stack pointer four doublewords (32 bytes) down and push X1 onto the stack str X2, [SP, #8] // Push X2 to one doubleword above the current stack pointer str X3, [SP, #16] // Push X3 to two doublewords above the current stack pointer adrp X0, ptfStr@PAGE // printf format str add X0, X0, ptfStr@PAGEOFF // add offset for format str bl _printf // call printf add SP, SP, #32 // Clean up stack ``` 首先,我们向下扩展堆栈 32 字节,为三个 64 位值腾出空间。我们为第四个值创建了填充空间,因为正如书中第 137 页指出的那样,ARM 硬件要求堆栈指针始终进行 16 字节对齐。 在同一条指令中,**X1** 被存储到堆栈指针的新位置。 现在,我们通过将 **X2** 存储在高于堆栈指针 8 个字节的位置,并将 **X3** 存储在高于堆栈指针 16 个字节的位置,来填充刚刚创建的剩余空间。请注意,**X2** 和 **X3** 的 **str** 指令不会移动 **SP**。 我们可以用不同的方式填充堆栈;重要的是 `printf` 函数期望参数作为双字值从当前堆栈指针向上依次排列。因此,在 `debug.s` 文件的情况下,它期望 `%c` 的参数位于 **SP** 处,`%32ld` 的参数位于其上方一个双字处,最后,`%016lx` 的参数位于当前堆栈指针上方两个双字、即 16 字节处。 我们实际所做的是[在堆栈上分配内存](https://en.wikipedia.org/wiki/Stack-based_memory_allocation)。作为调用者,我们“拥有”该内存,因此我们需要在函数跳转后释放它,在这种情况下,只需将堆栈(向上)收缩我们分配的 32 字节即可。指令 `add SP, SP, #32` 将执行此操作。 ### 清单 9-5 `mytoupper` 被加上了 `_` 前缀,因为这是 Darwin 上的 C 语言找到它所必需的。 ### 清单 9-6 无需更改。 ### 清单 9-7 创建的是动态 Mach-O 库,而不是共享的 `.so` ELF 库。更多信息可以在这里找到:[Creating Dynamic Libraries](https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/DynamicLibraries/100-Articles/CreatingDynamicLibraries.html) ### 清单 9-8 在我们这里使用的内联汇编中,`cont` 标签必须通过在前面加上 `L` 来声明为局部标签。虽然在第 5 章的纯汇编中这不是必需的,但 llvm C-Frontend 会自动将指令 [`.subsections_via_symbols`](https://developer.apple.com/library/archive/documentation/DeveloperTools/Reference/Assembler/040-Assembler_Directives/asm_directives.html#//apple_ref/doc/uid/TP30000823-SW13) 添加到代码中: 在我们使用 LLVM 工具链的同时,在汇编中 —— 包括内联汇编 —— 所有的安全检查都是关闭的,因此我们必须采取额外的预防措施,并专门将前向标签声明为局部的。 此外,必须将一个变量的大小从 int 更改为 long,以使编译器完全满意并消除所有警告。 ### 从 Python 调用汇编 ### 清单 9-9 虽然 `uppertst5.py` 文件只需要很少的更改,但调用代码却更具挑战性。在 Apple silicon Mac 上,Python 是一个包含 x86_64 和 arm64e 两种架构的 Mach-O 通用二进制文件: ``` % lipo -info /usr/bin/python3 Architectures in the fat file: /usr/bin/python3 are: x86_64 arm64e ``` 明显缺失的是我们到目前为止一直为之构建的 arm64 架构。这使得我们的 dylib 无法与 Python 一起使用。 arm64e 是 [Armv-8 架构](https://community.arm.com/developer/ip-products/processors/b/processors-ip-blog/posts/armv8-a-architecture-2016-additions),Apple 自 A12 芯片开始使用。如果你想针对 A12 之前的设备,你必须坚持使用 arm64。第一批使用 ARM64 的 Mac 运行在基于 A14 架构的 M1 CPU 上,因此 Apple 决定利用这些新特性。 那么,该怎么办呢?我们可以将所有内容编译为 arm64e,但这会使该库在 iPhone X 或更早的设备上毫无用处,而我们也希望支持它们。 在上面,你阅读了一些关于_通用二进制文件_的内容。很久以来,Mach-O 可执行文件格式就一直在单个文件中支持多种处理器架构。这包括但不限于 Motorola 68k(在 NeXT 计算机上)、PowerPC、Intel x86 以及 ARM 代码,在适用的地方每种都有其 32 位和 64 位变体。在这种情况下,我正在构建一个包含 arm64 和 arm64e 代码的通用动态库。更多信息可以在这[里](https://developer.apple.com/documentation/xcode/building_a_universal_macos_binary)找到。 虽然大多数适用于 Linux 的 Python IDE 也可用于 macOS,但在撰写本文时,唯一本身以 arm64 运行的 Python IDE —— 因此能够加载 arm64 库 —— 是 Python.org 的 [IDLE](https://www.python.org/downloads/macos/),版本 3.10 或更高版本。 ![我们的 Python 程序在 IDLE IDE 中运行](images/Figure_9-1.png?raw=true "Our Python program running in the IDLE IDE") ***图 9-1.*** *我们的 Python 程序在 IDLE IDE 中运行* 或者,你可以使用命令行来测试该程序。(从 macOS 12.3 开始,Apple [移除了 Python 2](https://developer.apple.com/documentation/macos-release-notes/macos-12_3-release-notes),开发者应使用 Python 3) ``` % python3 uppertst5.py b'This is a test!' b'THIS IS A TEST!' 16 ``` 最后一点说明:虽然 Apple 的 python3 二进制文件是 arm64e 的,但 IDLE 使用的 Python 框架却是 arm64 的。本章构建的库是一个包含这两种架构的通用二进制文件,这一事实使其能够用于任何一种环境中。 ## 第 10 章:与 Kotlin 和 Swift 交互 核心代码无需更改,但我创建了一个 SwiftUI 应用程序,而不是仅仅是一个 iOS 应用程序,它可以在 macOS、iOS、watchOS(Series 4 及更高版本)和 tvOS 上运行。 ## 第 11 章:乘法、除法与累加 此时,这些更改应该不言自明。通常的 makefile 调整、`.align 4`、地址模式更改以及 `_printf` 调整。 ## 第 12 章:浮点运算 与第 11 章一样,所有的更改都已经介绍过了。这里没有什么新内容。 ## 第 13 章:Neon 协处理器 该示例使用了非标准语法来引用单个向量元素, GNU 汇编器接受这种语法,但 Clang 不接受。 当示例使用 `V3.4H[0]` 来引用第一个 16 位元素时, 正确且标准的语法是 `V3.H[0]`,GNU 汇编器和 Clang 都接受这种语法。 到这一步,对代码的所有其他更改应该都很简单了。 ## 第 14 章:代码优化 这里没有什么不寻常的更改。 ## 第 15 章:阅读和理解代码 ### 复制内存页 在 [bcopy.s](https://github.com/apple/darwin-xnu/blob/master/osfmk/arm64/bcopy.s) 中可以找到一些阅读 Darwin 内核中 ARM64 代码的起点。该目录和整个仓库中还有更多内容。 ### GCC 创建的代码 无需更改。Mach-O 可执行文件不支持“tiny”代码模型: ``` % clang -O3 -mcmodel=tiny -o upper upper.c fatal error: error in backend: tiny code model is only supported on ELF ``` ## 第 16 章:骇客代码 可以说的是,clang 会自动启用位置无关可执行文件,并且 `-no-pie` 选项不起作用。因此,无法重现 `upper.s` 文件中展示的漏洞利用。 ## 额外参考资料 * [Writing ARM64 Code for Apple Platforms](https://developer.apple.com/documentation/xcode/writing_arm64_code_for_apple_platforms),关于 Apple 平台如何偏离标准 64 位 ARM 架构的文档 * [Mach-O Programming Topics](https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/MachOTopics/0-Introduction/introduction.html#//apple_ref/doc/uid/TP40001827-SW1),关于 Mach-O 可执行文件格式及其与 ELF 区别的优秀介绍。即使它仍然引用 PowerPC 64 位架构并且没有提及 ARM,但大部分内容仍然适用。 * [What is required for a Mach-O executable to load?](https://stackoverflow.com/a/42399119/1600891) * [Mac OS X Internals, A Systems Approach](https://www.pearson.ch/Informatik/Macintosh/EAN/9780134426549/Mac-OS-X-Internals) Amit Singh,2007 年。无论好坏,这仍然是关于 macOS 及其兄弟系统核心的权威纲要。 * [WWDC20: Explore the new system architecture of Apple silicon Macs](https://developer.apple.com/videos/play/wwdc2020/10686/) 新型 Apple silicon 机器的系统概述 * [Darwin Source Code](https://opensource.apple.com/source/xnu/) * [ARM Architecture Reference Manual](https://developer.arm.com/documentation/ddi0487/latest/) ## 还有一件事…… _“C 语言是区分大小写的。编译器是区分大小写的。Unix 命令行、ufs 和 nfs 文件系统是区分大小写的。我也是区分大小写的,尤其是对于产品名称。这个 IDE 叫做 Xcode。大写的 X,小写的 c。不是 XCode 或 xCode 或 X-Code。请记住这一点。”_ —— Chris Espinosa
标签:Apple Silicon, ARM64, 安全报告生成, 快速连接, 教程, 汇编语言, 系统编程