前言
这篇主要介绍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有两种寄存器:
- 处理整型和指针的寄存器
- 通用寄存器和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。当寄存器在此程序调用标准中具有固定角色时,使用大写。
- 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类:
- 代码区。只能被进程读,不可些。
- 可写静态数据。
- 只读静态数据。
- 堆。
- 栈。
可写静态数据可以细分为初始化,零初始化和未初始化数据。 除了栈之外,其它4类内存不需要占用连续的内存。 进程必须具有一些代码和栈,其它3类不是必须有。
堆是由进程管理的内存区域, 通常用于创建动态数据对象。
内存地址
地址空间包括一个或多个不相交的区域。 区域不能跨越零地址,但是可以从零开始。
标记寻址(tagged addressing)的使用是特定平台解释的。 当禁用标记寻址时,指针的所有64位都被传递到地址转换系统。 启用标记寻址时,为了进行地址转换,将忽略指针的前八位。注意:此tagged addressing,非iOS里的Tagged Pointer。
栈
栈是连续的内存空间,可用于存储局部变量和参数传递(用于传递参数的寄存器不够用时候)。栈地址是从高到低,栈的地址保存在SP中。
栈使用限制:
- Stack-limit < SP <= stack-base
- 进程只能访问这个范围内的栈空间:[SP, stack-base – 1]
- SP mod 16 = 0
函数调用
A64指令集包含函数调用指令BL和BLR。
执行BL:PC(program counter)顺序的下一个值,也就是返回地址(函数调用完成返回要执行指令的地址),存放到LR中,将跳转地址传给PC。BLR跟BL类似,只不过PC的值是从寄存器中读取。
参数传递
参数可通过r0-r7、v0-v7,栈来传递;如果参数个数不多,且参数可放进寄存器,那仅用寄存器传递参数。
可变参数
可变参数可分为命名参数(已声明的)和匿名参数(可选的参数)。
当可变参数的函数,调用时候,没有可选参数时候(只有已声明的参数),调用过程和固定参数的函数一样的。
参数传递规则
参数传递从概念上可以分为2阶段:
- 从源语言参数类型到机器类型的映射(不同源语言,映射规则不同)
- 整理机器类型,生成最终参数列表
参数传递过程分为3个阶段:
-
阶段A – 初始化
(在开始处理参数之前,该阶段仅执行一次)- NGRN = 0 (NGRN意义,见术语)
- NSRN = 0 (NSRN意义,见术语)
- NSAA = SP(NSAA意义,见术语)
-
阶段B - 预填充和扩展参数 (把参数列表中的每一个参数,去匹配下面规则,第一个被匹配到的规则,应用到该参数上。)
- 如果参数类型是复合类型,调用者和被调用者都不能确定其大小,则将参数复制到内存中,并将参数替换为指向该内存的指针。 (C / C ++语言中没有这样的类型,其它语言存在。)
- 如果参数是HFA或HVA类型,则参数不修改。
- 如果参数是大于16个字节的复合类型,调用者申请一个内存,将参数复制到内存里去,并将参数替换为指向该内存的指针。
- 如果参数是复合类型,则参数的大小向上舍入为最接近8个字节的倍数。(例如参数大小为9字节,修改为16字节)
-
阶段C- 把参数放到寄存器或栈里 (参数列表中的每个参数,将依次应用以下规则,直到参数放到寄存器或栈里,此参数处理完成,然后再从参数列表中取参数。注: 将参数分配给寄存器时,寄存器中未使用的位的值不确定。 将参数分配给栈时,未填充字节的值不确定。)
- (1) 如果参数是half(16bit),single(16bit),double(32bit)或quad(64bit)浮点数或Short Vector Type,并且NSRN小于8,则将参数放入寄存器v[NSRN]的最低有效位。 NSRN增加1。 此参数处理完成。
- (2) 如果参数是HFA(homogeneous floating-point aggregate)或HVA(homogeneous short vector aggregate)类型,且NSRN + (HFA或HVA成员个数) ≤ 8,则每个成员依次放入SIMD and Floating-point 寄存器,NSRN=NSRN+ HFA或HVA成员个数。此参数处理完成。
- (3) 如果参数是HFA(homogeneous floating-point aggregate)或HVA(homogeneous short vector aggregate)类型,但是NSRN已经等于8(说明v0-v7被使用完毕)。则参数的大小向上舍入为最接近8个字节的倍数。(例如参数大小为9字节,修改为16字节)
- (4) 如果参数是HFA(homogeneous floating-point aggregate)、HVA(homogeneous short vector aggregate)、quad(64bit)浮点数或Short Vector Type,NSAA = NSAA+max(8, 参数自然对齐大小)。
- (5) 如果参数是half(16bit),single(16bit)浮点数,参数扩展到8字节(放入最低有效位,其余bits值不确定)
- (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) 如果参数是整型或指针类型、size(参数)<=8字节,且NGRN小于8,则参数复制到x[NGRN]中的最低有效位。 NGRN增加1。 此参数处理完成。
- (8) 如果参数对齐后16字节,NGRN向上取偶数。(例如:NGRN为2,那值保持不变;假如NGRN为3,则取4。 注:iOS ABI没有这个规则)
- (9) 如果参数是整型,对齐后16字节,且NGRN小于7,则把参数复制到x[NGRN] 和 x[NGRN+1],x[NGRN]是低位。NGRN = NGRN + 2。 此参数处理完成。
- (10) 如果参数是复合类型,且参数可以完全放进x寄存器(8-NGRN>= 参数字节大小/8)。从x[NGRN]依次放入参数(低位开始)。未填充的bits的值不确定。NGRN = NGRN + 此参数用掉的寄存器个数。此参数处理完成。
- (11) NGRN设为8。
- (12) NSAA = NSAA+max(8, 参数自然对齐大小)。
- (13) 如果参数是复合类型,参数copy到内存,NSAA=NSAA+size(参数)。此参数处理完成。
- (14) 如果参数小于8字节,参数设置为8字节大小,高位bits值不确定。
- (15) 参数copy到内存,NSAA=NSAA+size(参数)。此参数处理完成。
从上面规则,可以得到经验:
- 处理完参数列表中所有的参数后,调用者一定知道传递参数用了多少栈空间。(NSAA - SP)
- 浮点数和short vector types通过v寄存器和栈传递,不会通过r寄存器传递。(除非是小复合类型的成员)
- 寄存器和栈中,参数未填充满的部分的值,不可确定。
函数返回结果
函数返回方式取决于返回结果的类型。
- 如果返回是类型T,如下
void func(T arg)
arg值通过寄存器(组)传递,返回的结果也是通过相同的寄存器(组)返回。
- 调用者申请内存(内存大小足够放入返回结果且是内存对齐的),将内存地址放入x8中传递给子函数,子函数运行时候,可以更新x8指向内存的内容,从而将结果返回。
结语
假如文章有不对地方,欢迎大家留言指出;或者给我发邮件(wu_k_k@foxmail.com)。
引用
- http://infocenter.arm.com/help/topic/com.arm.doc.ihi0055b/IHI0055B_aapcs64.pdf
- https://blog.csdn.net/adaptiver/article/details/80492292
- https://developer.apple.com/library/archive/documentation/Xcode/Conceptual/iPhoneOSABIReference/Articles/ARM64FunctionCallingConventions.html
--EOF-- 转载请保留链接,谢谢
网友评论