arm64程序调用规则

前言

我的博客

这篇主要介绍arm64程序调用规则,详细分析了程序调用过程中,参数是如何传递的。Android、iOS、Linux等基本遵循这些规则,但是各个操作系统平台也有小部分自己特定的规则。下一篇,我将介绍iOS平台的特定规则。

术语介绍

术语 意义
A32 在ARMv7架构中,使用32位固定长度指令的ARM指令集。
A64 AArch64可用时的指令集。
AAPCS64 AArch64程序调用标准。(PCS:Procedure Call Standard)
AArch32 ARMv8中的32位通用寄存器,兼容ARMv7-A。
AArch64 ARMv8中的64位通用寄存器
ABI(Application Binary Interface) 汇编接口规范,跟执行环境相关,比如Linux ABI,说的是Linux环境下的汇编接口规范;
ARM-based 基于ARM
Floating point 根据上下文有这三种意思:(1)遵循IEEE 754 2008的浮点运算; (2)ARMv8浮点指令集; (3)一个被ARMv8浮点指令集和ARMv8 SIMD指令集共享的寄存器组。
Q-o-I Quality of Implementation
SIMD Single Instruction Multiple Data 一条指令操作多个数据
T32 T32使用可变16bit和32bit
Routine, subroutine Routine:调用者;subroutine:被调用者
Procedure 没有返回值的函数
Function 有返回值的函数
PIC, PID Position-independent code, position-independent data.
Program state 指程序内存和寄存器的值
Caller- saved register 调用者在调用函数之前,保存寄存器(一般入栈),函数返回后恢复寄存器(一般出栈)
Callee-saved register 被调用者(函数内部),在起始地方保存寄存器,在结束时,恢复寄存器
NGRN(The Next General-purpose Register Number ) 可以理解为,记录r0-r7(见下文寄存器)使用个数,参数传递前设为0,每放一个参数进入寄存器(整型寄存器),值加1。当等于8时候,说明r0-r7寄存器使用完了,再有参数,只能放入内存了。
NSRN (The Next SIMD and Floating-point Register Number) 同上,记录v0-v7使用个数
NSAA (The next stacked argument address) 记录参数放入内存,参数传递前设为SP,所以内存中参数范围应该是 sp~NSAA。详细见下文参数传递

数据类型和对齐

基本数据类型

程序调用规则

寄存器

arm64有两种寄存器:

  1. 处理整型和指针的寄存器
    1. 通用寄存器和AAPCS64用法
寄存器 别名 意义
SP Stack Pointer:栈指针
r30 LR Link Register:在调用函数时候,保存下一条要执行指令的地址。
r29 FP Frame Pointer:保存函数栈的基地址。
r19...r28 Callee-saved registers(含义见上面术语解释)
r18 平台寄存器,有特定平台解释其用法。如果平台未把其做特殊用途,可当做临时寄存器使用。(iOS平台保留的寄存器,应用不可使用)
r17 IP1 The second intra-procedure-call temporary register (can be used by call veneers and PLT code); at other times may be used as a temporary register.
r16 IP0 The first intra-procedure-call scratch register (can be used by call veneers and PLT code); at other times may be used as a temporary register.
r9...r15 临时寄存器
r8 在一些情况下,返回值是通过r8返回的
r0...r7 r0-r7在函数调用过程中传递参数和返回值
NZCV 状态寄存器:N(Negative)负数 Z(Zero) 零 C(Carry) 进位 V(Overflow) 溢出

arm64有31个通用整型寄存器,r0-r30。当使用64bits时候,命名x0-x30;使用32bits时,命名w0-w30。当寄存器在此程序调用标准中具有固定角色时,使用大写。

  1. SIMD 和 Floating-Point寄存器

ARM64有32个寄存器v0-v31,用于处理SIMD和浮点运算。长度不同称谓也不同,b,h,s,d,q,分别代表byte(8位),half(16位),single(32位),double(64位),quad(128位)。v0-v7在函数调用过程中传递参数和返回值;v8-v15 是Callee-saved registers(见术语解释),且是保存前64bits(更大的位数,调用者负责保存),v0-v7, v16-v31不需要保存或者调用者保存。

进程、内存、栈

一个进程的内存可分为5类:

  1. 代码区。只能被进程读,不可些。
  2. 可写静态数据。
  3. 只读静态数据。
  4. 堆。
  5. 栈。

可写静态数据可以细分为初始化,零初始化和未初始化数据。 除了栈之外,其它4类内存不需要占用连续的内存。 进程必须具有一些代码和栈,其它3类不是必须有。
堆是由进程管理的内存区域, 通常用于创建动态数据对象。

内存地址

地址空间包括一个或多个不相交的区域。 区域不能跨越零地址,但是可以从零开始。
标记寻址(tagged addressing)的使用是特定平台解释的。 当禁用标记寻址时,指针的所有64位都被传递到地址转换系统。 启用标记寻址时,为了进行地址转换,将忽略指针的前八位。注意:此tagged addressing,非iOS里的Tagged Pointer。

栈是连续的内存空间,可用于存储局部变量和参数传递(用于传递参数的寄存器不够用时候)。栈地址是从高到低,栈的地址保存在SP中。
栈使用限制:

  1. Stack-limit < SP <= stack-base
  2. 进程只能访问这个范围内的栈空间:[SP, stack-base – 1]
  3. SP mod 16 = 0

函数调用

A64指令集包含函数调用指令BL和BLR。
执行BL:PC(program counter)顺序的下一个值,也就是返回地址(函数调用完成返回要执行指令的地址),存放到LR中,将跳转地址传给PC。BLR跟BL类似,只不过PC的值是从寄存器中读取。

参数传递

参数可通过r0-r7、v0-v7,栈来传递;如果参数个数不多,且参数可放进寄存器,那仅用寄存器传递参数。

可变参数

可变参数可分为命名参数(已声明的)和匿名参数(可选的参数)。
当可变参数的函数,调用时候,没有可选参数时候(只有已声明的参数),调用过程和固定参数的函数一样的。

参数传递规则

参数传递从概念上可以分为2阶段:

  1. 从源语言参数类型到机器类型的映射(不同源语言,映射规则不同)
  2. 整理机器类型,生成最终参数列表

参数传递过程分为3个阶段:

  • 阶段A – 初始化
    (在开始处理参数之前,该阶段仅执行一次)

    1. NGRN = 0 (NGRN意义,见术语)
    2. NSRN = 0 (NSRN意义,见术语)
    3. NSAA = SP(NSAA意义,见术语)
  • 阶段B - 预填充和扩展参数 (把参数列表中的每一个参数,去匹配下面规则,第一个被匹配到的规则,应用到该参数上。)

    1. 如果参数类型是复合类型,调用者和被调用者都不能确定其大小,则将参数复制到内存中,并将参数替换为指向该内存的指针。 (C / C ++语言中没有这样的类型,其它语言存在。)
    2. 如果参数是HFA或HVA类型,则参数不修改。
    3. 如果参数是大于16个字节的复合类型,调用者申请一个内存,将参数复制到内存里去,并将参数替换为指向该内存的指针。
    4. 如果参数是复合类型,则参数的大小向上舍入为最接近8个字节的倍数。(例如参数大小为9字节,修改为16字节)
  • 阶段C- 把参数放到寄存器或栈里 (参数列表中的每个参数,将依次应用以下规则,直到参数放到寄存器或栈里,此参数处理完成,然后再从参数列表中取参数。注: 将参数分配给寄存器时,寄存器中未使用的位的值不确定。 将参数分配给栈时,未填充字节的值不确定。)

    1. (1) 如果参数是half(16bit),single(16bit),double(32bit)或quad(64bit)浮点数或Short Vector Type,并且NSRN小于8,则将参数放入寄存器v[NSRN]的最低有效位。 NSRN增加1。 此参数处理完成。
    2. (2) 如果参数是HFA(homogeneous floating-point aggregate)或HVA(homogeneous short vector aggregate)类型,且NSRN + (HFA或HVA成员个数) ≤ 8,则每个成员依次放入SIMD and Floating-point 寄存器,NSRN=NSRN+ HFA或HVA成员个数。此参数处理完成。
    3. (3) 如果参数是HFA(homogeneous floating-point aggregate)或HVA(homogeneous short vector aggregate)类型,但是NSRN已经等于8(说明v0-v7被使用完毕)。则参数的大小向上舍入为最接近8个字节的倍数。(例如参数大小为9字节,修改为16字节)
    4. (4) 如果参数是HFA(homogeneous floating-point aggregate)、HVA(homogeneous short vector aggregate)、quad(64bit)浮点数或Short Vector Type,NSAA = NSAA+max(8, 参数自然对齐大小)。
    5. (5) 如果参数是half(16bit),single(16bit)浮点数,参数扩展到8字节(放入最低有效位,其余bits值不确定)
    6. (6) 如果参数是HFA(homogeneous floating-point aggregate)、HVA(homogeneous short vector aggregate)、half(16bit),single(16bit),double(32bit)或quad(64bit)浮点数或Short Vector Type,参数copy到内存,NSAA=NSAA+size(参数)。此参数处理完成。
    7. (7) 如果参数是整型或指针类型、size(参数)<=8字节,且NGRN小于8,则参数复制到x[NGRN]中的最低有效位。 NGRN增加1。 此参数处理完成。
    8. (8) 如果参数对齐后16字节,NGRN向上取偶数。(例如:NGRN为2,那值保持不变;假如NGRN为3,则取4。 注:iOS ABI没有这个规则)
    9. (9) 如果参数是整型,对齐后16字节,且NGRN小于7,则把参数复制到x[NGRN] 和 x[NGRN+1],x[NGRN]是低位。NGRN = NGRN + 2。 此参数处理完成。
    10. (10) 如果参数是复合类型,且参数可以完全放进x寄存器(8-NGRN>= 参数字节大小/8)。从x[NGRN]依次放入参数(低位开始)。未填充的bits的值不确定。NGRN = NGRN + 此参数用掉的寄存器个数。此参数处理完成。
    11. (11) NGRN设为8。
    12. (12) NSAA = NSAA+max(8, 参数自然对齐大小)。
    13. (13) 如果参数是复合类型,参数copy到内存,NSAA=NSAA+size(参数)。此参数处理完成。
    14. (14) 如果参数小于8字节,参数设置为8字节大小,高位bits值不确定。
    15. (15) 参数copy到内存,NSAA=NSAA+size(参数)。此参数处理完成。

从上面规则,可以得到经验:

  1. 处理完参数列表中所有的参数后,调用者一定知道传递参数用了多少栈空间。(NSAA - SP)
  2. 浮点数和short vector types通过v寄存器和栈传递,不会通过r寄存器传递。(除非是小复合类型的成员)
  3. 寄存器和栈中,参数未填充满的部分的值,不可确定。

函数返回结果

函数返回方式取决于返回结果的类型。

  1. 如果返回是类型T,如下
void func(T arg)

arg值通过寄存器(组)传递,返回的结果也是通过相同的寄存器(组)返回。

  1. 调用者申请内存(内存大小足够放入返回结果且是内存对齐的),将内存地址放入x8中传递给子函数,子函数运行时候,可以更新x8指向内存的内容,从而将结果返回。

结语

假如文章有不对地方,欢迎大家留言指出;或者给我发邮件([email protected])。

引用

  1. http://infocenter.arm.com/help/topic/com.arm.doc.ihi0055b/IHI0055B_aapcs64.pdf
  2. https://blog.csdn.net/adaptiver/article/details/80492292
  3. https://developer.apple.com/library/archive/documentation/Xcode/Conceptual/iPhoneOSABIReference/Articles/ARM64FunctionCallingConventions.html

--EOF-- 转载请保留链接,谢谢

你可能感兴趣的:(arm64程序调用规则)