/* TODO 本系列文章是对 ARMv8 Cortex-a 系列编程向导手册拙劣的翻译和注解,若有出入,以官方文档为准 */
Chapter 8 移植到 A64
这一章节不打算对如何编写可移植代码做出详细介绍,而只介绍应用工程师在编写可移植代码时的主要应该关注的方面。
当移动 A32 代码到 A64 架构上运行时,我们应该清晰的认识到 A64 指令集与 A32/T32 指令集存在下面这些重大的差异:
- 大多数 A32 指令可以条件执行,但是在 A64 指令集中,只有少数指令可以条件执行。
- 大多数 A64 指令可以应用一个任意的偏移量到源寄存器。
- A64 指令集的寻址模式与 A32 不同。A32、T32 指令集使用的偏移寻址、索引前/索引后寻址在 A64 中依然可用,但是,A64 中新增了 PC 相关寻址模式,因为在 A64 中, PC 不能作为一个通用目的寄存器被访问。
- A64 指令集移除了所有的多内存访问指令,但是添加了
LDP
与STP
指令来加载、存储寄存器对,这两个指令可以操作任意两个寄存器,A64 同样移除了PUSH
和POP
指令。 - ARMv8 新增了包含单向内存屏障的加载与存储指令:
load-acquire
与store-release
。load-acquire
指令要求接下来任意的内存访问只有在load-acquire
完成之后才可见;store-release
确保了所有之前的内存访问在store-release
指令可见之前可见。 - A64 指令集不支持协处理器的访问,比如 CP15。
- 在 AArch64 状态下,不存在
CPSR
寄存器,CPSR
功能包含在PSTATE
位域中,可以通过特殊目的寄存器来访问PSTATE
位域 。
对于大多数应用,当代码移植到 AArch64 架构上运行时,仅仅需要重编译源代码。然而,在某些方面也存在一些不可移植的 C 代码。
8.1 对齐
数据和代码必须对齐合适的边界。
对齐可以影响 ARM 内核的性能,并且可以代表一个可以问题。
之前的 ARM 编译器语法使用 ALIGN n
指令来实现对齐,n
表示对齐的字节边界,比如 ALIGN 128
表示 128 字节对齐。
GNU 汇编语法与 ARM Complier 6 语法使用 .balign n
指令来实现对齐,同样,n
表示对齐的字节边界。
当我们移植 A32 代码到 AArch64 架构时,应该把 ALIGN n
指令更换为 .balign n
。
注意: GNU 汇编也提供一个 .align n
指令,但此时 n 的格式在不同架构的内核中具有不同的格式
8.2 数据类型
64 位机器上,大多数 C 编程环境中, int
类型依旧是 4 字节,但是,long
与指针类型是 8 字节宽。这种数据类型被称作 LP64
数据模型。
ARM ABI 为 LP64 数据模型定义了一些基本数据类型,如下:
在 AArch64 架构中,64 位数据可以更有效的被处理。int
类型依旧是 4 字节,指针
类型是 8 字节。ARM ABI 默认 char
类型为 unsigned
。
如果应用代码没有对指针
类型的不可移植操作,比如:将一个非指针类型的数,强制转换为一个指针类型,或者对指针做算数运算。那么代码移植到 AArch64 架构上运行是很简单的,只需要重编译源代码就好。
如果涉及当上述的强制类型转换与指针算数运算,那么我们需要慎重分析源代码,并消除每一个警告。
8.2.1 汇编代码
大多数 A32 汇编指令可以很简单的被 A64 指令所取代,比如下表:
但是,A64 指令集与 A32 指令集依旧在很多方面存在不同,这时,我们需要重新编写汇编代码,如下表:
注意:根据 64 位的 APCS, 堆栈指针需要 16 直接对齐
在 AArch64 架构中,我们通过访问 PSTATE
位域来代替对 CPSR
寄存器的访问:
8.3 当代码从 32 位环境移植到 64 位环境时的问题
当移植 C 代码到 64 位环境时,需要考虑如下的问题:
- 考虑
int
类型与指针
类型长度,因为在某些芯片架构上,这两个类型长度可能不一致。 - 64 位系统有更大的内存访问范围,
int
类型的索引可能不能索引到数组所有的元素,导致死循环。 - C 表达式的隐式类型转换也许会造成意料之外的影响。
- 不同数据类型与符号数之间的运算必须谨慎。因为会涉及到类型转换。
8.3.1 重编译或重写代码
任何代码的移植都需要重编译或重写代码,我们的目标是最大化前者,最小化后者。
由于基本数据类型的改变,建议谨慎思考代码编译过程中的每一个错误与警告。
最后,建议谨慎使用强制类型转换。
8.3.2 ARM Compiler 6 针对 ARMv8-A 的编译器选项
选项举例 | 说明 |
---|---|
–target==aarch64-arm-none-eabi | 生成 ARMv8-A 架构的代码,使用 A64 指令集 |
–target==armv8a-arm-none-eabi | 生成 ARMv8-A 架构的代码,使用 A32/T32 指令集 |
–target==armv7a-arm-none-eabi | 生成 ARMv7-A 架构的代码,使用 A32/T32 指令集 |
8.4 对编写 C 代码的建议
- 分配内存时使用
sizeof
,而不是常量,比如:
(void**) calloc(4,100)
替换为
(void**) calloc(sizeof(void *), 100)
- 如果需要用到强制类型转换,那么请使用
stdint.h
文件中的统一类型 - 谨慎考虑结构体成员布局,因为存在结构体对齐这一原因。
- 请一定知道任意的立即数都是
int
类型,请在移位操作中谨慎使用立即数,如果一定要使用,那么请按照如下方式:
long value = 1L << SOMANY;
8.4.1 显式与隐式类型转换
在不同类型的数据算数运算时,请谨慎思考类型转换的影响,举例如下:
long a;
int b;
unsigned int c;
b = -2;
c = 1;
a = b + c;
根据隐式类型转换规则,a 的值为 0x00000000FFFFFFFF,而不是 -1.
代码可以更改为如下,来让 a 的值 为 -1:
long a;
int b;
unsigned int c;
b = -2;
c = 1;
a = (long)b + c;
8.4.2 位操作
按照之前所说,任意的立即数被认为是 int
类型,所以,64位环境中,我们在移位操作中需要格外小心。
下列函数用于置位变量的低32位,但不能置位高32位:
long SetBitN(long value, unsigned bitNum)
{
long mask;
mask = 1 << bitNum; // 1 的数据类型是 int, 不能置位高32位
return value | mask;
}
如果需要置位高 32 位,那么需要将 1 的数据类型更改为 long
:
long long SetBitN(long long value, unsigned bitNum)
{
long long mask;
mask = 1LL << bitNum;// 将 1 的数据类型更改为 long long
return value | mask;
}
8.4.3 索引值
在 64 位环境中,当我们定义了一个非常大的数组时,我们必须使用 long 类型去遍历数组,而不是 int 类型,代码举例如下:
static char array[BIG_NUMBER];
for (unsigned int index = 0; index != BIG_NUMBER; index++);
此时,如果 BIG_NUMBER
的值大于 0xFFFFFFFF, 那么会陷入死循环
Chapter 9 AArch64 的 ABI
ABI:Application Binary Interface(应用二进制接口).
ABI 用于指定可执行代码模块必须遵守的基本规则,以便于这些代码可以正确的运行。这些基本由指定编程语言的额外规则补充,单独的操作系统或者执行环境也许会指定额外的规则,比如 linux。
AArch64 架构的 ABI 存在很多的组件:
- 可执行与可连接格式(ELF),ELF 指定了可连接对象格式与可执行文件格式。
- 流程调用标准(PCS),PCS 规定了如何独立的编写一个子函数,并将这个子函数与代码编译到一起。PCS 指定了调用函数与子函数之间的约定,或者函数与执行环境之间的约定,比如,当调用一个函数时的堆栈布局以及通用目的寄存器的保存与恢复。
- DWARF,用于标准化调试数据格式。
- C 与 C++ 库支持。
- C++ ABI。
9.1 AArch64 PCS 中的通用目的寄存器的使用
理解通用目的寄存器的使用标准是非常有用的。理解子函数中的参数传递可以帮助我们:
- 编写更有效率的 C 代码
- 理解反汇编代码
- 编写汇编代码
- 调用一个由不同语言编写的函数,比如:C 内嵌汇编。
9.1.1 通用目的寄存器中的参数
出于函数调用的目的,可以把 31 个通用目的寄存器(X0-X31)分为 4 组:
- 参数寄存器(X0-X7): 这 8 个寄存器用于调用子函数时传递形参,以及返回子函数运行的结果。这 8 个寄存器可以用作临时寄存器,调用者如果需要保存这 8 个寄存器的值的话,那么必须将这 8 寄存器的值保存在堆栈中。AArch64 可以使用这 8 个寄存器传递 8 个形参。
- 调用者需要保存的寄存器(X9-X15):如果这7个寄存器中的值不能被污染,那么调用者必须将这 7 个寄存器的值在函数调用时,保存在自己的堆栈中。换言之,子函数可以随意使用这 7 个寄存器的值,且不会将这 7 个寄存器的值进行保存与恢复。
- 子函数需要保存的寄存器(X19-X29):子函数在运行之前需要将这 11 个寄存器保存在自己的堆栈中,在子函数返回前,从堆栈中恢复这 11 个寄存器的值。
- 用作特殊目的的寄存器:
寄存器 | 描述 |
---|---|
X8 | 用于保存间接结果寄存器,X8 保存一个间接结果的地址,比如说,当一个子函数返回一个很大的结构体时。 |
X16/X17 | 这两个寄存器是 IP0 与 IP1,用作过程内调用临时寄存器(不懂)。 |
X18 | X18 是平台寄存器,被指定平台的 ABI 所保护,无实际意义? |
X29 | X29 是帧指针寄存器(FP) |
X30 | 链接寄存器 - LR,用于保存子函数返回地址 |
下图展示了 AArch64 的所有通用目的寄存器及其分类:
9.1.2 间接结果位置寄存器 - X8
X8 用作传递间接结果的位置,比如子函数返回一个很大的结构体时。那么在子函数调用之前,需要给 X8 赋值。
示例代码如下:
//test.c//
struct struct_A
{
int i0;
int i1;
double d0;
double d1;
} AA;
struct struct_A foo(int i0, int i1, double d0, double d1)
{
struct struct_A A1;
A1.i0 = i0;
A1.i1 = i1;
A1.d0 = d0;
A1.d1 = d1;
return A1;
}
void bar()
{
AA = foo(0, 1, 1.0, 2.0);
}
反汇编代码如下:
foo//
SUB SP, SP, #0x30
STR W0, [SP, #0x2C]
STR W1, [SP, #0x28]
STR D0, [SP, #0x20]
STR D1, [SP, #0x18]
LDR W0, [SP, #0x2C]
STR W0, [SP, #0]
LDR W0, [SP, #0x28]
STR W0, [SP, #4]
LDR W0, [SP, #0x20]
STR W0, [SP, #8]
LDR W0, [SP, #0x18]
STR W0, [SP, #10]
LDR X9, [SP, #0x0]
STR X9, [X8, #0]
LDR X9, [SP, #8]
STR X9, [X8, #8]
LDR X9, [SP, #0x10]
STR X9, [X8, #0x10]
ADD SP, SP, #0x30
RET
bar//
STP X29, X30, [SP, #0x10]!
MOV X29, SP
SUB SP, SP, #0x20
ADD X8, SP, #8
MOV W0, WZR
ORR W1, WZR, #1
FMOV D0, #1.00000000
FMOV D1, #2.00000000
BL foo:
ADRP X8, {PC}, 0x78
ADD X8, X8, #0
LDR X9, [SP, #8]
STR X9, [X8, #0]
LDR X9, [SP, #0x10]
STR X9, [X8, #8]
LDR X9, [SP, #0x18]
STR X9, [X8, #0x10]
MOV SP, X29
LDP X20, X30, [SP], #0x10
RET
在上述的示例代码中,由于 foo() 返回的结构体超过 16 个字节,所以,返回结果需要保存在 X8 中。示例代码体现了下列通用寄存器的作用:
- W0,W1,D0,D1 用于传递子函数的形参。
- bar() 在栈中开辟空间,并给 X8 赋值,foo() 将返回值保存在 X8 中。
AAPCS64 栈帧的使用如下图所示,X29(FP) 应该指向堆栈中的前一帧,并且保存在 LR 之后。
9.1.3 NEON 与浮点寄存器的使用
AArch64 包含了 32 个浮点寄存器 V0-V31,可以用于 NEON 与浮点运算。
AArch64 PCS 对浮点寄存器分类如下: