轉自:https://blog.csdn.net/wuhenyouyuyouyu/article/details/85756088
出處:APCS,ARM 過程調用標準(ARM Procedure Call Standard)
介紹
APCS,ARM 過程調用標準(ARM Procedure Call Standard),提供了緊湊的編寫例程的一種機制,定義的例程可以與其他例程交織在一起。最顯著的一點是對這些例程來自哪裏沒有明確的限制。它們可以編譯自 C、 Pascal、也可以是用匯編語言寫成的。
APCS 定義了:
- 對寄存器使用的限制。
- 使用棧的慣例。
- 在函數調用之間傳遞/返回參數。
- 可以被‘回溯’的基於棧的結構的格式,用來提供從失敗點到程序入口的函數(和給予的參數)的列表。
APCS 不一個單一的給定標準,而是一系列類似但在特定條件下有所區別的標準。例如,APCS-R (用於 RISC OS)規定在函數進入時設置的標誌必須在函數退出時復位。在 32 位標準下,並不是總能知道進入標誌的(沒有 USR_CPSR),所以你不需要恢復它們。如你所預料的那樣,在不同版本間沒有相容性。希望恢復標誌的代碼在它們未被恢復的時候可能會表現失常...
如果你開發一個基於 ARM 的系統,不要求你去實現 APCS。但建議你實現它,因爲它不難實現,且可以使你獲得各種利益。但是,如果要寫用來與編譯後的 C 連接的彙編代碼,則必須使用 APCS。編譯器期望特定的條件,在你的加入(add-in)代碼中必須得到滿足。一個好例子是 APCS 定義 a1 到 a4 可以被破壞,而 v1 到 v6 必須被保護。現在我確信你正在撓頭並自言自語“a 是什麼? v 是什麼?”。所以首先介紹 APCS-R 寄存器定義...
寄存器命名
APCS 對我們通常稱爲 R0 到 R14 的寄存器起了不同的名字。使用匯編器預處理器的功能,你可以定義 R0 等名字,但在你修改其他人寫的代碼的時候,最好還是學習使用 APCS 名字。
寄存器名字 | ||
Reg # |
APCS |
意義 |
R0 |
a1 |
工作寄存器 |
R1 |
a2 |
" |
R2 |
a3 |
" |
R3 |
a4 |
" |
R4 |
v1 |
必須保護 |
R5 |
v2 |
" |
R6 |
v3 |
" |
R7 |
v4 |
" |
R8 |
v5 |
" |
R9 |
v6 |
" |
R10 |
sl |
棧限制 |
R11 |
fp |
楨指針 |
R12 |
ip |
|
R13 |
sp |
棧指針 |
R14 |
lr |
連接寄存器 |
R15 |
pc |
程序計數器 |
譯註:ip 是指令指針的簡寫。
這些名字不是由標準的 Acorn 的 objasm(版本 2.00)所定義的,但是 objasm 的後來版本,和其他彙編器(比如 Nick Robert 的 ASM)定義了它們。要定義一個寄存器名字,典型的,你要在程序最開始的地方使用 RN
宏指令(directive):
a1 RN 0 a2 RN 1 a3 RN 2 ...等... r13 RN 13 sp RN 13 r14 RN 14 lr RN r14 pc RN 15
這個例子展示了一些重要的東西:
- 寄存器可以定義多個名字 - 你可以定義‘r13’和‘sp’二者。
- 寄存器可以定義自前面定義的寄存器 - ‘lr’定義自叫做‘r14’的寄存器。
(對於 objasm 是正確的,其他彙編器可能不是這樣)
設計關鍵
- 函數調用應當快、小、和易於(由編譯器來)優化。
- 函數應當可以妥善處理多個棧。
- 函數應當易於寫可重入和可重定位的代碼;主要通過把可寫的數據與代碼分離來實現。
- 但是最重要的是,它應當簡單。這樣彙編編程者可以非常容易的使用它的設施,而調試者能夠非常容易的跟蹤程序。
一致性
程序的遵循 APCS 的部分在調用外部函數時被稱爲“一致”。在程序執行期間的所有時候都遵循 APCS (典型的,由編譯器生成的程序)被稱爲“嚴格一致”。協議指出,假如你遵守正確的進入和退出參數,你可以在你自己的函數範圍內做你需要的任何事情,而仍然保持一致。這在有些時候是必須的,比如在寫 SWI 僞裝(veneers)的時候使用了許多給實際的 SWI 調用的寄存器。
棧
棧是鏈接起來的‘楨’的一個列表,通過一個叫做‘回溯結構’的東西來鏈接它們。這個結構存儲在每個楨的高端。按遞減地址次序分配棧的每一塊。寄存器 sp
總是指向在最當前楨中最低的使用的地址。這符合傳統上的滿降序棧。在 APCS-R 中,寄存器 sl
持有一個棧限制,你遞減 sp
不能低於它。在當前棧指針和當前棧之間,不應該有任何其他 APCS 函數所依賴的東西,在被調用的時候,函數可以爲自己設置一個棧塊。
可以有多個棧區(chunk)。它們可以位於內存中的任何地址,這裏沒有提供規範。典型的,在可重入方式下執行的時候,這將被用於爲相同的代碼提供多個棧;一個類比是 FileCore,它通過簡單的設置‘狀態’信息和並按要求調用相同部分的代碼,來向當前可獲得的 FileCore 文件系統(ADFS、RAMFS、IDEFS、SCSIFS 等)提供服務。
回溯結構
寄存器 fp
(楨指針)應當是零或者是指向棧回溯結構的列表中的最後一個結構,提供了一種追溯程序的方式,來反向跟蹤調用的函數。
回溯結構是:
地址高端 保存代碼指針 [fp] fp 指向這裏 返回 lr 值 [fp, #-4] 返回 sp 值 [fp, #-8] 返回 fp 值 [fp, #-12] 指向下一個結構 [保存的 sl] [保存的 v6] [保存的 v5] [保存的 v4] [保存的 v3] [保存的 v2] [保存的 v1] [保存的 a4] [保存的 a3] [保存的 a2] [保存的 a1] [保存的 f7] 三個字 [保存的 f6] 三個字 [保存的 f5] 三個字 [保存的 f4] 三個字 地址低端
這個結構包含 4 至 27 個字,在方括號中的是可選的值。如果它們存在,則必須按給定的次序存在(例如,在內存中保存的 a3 下面可以是保存的 f4,但 a2-f5 則不能存在)。浮點值按‘內部格式’存儲並佔用三個字(12 字節)。
fp 寄存器指向當前執行的函數的棧回溯結構。返回 fp 值應當是零,或者是指向由調用了這個當前函數的函數建立的棧回溯結構的一個指針。而這個結構中的返回 fp 值是指向調用了調用了這個當前函數的函數的函數的棧回溯結構的一個指針;並以此類推直到第一個函數。
在函數退出的時候,把返回連接值、返回 sp 值、和返回 fp 值裝載到 pc、sp、和 fp 中。
#include <stdio.h> void one(void); void two(void); void zero(void); int main(void) { one(); return 0; } void one(void) { zero(); two(); return; } void two(void) { printf("main...one...two/n"); return; } void zero(void) { return; } 當它在屏幕上輸出消息的時候, APCS 回溯結構將是: fp ----> two_structure return link return sp return fp ----> one_structure ... return link return sp return fp ----> main_structure ... return link return sp return fp ----> 0 ...
所以,我們可以檢查 fp 並參看給函數‘two’的結構,它指向給函數‘one’的結構,它指向給‘main’的結構,它指向零來終結。在這種方式下,我們可以反向追溯整個程序並確定我們是如何到達當前的崩潰點的。值得指出‘zero’函數,因爲它已經被執行並退出了,此時我們正在做它後面的打印,所以它曾經在回溯結構中,但現在不在了。值得指出的還有對於給定代碼不太可能總是生成象上面那樣的一個 APCS 結構。原因是不調用任何其他函數的函數不要求完全的 APCS 頭部。
爲了更細緻的理解,下面是代碼是 Norcroft C v4.00 爲上述代碼生成的...
AREA |C$code|, CODE, READONLY IMPORT |__main| |x$codeseg| B |__main| DCB &6d,&61,&69,&6e DCB &00,&00,&00,&00 DCD &ff000008 IMPORT |x$stack_overflow| EXPORT one EXPORT main main MOV ip, sp STMFD sp!, {fp,ip,lr,pc} SUB fp, ip, #4 CMPS sp, sl BLLT |x$stack_overflow| BL one MOV a1, #0 LDMEA fp, {fp,sp,pc}^ DCB &6f,&6e,&65,&00 DCD &ff000004 EXPORT zero EXPORT two one MOV ip, sp STMFD sp!, {fp,ip,lr,pc} SUB fp, ip, #4 CMPS sp, sl BLLT |x$stack_overflow| BL zero LDMEA fp, {fp,sp,lr} B two IMPORT |_printf| two ADD a1, pc, #L000060-.-8 B |_printf| L000060 DCB &6d,&61,&69,&6e DCB &2e,&2e,&2e,&6f DCB &6e,&65,&2e,&2e DCB &2e,&74,&77,&6f DCB &0a,&00,&00,&00 zero MOVS pc, lr AREA |C$data| |x$dataseg| END
這個例子不遵從 32 爲體系。APCS-32 規定只是簡單的說明了標誌不需要被保存。所以刪除 LDM 的‘^’後綴,並在函數 zero 中刪除 MOVS 的‘S’後綴。則代碼就與遵從 32-bit 的編譯器生成的一樣了。
保存代碼指針包含這條設置回溯結構的指令(STMFD ...)的地址再加上 12 字節。記住,對於 26-bit 代碼,你需要去除其中的 PSR 來得到實際的代碼地址。
現在我們查看剛進入函數的時候:
- pc 總是包含下一個要被執行的指令的位置。
- lr (總是)包含着退出時要裝載到 pc 中的值。在 26-bit 位代碼中它還包含着 PSR。
- sp 指向當前的棧塊(chunk)限制,或它的上面。這是用於複製臨時數據、寄存器和類似的東西到其中的地方。在 RISC OS 下,你有可選擇的至少 256 字節來擴展它。
- fp 要麼是零,要麼指向回溯結構的最當前的部分。
- 函數實參佈置成(下面)描述的那樣。
實際參數
APCS 沒有定義記錄、數組、和類似的格局。這樣語言可以自由的定義如何進行這些活動。但是,如果你自己的實現實際上不符合 APCS 的精神,那麼將不允許來自你的編譯器的代碼與來自其他編譯器的代碼連接在一起。典型的,使用 C 語言的慣例。
- 前 4 個整數實參(或者更少!)被裝載到 a1 - a4。
- 前 4 個浮點實參(或者更少!)被裝載到 f0 - f3。
- 其他任何實參(如果有的話)存儲在內存中,用進入函數時緊接在 sp 的值上面的字來指向。換句話說,其餘的參數被壓入棧頂。所以要想簡單。最好定義接受 4 個或更少的參數的函數。
函數退出
通過把返回連接值傳送到程序計數器中來退出函數,並且:
- 如果函數返回一個小於等於一個字大小的值,則把這個值放置到 a1 中。
- 如果函數返回一個浮點值,則把它放入 f0 中。
- sp、fp、sl、v1-v6、和 f4-f7 應當被恢復(如果被改動了)爲包含在進入函數時它所持有的值。
我測試了故意的破壞寄存器,而結果是(經常在程序完全不同的部分)出現不希望的和奇異的故障。 - ip、lr、a2-a4、f1-f3 和入棧的這些實參可以被破壞。
在 32 位模式下,不需要對 PSR 標誌進行跨越函數調用的保護。在 26 位模式下必須這樣,並通過傳送 lr 到 pc 中(MOVS、或 LDMFD xxx^)來暗中恢復。必須從 lr 重新裝載 N、Z、C 和 V,跨越函數保護這些標誌不是足夠的。
建立棧回溯結構
對於一個簡單函數(固定個數的參數,不可重入),你可以用下列指令建立一個棧回溯結構:
function_name_label MOV ip, sp STMFD sp!, {fp,ip,lr,pc} SUB fp, ip, #4
這個片段(來自上述編譯後的程序)是最基本的形式。如果你要破壞其他不可破壞的寄存器,則你應該在這個 STMFD 指令中包含它們。
下一個任務是檢查棧空間。如果不需要很多空間(小於 256 字節)則你可以使用:
CMPS sp, sl BLLT |x$stack_overflow| 這是 C 版本 4.00 處理溢出的方式。在以後的版本中,你要調用 |__rt_stkovf_split_small|。
接着做你自己的事情...
通過下面的指令完成退出:
LDMEA fp, {fp,sp,pc}^
還有,如果你入棧了其他寄存器,則也在這裏重新裝載它們。選擇這個簡單的 LDM 退出機制的原因是它比分支到一個特殊的函數退出處理器(handler)更容易和更合理。
用在回溯中的對這個協議的一個擴展是把函數名字嵌入到代碼中。緊靠在函數(和 MOV ip, sp
)的前面的應該是:
DCD &ff0000xx
這裏的‘xx’是函數名字符串的長度(包括填充和終結符)。這個字符串是字對齊、尾部填充的,並且應當被直接放置在 DCD &ff....的前面。
所以一個完整的棧回溯代碼應當是:
DCB "my_function_name", 0, 0, 0, 0 DCD &ff000010 my_function_name MOV ip, sp STMFD sp!, {fp, ip, lr, pc} SUB fp, ip, #4 CMPS sp, sl ; 如果你不使用棧 BLLT |x$stack_overflow| ; 則可以省略 ...處理... LDMEA fp, {fp, sp, pc}^
要使它遵從 32-bit 體系,只須簡單的省略最後一個指令的‘^’。注意你不能在一個編譯的 26-bit 代碼中使用這個代碼。實際上,你可以去除它,但這不是我願意打賭的事情。
如果你不使用棧,並且你不需要保存任何寄存器,並且你不調用任何東西,則沒有必要設置 APCS 塊(但在調試階段對跟蹤問題仍是有用的)。在這種情況下你可以:
my_simple_function ...處理... MOVS pc, lr
(再次,對 32 位 APCS 使用 MOV 而不是 MOVS,但是不要冒險與 26 位代碼連接)。
APCS 標準
總的來說,有多個版本的 APCS (實際上是 16 個)。我們只關心在 RISC OS 上可能遇到的。
APCS-A
就是 APCS-Arthur;由早期的 Arthur 所定義。它已經被廢棄,原因是它有不同的寄存器定義(對於熟練的 RISC OS 程序員它是某種異類)。它用於在 USR 模式下運行的 Arthur 應用程序。不應該使用它。
sl = R13, fp = R10, ip = R11, sp = R12, lr = R14, pc = R15。
- PRM (p4-411) 中說“用
r12
作爲sp
,而不是在體系上更自然的r13
,是歷史性的並先於 Arthur 和 RISC OS 二者。” - 棧是分段的並可按需要來擴展。
- 26-bit 程序計數器。
- 不在 FP 寄存器中傳遞浮點實參。
- 不可重入。標誌必須被恢復。
APCS-R
就是 APCS-RISC OS。用於 RISC OS 應用程序在 USR 模式下進行操作;或在 SVC 模式下的模塊/處理程序。
sl = R10, fp = R11, ip = R12, sp = R13, lr = R14, pc = R15。
- 它是唯一的最通用的 APCS 版本。因爲所有編譯的 C 程序都使用 APCS-R。
- 顯式的棧限制檢查。
- 26-bit 程序計數器。
- 不在 FP 寄存器中傳遞浮點實參。
- 不可重入。標誌必須被恢復。
APCS-U
就是 APCS-Unix,Acorn 的 RISCiX 使用它。它用於 RISCiX 應用程序(USR 模式)或內核(SVC 模式)。
sl = R10, fp = R11, ip = R12, sp = R13, lr = R14, pc = R15。
- 隱式的棧限制檢查(使用 sl)。
- 26-bit 程序計數器。
- 不在 FP 寄存器中傳遞浮點實參。
- 不可重入。標誌必須被恢復。
APCS-32
它是 APCS-2(-R 和 -U)的一個擴展,允許 32-bit 程序計數器,並且從執行在 USR 模式下的一個函數中退出時,允許標誌不被恢復。其他事情同於 APCS-R。
Acorn C 版本 5 支持生成 32-bit 代碼;在用於廣域調試的 32 位工具中,它是最完整的開發發行。一個簡單的測試是要求你的編譯器導出彙編源碼(而不是製作目標代碼)。你不應該找到:MOVS PC, R14
或者LDMFD R13!, {Rx-x, PC}^
對編碼有用的東西
首先要考慮的是該死的 26/32 位問題。 簡單的說,不轉彎抹角絕對沒有方法爲兩個版本的 APCS 彙編同一個通用代碼。但是幸運的這不是問題。APCS 標準不會突然改變。RISC OS 的 32 位版本也不會立刻變異。所以利用這些,我們可以設計一種支持兩種版本的方案。這將遠遠超出 APCS,對於 RISC OS 的 32 位版本你需要使用 MSR 來處理狀態和模式位,而不是使用 TEQP。許多現存的 API 實際上不需要保護標誌位。所以在我們的 32 版本中可以通過把 MOVS PC,...
變成 MOV PC,...
,和把 LDM {...}^
變成 LDM {...}
,並重新建造來解決。objasm 彙編器(v3.00 和以後)有一個 {CONFIG}
變量可以是 26
或 32
。可以使用它建造宏...
my_function_name MOV ip, sp STMFD sp!, {fp, ip, lr, pc} SUB fp, ip, #4 ...處理... [ {CONFIG} = 26 LDMEA fp, {fp, sp, pc}^ | LDMEA fp, {fp, sp, pc} ]
我未測試這個代碼。它(或類似的東西)好象是保持與兩個版本的 APCS 相兼容的最佳方式,也是對 RISC OS 的不同版本,26 位版本和將來的 32 位版本的最佳方法。
測試是否處於 32 位? 如果你要求你的代碼有適應性,有一個最簡單的方法來確定處理器的 PC 狀態:
TEQ PC, PC ; 對於 32 位是 EQ;對於 26 位是 NE
使用它你可以確定:
- 26 位 PC,可能是 APCS-R 或 APCS-32。
- 32 位 PC,不能 APCS-R。所有 26-bit 代碼(TEQP 等)面臨着失敗!
《Procedure Call Standard for the ARM® Architecture》之(5.1)
本文基於以下版本
Document number: ARM IHI 0042F, current through ABI release 2.10
Date of Issue: 24th November 2015
聲明:以下翻譯限於個人學識水平,個別語句也加入了個人理解。原文參見:
http://infocenter.arm.com/help/topic/com.arm.doc.ihi0042f/IHI0042F_aapcs.pdf
5 基本過程調用規範
本節定義一種機器級別的(machine-level)、核心寄存器(core-registers-only)相關的過程調用規範。此規範同時適用於ARM和Thumb指令集。此規範可用於沒有浮點硬件部分,或者需要與Thumb指令高度交互的系統。
5.1 寄存器
ARM架構定義了一套核心指令集,外加協處理器相關的指令集。核心指令集可以訪問核心寄存器。協處理器提供了更多的寄存器,這些寄存器可以完成特定的操作。
5.1.1 核心寄存器
ARM和Thumb指令集中包含16個32bit的核心寄存器,標號依次是r0-r15或者R0-R15。這些寄存器名稱以大寫或者小寫形式出現在彙編語言中。此規範約定:若在過程調用中,寄存器功能固定(has a fixed role),那麼使用大寫形式。下表概括了核心寄存器的使用。除此之外,還有一個程序狀態寄存器(CPSR)。
r0-r3用於向跳轉過程傳遞參數,並存儲返回結果。它們也用於暫存運算的中間結果。
r12(IP)可以記錄對子程序調用的調用。也可以用來保存一個程序在調用子程序的中間結果。其使用詳細參見5.3.1.1。
r9的使用與平臺有關。某個平臺可以將它作任何用途,同時要有對這些用途的說明文檔。例如,在位置無關的數據模型中,可以把它指定位靜態基準(static base, SB)。也可以在本地線程存儲(thread-local storage)環境中,把它指定作線程寄存器(thread register)。在某些時候,在所有調用過程中,我們期望r9的值固定不變。若不需要r9包含不變的值,可以僅僅把用它存儲變量的值(別名v6)。
r4-r8,r10和r11典型用途是存儲函數體中的局部變量。其中,只有r1-r4能夠用於所有的Thumb指令集。但是AAPCS並不限定Thumb只能使用這些寄存器。
子程序必須保存r4-r8,r10,r11和SP的內容(若r9用於v6,也必須被保存起來)。
r12-r15均作特殊的用途,分別是IP,SP,LR和PC。
CPSR是一個全局寄存器,特性如下:
a) N,Z,C,V和Q比特位(bits 27-31),以及GE(3:0)(bits 16-19)在調用發生時和從調用返回時沒有被定義。在程序執行時出現對應狀態時,Q和GE(3:0)可能會被修改。
b) ARM6體系中,little-endian或者big-endian-8模式(臨時改變字節存儲順序)下使用比特位E(bit8)。任何程序都需要被指定字節序。發生過程調用或者從過程調用中返回時,E的設置值需要與程序的字節序保持一致。
c) 比特T(bit5)和比特J(比特24)是程序執行狀態位。只有特定的指令可以修改它們。
d) 比特A, I, F 和 M[4:0] bits (bits 0-7)是權限標誌位,只能被特權模式下的程序修改。
e) 其他的比特位被保留且不能被修改。它們的狀態沒有被定義。
5.1.1.1 bit數多於32的數據處理
一個函數的參數可能是多於32bits的基本數據類型數據。也可能一個函數返回數據類型多於32bits。這種情況下作如下處理:
a) 雙字在兩個連續的寄存器中保存。此時寄存器中的值相當於使用LDM指令從存儲器中加載出來的值。
b) 128 bits的矢量數據保存在連續4個寄存器中。此時寄存器中的值相當於使用LDM指令從存儲器中加載出來的值。
5.1.2 協處理器寄存器
協處理指令空間可以操作更多的寄存器。從某種程度上來說,這些寄存器不用於傳遞參數或者保存返回參數。協處理器寄存器的使用與此標準是兼容的。每一個協處理器都應該提供如何操作這些寄存器的規範。
注意:雖然協處理器寄存器不用於傳遞函數參數或者返回函數值,但是運行時的某些元素需要知道一個程序中所有使用的協處理器,以正確執行。
5.1.2.1 VFP寄存器規範
VFP-V2協處理器有32個單精度寄存器,s0-s31。也可以被當做16個雙精度寄存器d0-d14使用(d0對應s0+s1;d1對應s2+s3,以此類推)。此外,根據不同的實現,還包含3個或者更多系統寄存器。VFP-V3擴充16個雙精度寄存器d16-d31。但是並沒有對應擴充單精度寄存器。SIMD擴展使用VFP寄存器組。64bits數據類型時使用雙精度寄存器。對於128bits類型數據,使用四字寄存器(q0對應d0和d1;q1對應d2和d3,以此類推)。
發生過程調用時s16-s31 (d8-d15, q4-q7)必須被保存。 s0-s15 (d0-d7, q0-q3)則不需要被保存(這些寄存器可用來保存傳遞參數和函數返回值)。d16-d31 (q8-q15)在函數調用時也不需要被保存。
FPSCR是協處理器的狀態寄存器。它是全局寄存器。特性如下:
a) 條件碼比特(bits (28-31),cumulative saturation (QC) bit (27) 和cumulative exception-status bits (0-4)不需要再調用發生時保存。
b) exception-control bits (8-12), rounding mode bits (22-23) 和flush-to-zero bits (24),當調用某些影響全局狀態的函數時,可能會被修改。
c) length bits (16-18) 和 stride bits (20-21),調用函數時和從子程序返回時,必須是全0。
d) 其他bits被保留沒有被使用。對於它們的狀態,spec沒有定義。