ARM 過(guò)程調用標準
APCS 簡(jiǎn)介(ARM 過(guò)程調用標準) |
- 介紹
- 寄存器命名
- 設計關(guān)鍵
- 一致性
- 棧
- 回溯結構
- 實(shí)際參數
- 函數退出
- 建立?;厮萁Y構
- APCS 標準
- 對編碼有用的東西
本文引用地址:http://dyxdggzs.com/article/201611/317232.htm
介紹
APCS,ARM 過(guò)程調用標準(ARMProcedureCallStandard),提供了緊湊的編寫(xiě)例程的一種機制,定義的例程可以與其他例程交織在一起。最顯著(zhù)的一點(diǎn)是對這些例程來(lái)自哪里沒(méi)有明確的限制。它們可以編譯自 C、 Pascal、也可以是用匯編語(yǔ)言寫(xiě)成的。APCS 定義了:
- 對寄存器使用的限制。
- 使用棧的慣例。
- 在函數調用之間傳遞/返回參數。
- 可以被‘回溯’的基于棧的結構的格式,用來(lái)提供從失敗點(diǎn)到程序入口的函數(和給予的參數)的列表。
APCS 不一個(gè)單一的給定標準,而是一系列類(lèi)似但在特定條件下有所區別的標準。例如,APCS-R (用于 RISC OS)規定在函數進(jìn)入時(shí)設置的標志必須在函數退出時(shí)復位。在 32 位標準下,并不是總能知道進(jìn)入標志的(沒(méi)有 USR_CPSR),所以你不需要恢復它們。如你所預料的那樣,在不同版本間沒(méi)有相容性。希望恢復標志的代碼在它們未被恢復的時(shí)候可能會(huì )表現失常...
如果你開(kāi)發(fā)一個(gè)基于 ARM 的系統,不要求你去實(shí)現 APCS。但建議你實(shí)現它,因為它不難實(shí)現,且可以使你獲得各種利益。但是,如果要寫(xiě)用來(lái)與編譯后的 C 連接的匯編代碼,則必須使用 APCS。編譯器期望特定的條件,在你的加入(add-in)代碼中必須得到滿(mǎn)足。一個(gè)好例子是 APCS 定義 a1 到 a4 可以被破壞,而 v1 到 v6 必須被保護?,F在我確信你正在撓頭并自言自語(yǔ)“a 是什么? v 是什么?”。所以首先介紹 APCS-R 寄存器定義...
寄存器命名
APCS 對我們通常稱(chēng)為 R0 到 R14 的寄存器起了不同的名字。使用匯編器預處理器的功能,你可以定義 R0 等名字,但在你修改其他人寫(xiě)的代碼的時(shí)候,最好還是學(xué)習使用 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 是指令指針的簡(jiǎn)寫(xiě)。
這些名字不是由標準的 Acorn 的 objasm(版本 2.00)所定義的,但是 objasm 的后來(lái)版本,和其他匯編器(比如 Nick Robert 的 ASM)定義了它們。要定義一個(gè)寄存器名字,典型的,你要在程序最開(kāi)始的地方使用RN宏指令(directive):
a1 RN 0a2 RN 1a3 RN 2...等...r13 RN 13sp RN 13r14 RN 14lr RN r14pc RN 15這個(gè)例子展示了一些重要的東西:
- 寄存器可以定義多個(gè)名字 - 你可以定義‘r13’和‘sp’二者。
- 寄存器可以定義自前面定義的寄存器 - ‘lr’定義自叫做‘r14’的寄存器。
(對于 objasm 是正確的,其他匯編器可能不是這樣)
設計關(guān)鍵
- 函數調用應當快、小、和易于(由編譯器來(lái))優(yōu)化。
- 函數應當可以妥善處理多個(gè)棧。
- 函數應當易于寫(xiě)可重入和可重定位的代碼;主要通過(guò)把可寫(xiě)的數據與代碼分離來(lái)實(shí)現。
- 但是最重要的是,它應當簡(jiǎn)單。這樣匯編編程者可以非常容易的使用它的設施,而調試者能夠非常容易的跟蹤程序。
一致性
程序的遵循 APCS 的部分在調用外部函數時(shí)被稱(chēng)為“一致”。在程序執行期間的所有時(shí)候都遵循 APCS (典型的,由編譯器生成的程序)被稱(chēng)為“嚴格一致”。協(xié)議指出,假如你遵守正確的進(jìn)入和退出參數,你可以在你自己的函數范圍內做你需要的任何事情,而仍然保持一致。這在有些時(shí)候是必須的,比如在寫(xiě) SWI 偽裝(veneers)的時(shí)候使用了許多給實(shí)際的 SWI 調用的寄存器。
棧
棧是鏈接起來(lái)的‘楨’的一個(gè)列表,通過(guò)一個(gè)叫做‘回溯結構’的東西來(lái)鏈接它們。這個(gè)結構存儲在每個(gè)楨的高端。按遞減地址次序分配棧的每一塊。寄存器sp總是指向在最當前楨中最低的使用的地址。這符合傳統上的滿(mǎn)降序棧。在 APCS-R 中,寄存器sl持有一個(gè)棧限制,你遞減sp不能低于它。在當前棧指針和當前棧之間,不應該有任何其他 APCS 函數所依賴(lài)的東西,在被調用的時(shí)候,函數可以為自己設置一個(gè)棧塊。可以有多個(gè)棧區(chunk)。它們可以位于內存中的任何地址,這里沒(méi)有提供規范。典型的,在可重入方式下執行的時(shí)候,這將被用于為相同的代碼提供多個(gè)棧;一個(gè)類(lèi)比是 FileCore,它通過(guò)簡(jiǎn)單的設置‘狀態(tài)’信息和并按要求調用相同部分的代碼,來(lái)向當前可獲得的 FileCore 文件系統(ADFS、RAMFS、IDEFS、SCSIFS 等)提供服務(wù)。
回溯結構
寄存器fp(楨指針)應當是零或者是指向?;厮萁Y構的列表中的最后一個(gè)結構,提供了一種追溯程序的方式,來(lái)反向跟蹤調用的函數。回溯結構是:
地址高端保存代碼指針 [fp] fp 指向這里返回 lr 值 [fp, #-4] 返回 sp 值 [fp, #-8] 返回 fp 值 [fp, #-12] 指向下一個(gè)結構 [保存的 sl][保存的 v6] [保存的 v5] [保存的 v4] [保存的 v3] [保存的 v2][保存的 v1][保存的 a4][保存的 a3][保存的 a2][保存的 a1][保存的 f7] 三個(gè)字[保存的 f6] 三個(gè)字[保存的 f5] 三個(gè)字[保存的 f4] 三個(gè)字地址低端
這個(gè)結構包含 4 至 27 個(gè)字,在方括號中的是可選的值。如果它們存在,則必須按給定的次序存在(例如,在內存中保存的 a3 下面可以是保存的 f4,但 a2-f5 則不能存在)。浮點(diǎn)值按‘內部格式’存儲并占用三個(gè)字(12 字節)。
fp 寄存器指向當前執行的函數的?;厮萁Y構。返回 fp 值應當是零,或者是指向由調用了這個(gè)當前函數的函數建立的?;厮萁Y構的一個(gè)指針。而這個(gè)結構中的返回 fp 值是指向調用了調用了這個(gè)當前函數的函數的函數的?;厮萁Y構的一個(gè)指針;并以此類(lèi)推直到第一個(gè)函數。
在函數退出的時(shí)候,把返回連接值、返回 sp 值、和返回 fp 值裝載到 pc、sp、和 fp 中。
#include所以,我們可以檢查 fp 并參看給函數‘two’的結構,它指向給函數‘one’的結構,它指向給‘main’的結構,它指向零來(lái)終結。在這種方式下,我們可以反向追溯整個(gè)程序并 確定我們是如何到達當前的崩潰點(diǎn)的。值得指出‘zero’函數,因為它已經(jīng)被執行并退出了,此時(shí)我們正在做它后面的打印,所以它曾經(jīng)在回溯結構中,但現在 不在了。值得指出的還有對于給定代碼不太可能總是生成象上面那樣的一個(gè) APCS 結構。原因是不調用任何其他函數的函數不要求完全的 APCS 頭部。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;}當它在屏幕上輸出消息的時(shí)候,APCS 回溯結構將是:fp ----> two_structurereturn linkreturn spreturn fp ----> one_structure... return linkreturn spreturn fp ----> main_structure... return linkreturn spreturn fp ----> 0...
為了更細致的理解,下面是代碼是 Norcroft C v4.00 為上述代碼生成的...
AREA |C$code|, CODE, READONLYIMPORT |__main||x$codeseg|B |__main|DCB &6d,&61,&69,&6eDCB &00,&00,&00,&00DCD &ff000008IMPORT |x$stack_overflow|EXPORT oneEXPORT mainmainMOV ip, spSTMFD sp!, {fp,ip,lr,pc}SUB fp, ip, #4CMPS sp, slBLLT |x$stack_overflow|BL oneMOV a1, #0LDMEA fp, {fp,sp,pc}^DCB &6f,&6e,&65,&00DCD &ff000004EXPORT zeroEXPORT twooneMOV ip, spSTMFD sp!, {fp,ip,lr,pc}SUB fp, ip, #4CMPS sp, slBLLT |x$stack_overflow|BL zeroLDMEA fp, {fp,sp,lr}B twoIMPORT |_printf|twoADD a1, pc, #L000060-.-8B |_printf|L000060DCB &6d,&61,&69,&6eDCB &2e,&2e,&2e,&6fDCB &6e,&65,&2e,&2eDCB &2e,&74,&77,&6fDCB &0a,&00,&00,&00zeroMOVS pc, lrAREA |C$data||x$dataseg|END這個(gè)例子不遵從 32 為體系。APCS-32 規定只是簡(jiǎn)單的說(shuō)明了標志不需要被保存。所以刪除 LDM 的‘^’后綴,并在函數 zero 中刪除 MOVS 的‘S’后綴。則代碼就與遵從 32-bit 的編譯器生成的一樣了。
保存代碼指針包含這條設置回溯結構的指令(STMFD ...)的地址再加上 12 字節。記住,對于 26-bit 代碼,你需要去除其中的 PSR 來(lái)得到實(shí)際的代碼地址。
現在我們查看剛進(jìn)入函數的時(shí)候:
- pc總是包含下一個(gè)要被執行的指令的位置。
- lr(總是)包含著(zhù)退出時(shí)要裝載到pc中的值。在 26-bit 位代碼中它還包含著(zhù) PSR。
- sp指向當前的棧塊(chunk)限制,或它的上面。這是用于復制臨時(shí)數據、寄存器和類(lèi)似的東西到其中的地方。在 RISC OS 下,你有可選擇的至少 256 字節來(lái)擴展它。
- fp要么是零,要么指向回溯結構的最當前的部分。
- 函數實(shí)參布置成(下面)描述的那樣。
實(shí)際參數
APCS 沒(méi)有定義記錄、數組、和類(lèi)似的格局。這樣語(yǔ)言可以自由的定義如何進(jìn)行這些活動(dòng)。但是,如果你自己的實(shí)現實(shí)際上不符合 APCS 的精神,那么將不允許來(lái)自你的編譯器的代碼與來(lái)自其他編譯器的代碼連接在一起。典型的,使用 C 語(yǔ)言的慣例。- 前 4 個(gè)整數實(shí)參(或者更少!)被裝載到 a1 - a4。
- 前 4 個(gè)浮點(diǎn)實(shí)參(或者更少!)被裝載到 f0 - f3。
- 其他任何實(shí)參(如果有的話(huà))存儲在內存中,用進(jìn)入函數時(shí)緊接在 sp 的值上面的字來(lái)指向。換句話(huà)說(shuō),其余的參數被壓入棧頂。所以要想簡(jiǎn)單。最好定義接受 4 個(gè)或更少的參數的函數。
函數退出
通過(guò)把返回連接值傳送到程序計數器中來(lái)退出函數,并且:- 如果函數返回一個(gè)小于等于一個(gè)字大小的值,則把這個(gè)值放置到 a1 中。
- 如果函數返回一個(gè)浮點(diǎn)值,則把它放入 f0 中。
- sp、fp、sl、v1-v6、和 f4-f7 應當被恢復(如果被改動(dòng)了)為包含在進(jìn)入函數時(shí)它所持有的值。
我測試了故意的破壞寄存器,而結果是(經(jīng)常在程序完全不同的部分)出現不希望的和奇異的故障。 - ip、lr、a2-a4、f1-f3 和入棧的這些實(shí)參可以被破壞。
建立?;厮萁Y構
對于一個(gè)簡(jiǎn)單函數(固定個(gè)數的參數,不可重入),你可以用下列指令建立一個(gè)?;厮萁Y構:function_name_labelMOV ip, spSTMFD sp!, {fp,ip,lr,pc}SUB fp, ip, #4這個(gè)片段(來(lái)自上述編譯后的程序)是最基本的形式。如果你要破壞其他不可破壞的寄存器,則你應該在這個(gè) STMFD 指令中包含它們。
下一個(gè)任務(wù)是檢查??臻g。如果不需要很多空間(小于 256 字節)則你可以使用:
CMPS sp, slBLLT |x$stack_overflow|這是 C 版本 4.00 處理溢出的方式。在以后的版本中,你要調用 |__rt_stkovf_split_small|。
接著(zhù)做你自己的事情...
通過(guò)下面的指令完成退出:
LDMEA fp, {fp,sp,pc}^還有,如果你入棧了其他寄存器,則也在這里重新裝載它們。選擇這個(gè)簡(jiǎn)單的 LDM 退出機制的原因是它比分支到一個(gè)特殊的函數退出處理器(handler)更容易和更合理。
用在回溯中的對這個(gè)協(xié)議的一個(gè)擴展是把函數名字嵌入到代碼中。緊靠在函數(和MOV ip, sp)的前面的應該是:
DCD &ff0000xx這里的‘xx’是函數名字符串的長(cháng)度(包括填充和終結符)。這個(gè)字符串是字對齊、尾部填充的,并且應當被直接放置在 DCD &ff....的前面。
所以一個(gè)完整的?;厮荽a應當是:
DCB "my_function_name", 0, 0, 0, 0DCD &ff000010my_function_nameMOV ip, spSTMFD sp!, {fp, ip, lr, pc}SUB fp, ip, #4CMPS sp, sl ; 如果你不使用棧BLLT |x$stack_overflow| ; 則可以省略...處理...LDMEA fp, {fp, sp, pc}^要使它遵從 32-bit 體系,只須簡(jiǎn)單的省略最后一個(gè)指令的‘^’。注意你不能在一個(gè)編譯的 26-bit 代碼中使用這個(gè)代碼。實(shí)際上,你可以去除它,但這不是我愿意打賭的事情。
如果你不使用棧,并且你不需要保存任何寄存器,并且你不調用任何東西,則沒(méi)有必要設置 APCS 塊(但在調試階段對跟蹤問(wèn)題仍是有用的)。在這種情況下你可以:
my_simple_function...處理...MOVS pc, lr(再次,對 32 位 APCS 使用 MOV 而不是 MOVS,但是不要冒險與 26 位代碼連接)。
APCS 標準
總的來(lái)說(shuō),有多個(gè)版本的 APCS (實(shí)際上是 16 個(gè))。我們只關(guān)心在 RISC OS 上可能遇到的。APCS-A
就是 APCS-Arthur;由早期的 Arthur 所定義。它已經(jīng)被廢棄,原因是它有不同的寄存器定義(對于熟練的 RISC OS 程序員它是某種異類(lèi))。它用于在 USR 模式下運行的 Arthur 應用程序。不應該使用它。
- sl = R13, fp = R10, ip = R11, sp = R12, lr = R14, pc = R15。
- PRM (p4-411) 中說(shuō)“用r12作為sp,而不是在體系上更自然的r13,是歷史性的并先于 Arthur 和 RISC OS 二者。”
- 棧是分段的并可按需要來(lái)擴展。
- 26-bit 程序計數器。
- 不在 FP 寄存器中傳遞浮點(diǎn)實(shí)參。
- 不可重入。標志必須被恢復。
APCS-R
就是 APCS-RISC OS。用于 RISC OS 應用程序在 USR 模式下進(jìn)行操作;或在 SVC 模式下的模塊/處理程序。
- sl = R10, fp = R11, ip = R12, sp = R13, lr = R14, pc = R15。
- 它是唯一的最通用的 APCS 版本。因為所有編譯的 C 程序都使用 APCS-R。
- 顯式的棧限制檢查。
- 26-bit 程序計數器。
- 不在 FP 寄存器中傳遞浮點(diǎn)實(shí)參。
- 不可重入。標志必須被恢復。
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 寄存器中傳遞浮點(diǎn)實(shí)參。
- 不可重入。標志必須被恢復。
APCS-32
它是 APCS-2(-R 和 -U)的一個(gè)擴展,允許 32-bit 程序計數器,并且從執行在 USR 模式下的一個(gè)函數中退出時(shí),允許標志不被恢復。其他事情同于 APCS-R。
Acorn C 版本 5 支持生成 32-bit 代碼;在用于廣域調試的 32 位工具中,它是最完整的開(kāi)發(fā)發(fā)行。一個(gè)簡(jiǎn)單的測試是要求你的編譯器導出匯編源碼(而不是制作目標代碼)。你不應該找到:
MOVS PC, R14
或者
LDMFD R13!, {Rx-x, PC}^
對編碼有用的東西
首先要考慮的是該死的 26/32 位問(wèn)題。 簡(jiǎn)單的說(shuō),不轉彎抹角絕對沒(méi)有方法為兩個(gè)版本的 APCS 匯編同一個(gè)通用代碼。但是幸運的這不是問(wèn)題。APCS 標準不會(huì )突然改變。RISC OS 的 32 位版本也不會(huì )立刻變異。所以利用這些,我們可以設計一種支持兩種版本的方案。這將遠遠超出 APCS,對于 RISC OS 的 32 位版本你需要使用 MSR 來(lái)處理狀態(tài)和模式位,而不是使用 TEQP。許多現存的 API 實(shí)際上不需要保護標志位。所以在我們的 32 版本中可以通過(guò)把MOVS PC,...變成MOV PC,...,和把LDM {...}^變成LDM {...},并重新建造來(lái)解決。objasm 匯編器(v3.00 和以后)有一個(gè){CONFIG}變量可以是26或32??梢允褂盟ㄔ旌?..my_function_nameMOV ip, spSTMFD sp!, {fp, ip, lr, pc}SUB fp, ip, #4...處理...[ {CONFIG} = 26LDMEA fp, {fp, sp, pc}^|LDMEA fp, {fp, sp, pc}]我未測試這個(gè)代碼。它(或類(lèi)似的東西)好象是保持與兩個(gè)版本的 APCS 相兼容的最佳方式,也是對 RISC OS 的不同版本,26 位版本和將來(lái)的 32 位版本的最佳方法。
測試是否處于 32 位? 如果你要求你的代碼有適應性,有一個(gè)最簡(jiǎn)單的方法來(lái)確定處理器的 PC 狀態(tài):
TEQ PC, PC ; 對于 32 位是 EQ;對于 26 位是 NE
使用它你可以確定:
- 26 位 PC,可能是 APCS-R 或 APCS-32。
- 32 位 PC,不能 APCS-R。所有 26-bit 代碼(TEQP 等)面臨著(zhù)失敗!
評論