ARMv8 Cortex-a 编程向导手册学习_6.aarch64 应用移植注意事项与 AArch64 ABI

/* 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 指令集移除了所有的多内存访问指令,但是添加了 LDPSTP 指令来加载、存储寄存器对,这两个指令可以操作任意两个寄存器,A64 同样移除了 PUSHPOP 指令。
  • ARMv8 新增了包含单向内存屏障的加载与存储指令:load-acquirestore-releaseload-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 数据模型定义了一些基本数据类型,如下:

ARMv8 Cortex-a 编程向导手册学习_6.aarch64 应用移植注意事项与 AArch64 ABI_第1张图片

在 AArch64 架构中,64 位数据可以更有效的被处理。int 类型依旧是 4 字节,指针类型是 8 字节。ARM ABI 默认 char 类型为 unsigned

如果应用代码没有对指针类型的不可移植操作,比如:将一个非指针类型的数,强制转换为一个指针类型,或者对指针做算数运算。那么代码移植到 AArch64 架构上运行是很简单的,只需要重编译源代码就好。
如果涉及当上述的强制类型转换与指针算数运算,那么我们需要慎重分析源代码,并消除每一个警告。

8.2.1 汇编代码

大多数 A32 汇编指令可以很简单的被 A64 指令所取代,比如下表:
ARMv8 Cortex-a 编程向导手册学习_6.aarch64 应用移植注意事项与 AArch64 ABI_第2张图片
ARMv8 Cortex-a 编程向导手册学习_6.aarch64 应用移植注意事项与 AArch64 ABI_第3张图片

但是,A64 指令集与 A32 指令集依旧在很多方面存在不同,这时,我们需要重新编写汇编代码,如下表:
ARMv8 Cortex-a 编程向导手册学习_6.aarch64 应用移植注意事项与 AArch64 ABI_第4张图片

注意:根据 64 位的 APCS, 堆栈指针需要 16 直接对齐

在 AArch64 架构中,我们通过访问 PSTATE 位域来代替对 CPSR 寄存器的访问:
ARMv8 Cortex-a 编程向导手册学习_6.aarch64 应用移植注意事项与 AArch64 ABI_第5张图片

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 的所有通用目的寄存器及其分类:
ARMv8 Cortex-a 编程向导手册学习_6.aarch64 应用移植注意事项与 AArch64 ABI_第6张图片

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 之后。
ARMv8 Cortex-a 编程向导手册学习_6.aarch64 应用移植注意事项与 AArch64 ABI_第7张图片

9.1.3 NEON 与浮点寄存器的使用

AArch64 包含了 32 个浮点寄存器 V0-V31,可以用于 NEON 与浮点运算。

AArch64 PCS 对浮点寄存器分类如下:
ARMv8 Cortex-a 编程向导手册学习_6.aarch64 应用移植注意事项与 AArch64 ABI_第8张图片

你可能感兴趣的:(armv8,ARM,学习,arm,arm开发)