,ARM 過程調用標準(程序員
ARM編程
Procedure數組
Callide
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
這個例子展現了一些重要的東西:
程序的遵循 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 來獲得實際的代碼地址。
如今咱們查看剛進入函數的時候:
APCS 沒有定義記錄、數組、和相似的格局。這樣語言能夠自由的定義如何進行這些活動。可是,若是你本身的實現實際上不符合 APCS 的精神,那麼將不容許來自你的編譯器的代碼與來自其餘編譯器的代碼鏈接在一塊兒。典型的,使用 C 語言的慣例。
經過把返回鏈接值傳送到程序計數器中來退出函數,而且:
在 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 (其實是 16 個)。咱們只關心在 RISC OS 上可能遇到的。
APCS-A
就是 APCS-Arthur;由早期的 Arthur 所定義。它已經被廢棄,緣由是它有不一樣的寄存器定義(對於熟練的 RISC OS 程序員它是某種異類)。它用於在 USR 模式下運行的 Arthur 應用程序。不該該使用它。
sl = R13, fp = R10, ip = R11, sp = R12, lr = R14, pc = R15。
r12
做爲 sp
,而不是在體系上更天然的 r13
,是歷史性的並先於 Arthur 和 RISC OS 兩者。」APCS-R
就是 APCS-RISC OS。用於 RISC OS 應用程序在 USR 模式下進行操做;或在 SVC 模式下的模塊/處理程序。
sl = R10, fp = R11, ip = R12, sp = R13, lr = R14, pc = R15。
APCS-U
就是 APCS-Unix,Acorn 的 RISCiX 使用它。它用於 RISCiX 應用程序(USR 模式)或內核(SVC 模式)。
sl = R10, fp = R11, ip = R12, sp = R13, lr = R14, pc = R15。
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
使用它你能夠肯定: