前言
這篇主要介紹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
sizeNatural
Alignment
(bytes)
IntegralUnsigned byte11
Signed byte11
Unsigned half-
word22
Signed half-
word22
Unsigned word44
Signed word44
Unsigned
double-word88
Signed double-
word88
Unsigned quad-
word1616
Signed quad-
word1616
Floating PointHalf precision22
Single precision44
Double
precision88
Quad precision1616
Short vector64-bit vector88
128-bit vector1616
PointerData pointer88
Code pointer88
程序調用規則
寄存器
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值通過寄存器(組)傳遞,返回的結果也是通過相同的寄存器(組)返回。
2. 調用者申請內存(內存大小足夠放入返回結果且是內存對齊的),將內存地址放入x8中傳遞給子函數,子函數運行時候,可以更新x8指向內存的內容,從而將結果返回。
結語
假如文章有不對地方,歡迎大家留言指出;或者給我發郵件(wu_k_k@foxmail.com)。
引用
--EOF-- 轉載請保留鏈接,謝謝