文章目录
  1. 1. 前言
  2. 2. 术语介绍
  3. 3. 数据类型和对齐
    1. 3.1. 基本数据类型
  4. 4. 程序调用规则
    1. 4.1. 寄存器
    2. 4.2. 进程、内存、栈
      1. 4.2.1. 内存地址
      2. 4.2.2.
    3. 4.3. 函数调用
    4. 4.4. 参数传递
      1. 4.4.1. 可变参数
      2. 4.4.2. 参数传递规则
    5. 4.5. 函数返回结果
  5. 5. 结语
  6. 6. 引用

前言

欢迎大家关注我的掘金主页,我后面会以掘金为主;有文章先发在掘金上,然后同步到自己博客上来。

这篇主要介绍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。详细见下文参数传递

数据类型和对齐

基本数据类型

Type ClassMachine TypeByte
size
Natural
Alignment
(bytes)
IntegralUnsigned byte11
Signed byte11
Unsigned half-
word
22
Signed half-
word
22
Unsigned word44
Signed word44
Unsigned
double-word
88
Signed double-
word
88
Unsigned quad-
word
1616
Signed quad-
word
1616
Floating PointHalf precision22
Single precision44
Double
precision
88
Quad precision1616
Short vector64-bit vector88
128-bit vector1616
PointerData pointer88
Code pointer88

程序调用规则

寄存器

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,如下
    1
    void func(T arg)

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

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

结语

假如文章有不对地方,欢迎大家留言指出;或者给我发邮件(wu_k_k@foxmail.com)。

引用

  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– 若无特别说明,本站文章均为原创,转载请保留链接,谢谢

文章目录
  1. 1. 前言
  2. 2. 术语介绍
  3. 3. 数据类型和对齐
    1. 3.1. 基本数据类型
  4. 4. 程序调用规则
    1. 4.1. 寄存器
    2. 4.2. 进程、内存、栈
      1. 4.2.1. 内存地址
      2. 4.2.2.
    3. 4.3. 函数调用
    4. 4.4. 参数传递
      1. 4.4.1. 可变参数
      2. 4.4.2. 参数传递规则
    5. 4.5. 函数返回结果
  5. 5. 结语
  6. 6. 引用