/* TODO 本系列文章是对 ARMv8 Cortex-a 系列编程向导手册拙劣的翻译和注解,若有出入,以官方文档为准 */
这一章节不打算对如何编写可移植代码做出详细介绍,而只介绍应用工程师在编写可移植代码时的主要应该关注的方面。
当移动 A32 代码到 A64 架构上运行时,我们应该清晰的认识到 A64 指令集与 A32/T32 指令集存在下面这些重大的差异:
LDP
与 STP
指令来加载、存储寄存器对,这两个指令可以操作任意两个寄存器,A64 同样移除了 PUSH
和 POP
指令。load-acquire
与 store-release
。load-acquire
指令要求接下来任意的内存访问只有在 load-acquire
完成之后才可见;store-release
确保了所有之前的内存访问在 store-release
指令可见之前可见。CPSR
寄存器,CPSR
功能包含在 PSTATE
位域中,可以通过特殊目的寄存器来访问 PSTATE
位域 。对于大多数应用,当代码移植到 AArch64 架构上运行时,仅仅需要重编译源代码。然而,在某些方面也存在一些不可移植的 C 代码。
数据和代码必须对齐合适的边界。
对齐可以影响 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 的格式在不同架构的内核中具有不同的格式
64 位机器上,大多数 C 编程环境中, int
类型依旧是 4 字节,但是,long
与指针类型是 8 字节宽。这种数据类型被称作 LP64
数据模型。
ARM ABI 为 LP64 数据模型定义了一些基本数据类型,如下:
在 AArch64 架构中,64 位数据可以更有效的被处理。int
类型依旧是 4 字节,指针
类型是 8 字节。ARM ABI 默认 char
类型为 unsigned
。
如果应用代码没有对指针
类型的不可移植操作,比如:将一个非指针类型的数,强制转换为一个指针类型,或者对指针做算数运算。那么代码移植到 AArch64 架构上运行是很简单的,只需要重编译源代码就好。
如果涉及当上述的强制类型转换与指针算数运算,那么我们需要慎重分析源代码,并消除每一个警告。
大多数 A32 汇编指令可以很简单的被 A64 指令所取代,比如下表:
但是,A64 指令集与 A32 指令集依旧在很多方面存在不同,这时,我们需要重新编写汇编代码,如下表:
注意:根据 64 位的 APCS, 堆栈指针需要 16 直接对齐
在 AArch64 架构中,我们通过访问 PSTATE
位域来代替对 CPSR
寄存器的访问:
当移植 C 代码到 64 位环境时,需要考虑如下的问题:
int
类型与 指针
类型长度,因为在某些芯片架构上,这两个类型长度可能不一致。int
类型的索引可能不能索引到数组所有的元素,导致死循环。任何代码的移植都需要重编译或重写代码,我们的目标是最大化前者,最小化后者。
由于基本数据类型的改变,建议谨慎思考代码编译过程中的每一个错误与警告。
最后,建议谨慎使用强制类型转换。
选项举例 | 说明 |
---|---|
–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 指令集 |
sizeof
,而不是常量,比如:(void**) calloc(4,100)
替换为
(void**) calloc(sizeof(void *), 100)
stdint.h
文件中的统一类型int
类型,请在移位操作中谨慎使用立即数,如果一定要使用,那么请按照如下方式:long value = 1L << SOMANY;
在不同类型的数据算数运算时,请谨慎思考类型转换的影响,举例如下:
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;
按照之前所说,任意的立即数被认为是 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;
}
在 64 位环境中,当我们定义了一个非常大的数组时,我们必须使用 long 类型去遍历数组,而不是 int 类型,代码举例如下:
static char array[BIG_NUMBER];
for (unsigned int index = 0; index != BIG_NUMBER; index++);
此时,如果 BIG_NUMBER
的值大于 0xFFFFFFFF, 那么会陷入死循环
ABI:Application Binary Interface(应用二进制接口).
ABI 用于指定可执行代码模块必须遵守的基本规则,以便于这些代码可以正确的运行。这些基本由指定编程语言的额外规则补充,单独的操作系统或者执行环境也许会指定额外的规则,比如 linux。
AArch64 架构的 ABI 存在很多的组件:
理解通用目的寄存器的使用标准是非常有用的。理解子函数中的参数传递可以帮助我们:
出于函数调用的目的,可以把 31 个通用目的寄存器(X0-X31)分为 4 组:
寄存器 | 描述 |
---|---|
X8 | 用于保存间接结果寄存器,X8 保存一个间接结果的地址,比如说,当一个子函数返回一个很大的结构体时。 |
X16/X17 | 这两个寄存器是 IP0 与 IP1,用作过程内调用临时寄存器(不懂)。 |
X18 | X18 是平台寄存器,被指定平台的 ABI 所保护,无实际意义? |
X29 | X29 是帧指针寄存器(FP) |
X30 | 链接寄存器 - LR,用于保存子函数返回地址 |
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 中。示例代码体现了下列通用寄存器的作用:
AAPCS64 栈帧的使用如下图所示,X29(FP) 应该指向堆栈中的前一帧,并且保存在 LR 之后。
AArch64 包含了 32 个浮点寄存器 V0-V31,可以用于 NEON 与浮点运算。