1. 程式人生 > >C函式與彙編函式之間引數及返回值傳遞方法

C函式與彙編函式之間引數及返回值傳遞方法

AAPCS對ARM結構的一些標準做了定義,在這裡我們只重點介紹函式呼叫部分,如圖8所示,AAPCS為ARM的R0~R15暫存器做了定義,明確了它們在函式中的職責:

這裡寫圖片描述
圖 8 AAPCS關於ARM暫存器的定義

一、函式呼叫時的規則如下:
1、 父函式與子函式間的入口引數依次通過R0~R3這4個暫存器傳遞。父函式在呼叫子函式前先將引數存入到R0~R3中,若只有一個引數則使用R0傳遞,2個則使用R0和R1傳遞,依次類推,當超過4個引數時,其它引數通過棧傳遞。當子函式執行時,根據自身引數個數自動從R0~R3或者棧中讀取引數。
2、子函式通過R0暫存器將返回值傳遞給父函式。子函式返回時,將返回值存入R0,當返回到父函式時,父函式讀取R0獲得返回值。
3、發生函式呼叫時,R0~R3是傳遞引數的暫存器,即使是父函式沒有引數需要傳遞,子函式也可以任意更改R0~R3暫存器,無需考慮會破壞它們在父函式中儲存的數值,返回父函式前無需恢復其值。AAPCS規定,發生函式呼叫前,由父函式將R0~R3中有用的資料壓棧,然後才能呼叫子函式,以防止父函式R0~R3中的有用資料被子函式破壞。
4、 R4~R11為普通的通用暫存器,若子函式需要使用這些暫存器,則需要將這些暫存器先壓棧然後再使用,以免破壞了這些暫存器中儲存的父函式的數值,子函式返回父函式前需要先出棧恢復其數值,然後再返回父函式。AAPCS規定,發生函式呼叫時,父函式無需對這些暫存器進行壓棧處理,若子函式需要使用這些暫存器,則由子函式負責壓棧,以防止父函式R4~R11中的資料被破壞。
5、編譯器在編譯時就確定了函式間的呼叫關係,它會使函式間的呼叫遵守3、4條規定。但編譯器無法預知中斷函式的呼叫,被中斷的函式無法提前對R0~R3進行壓棧處理,因此需要在中斷函式裡對它所使用的R0~R11壓棧。對於中斷函式,不遵守第3條規定,遵守第5條規定。
6、R12暫存器在某些版本的編譯器下另有它用,使用者程式不能使用,因此我們在編寫彙編函式時也必須對它進行壓棧處理,確保它的數值不能被破壞。
7、R13暫存器是堆疊暫存器(SP),用來儲存堆疊的當前指標。
8、R14暫存器是連結暫存器(LR),用來儲存函式的返回地址。
9、R15暫存器是程式暫存器(PC),指向程式當前的地址。
上述只介紹了本手冊中使用到的情形,具體的情況在編寫作業系統程式碼時會涉及到,其它規則請請讀者自行查詢資料。

二、例子
接下來我們再通過幾個小例子熟悉一下C函式與彙編函式的呼叫過程。下面的C函式TestFunc1與彙編函式TestFunc2的功能是一樣的。
U8 TestFunc1(void)
{
U8 ucPara1;
U8 ucPara2;
U8 ucPara3;
U8 ucPara4;
U8 ucPara5;
U8 ucPara6;

ucPara1 = 1;
ucPara2 = 2;
ucPara3 = 3;
ucPara4 = 4;
ucPara5 = 5;
ucPara6 = 6;

return ucPara1 + ucPara2 + ucPara3 + ucPara4 + ucPara5 + ucPara6;

}

.func TestFunc2
TestFunc2:

STMDB R13!, {R5 - R6, R10}    @R5,R6,R10暫存器壓棧
LDR R1, =1
LDR R3, =2
LDR R4, =3
LDR R5, =4
LDR R6, =5
LDR R10, =6

ADD R0, R1, R3
ADD R0, R0, R4
ADD R0, R0, R5
ADD R0, R0, R6
ADD R0, R0, R10

LDMIA R13!, {R5 - R6, R10}    @R5,R6,R10暫存器出棧

.endfunc

TestFunc2函式使用了R0、R1、R3、R4、R5、R6、R10共7個暫存器,遵循AAPCS規則,在使用R0、R1和R3之前並沒有對它們壓棧,但對R5、R6和R10暫存器進行了壓棧儲存,在函式返回前又出棧還原了這3個暫存器,這樣TestFunc2函式返回到它的父函式之後,R5、R6和R10暫存器的數值是沒有改變的,而R0、R1和R3則分別被改寫為了1、2和3。

下面我們再來看看C函式TestFunc3調用匯編函式TestFunc4完成1+2的運算。
U8 TestFunc3(void)
{
return TestFunc4(1, 2);
}

.func TestFunc4

TestFunc4:

ADD R0, R0, R1
BX R14;

.endfunc

TestFunc3函式在呼叫TestFunc4函式前已經將引數1和2分別存入R0和R1,並將返回地址存入到R14中,然後才跳轉到TestFunc4函式,發生函式呼叫。這時程式將執行TestFunc4函式,它將R0和R1相加,將結果放入R0,需要通過R0將返回值返回給TestFunc3函式。此時R14中儲存的就是返回TestFunc3函式的返回地址,最後TestFunc4函式跳轉到R14就返回到了TestFunc3函式,TestFunc3函式從R0就可以取出TestFunc4函式計算的結果了。

下面我們再來看看彙編函式TestFunc5呼叫C函式TestFunc6完成1+2的運算。

.func TestFunc5
TestFunc5:

MOV R0, #1
MOV R1, #2
SUB R13, R13, #4
STR R14, [R13]
BL TestFunc6
LDR R14, [R13]
ADD R13, R13, #4
BX R14

.endfunc

U8 TestFunc6(U8 ucPara1, U8 ucpara2)
{
return ucPara1 + ucPara2;
}
TestFunc5函式先將引數1和2存入R0和R1暫存器,準備呼叫TestFunc6函式並傳遞入口引數,然後將R14暫存器壓棧,以防止使用BL指令時存入的R14返回地址破壞R14原有的資料,然後呼叫TestFunc6函式。在呼叫TestFunc6函式時BL指令會自動將“LDR R14, [R13]”這條指令的地址存入R14,這樣就開始執行TestFunc6函數了。TestFunc6函式會自動從R0和R1暫存器中取出引數,將計算結果存入R0,通過R0將返回值返回給TestFunc5函式。TestFunc6函式跳轉回TestFunc5函式後,TestFunc5函式從棧中恢復原有的R14暫存器,完成函式呼叫,此時R0中的數值就是TestFunc6函式的計算結果。

當函式比較簡單,不需要壓棧僅使用暫存器便可以完成運算的時候,那麼下面的TestFunc7函式,它的返回值是多少?
U8* TestFunc7(void)
{

U8 ucPara1;

ucPara1 = 1;

return &ucPara1;

}
按照上面的分析,對於這個簡單的函式,編譯器是不會為區域性變數ucPara1分配記憶體空間的,ucPara1只會儲存在暫存器中,因此無從談起它的地址。但這個這麼簡單的函式卻偏偏要獲取這個僅在暫存器中的區域性變數的地址,遇到這種情況,編譯器在編譯時會特別為ucPara1專門在棧中分配記憶體,因此也就可以獲取到它的地址了。
當然,這個函式沒有任何意義,僅是舉一個例子,而且寫C語言時要避免發生這種情況,因為TestFunc7函式返回的是棧內區域性變數的地址,當TestFunc7函式執行完後,ucPara1這個區域性變數所在的棧空間已經被釋放,這個棧空間很可能已經被其它變數佔用,如果這時候還使用這個地址的話就可能會導致系統崩潰,新手要避免產生這個錯誤。