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 或更高版本。

***图 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, 安全报告生成, 快速连接, 教程, 汇编语言, 系统编程