Arm上函數調用的規則在ARM System Developer's Guide文檔中的ATPCS部分有詳細的定義,這裏主要通過函數調用過程中函數棧的情況來說明fp和sp等寄存器的作用。有關ATPCS的詳細內容可以去文檔中看。
fp叫做frame pointer寄存器,即棧幀指針寄存器;sp叫做stack pointer寄存器,即棧指針寄存器。那麼它們具體的作用是什麼呢?
首先,大家知道每個進程都有自己獨立的棧空間,進程中有千千萬萬的函數調用,這些函數共享進程的這個棧空間,那麼問題就來了,函數運行過程中會有非常多的入棧出棧的過程,當函數返回backtrace的時候怎樣能精確定位到返回地址呢?還有子函數所保存的一些寄存器的內容?這樣就有了棧幀的概念,即每個函數所使用的棧空間是一個棧幀,所有的棧幀就組成了這個進程完整的棧。而fp就是棧基址寄存器,指向當前函數棧幀的棧底,sp則指向當前函數棧幀的棧頂。通過sp和fp所指出的棧幀可以恢復出母函數的棧幀,以此類推就可以backtrace出所有函數的調用順序。
那一個函數的棧幀具體的範圍什麼呢?fp和sp具體應該指向什麼位置呢?請看下圖:
圖1
上圖描述的是ARM的棧幀佈局方式,main stack frame爲調用函數的棧幀,func1 stack frame爲當前函數(被調用者)的棧幀,棧底在高地址,棧向下增長。此圖是網上的圖,理論上應該是上圖的格式,fp、sp、lr和pc這四個寄存器是非常特殊的寄存器,它們記錄了當前正在運行的函數一些重要信息,在剛進入一個新的函數開始執行的時候,它們保存的是上個函數的信息,需要將它們入棧保存起來,這很重要!這些並沒有定義在ATPCS中,ATPCS規定的是函數調用的時候參數如何傳遞,以及函數返回值的保存等。上面的這些個人覺得是一種默契,定義函數現場的保存及恢復,這些默契包括ATPCS都是人爲的一種約束,目的是爲了保證程序運行中不會出錯,具體怎樣實現應該是不同的編譯器不盡相同。
下面我爲了驗證arm的gcc編譯器的實現,自己寫了個小實驗程序:
main.c:
#include <stdio.h>
int func(int i);
int main(void)
{
int i = 25;
func(i);
return 0;
}
func.c
int func(int i)
{
int a = 2;
return a * i;
}
main.c中調用了func函數,而func函數的實現在func.c文件中。下面是用arm-linux-androideabi-gcc編譯後的執行文件反匯編出來的代碼:
Disassembly of section .text:
0000822c <func>:
822c: e52db004 push {fp} ; (str fp, [sp, #-4]!)
8230: e28db000 add fp, sp, #0
8234: e24dd014 sub sp, sp, #20
8238: e50b0010 str r0, [fp, #-16]
823c: e3a03002 mov r3, #2
8240: e50b3008 str r3, [fp, #-8]
8244: e51b3008 ldr r3, [fp, #-8]
8248: e51b2010 ldr r2, [fp, #-16]
824c: e0030392 mul r3, r2, r3
8250: e1a00003 mov r0, r3
8254: e24bd000 sub sp, fp, #0
8258: e49db004 pop {fp} ; (ldr fp, [sp], #4)
825c: e12fff1e bx lr
00008260 <main>:
8260: e92d4800 push {fp, lr}
8264: e28db004 add fp, sp, #4
8268: e24dd008 sub sp, sp, #8
826c: e3a03019 mov r3, #25
8270: e50b3008 str r3, [fp, #-8]
8274: e51b0008 ldr r0, [fp, #-8]
8278: ebffffeb bl 822c <func>
827c: e3a03000 mov r3, #0
8280: e1a00003 mov r0, r3
8284: e24bd004 sub sp, fp, #4
8288: e8bd8800 pop {fp, pc}
上面的彙編代碼可以看到,並沒有想上面圖中所畫的,將fp, sp, lr, pc全部都入棧,而是隻入棧這四個寄存器中有改動的。fp是肯定要保存的,它指向的是每個函數棧幀的棧基址,而sp一般不用入棧,因爲它的值一般保存在fp中,因爲剛進入一個函數的時候,將上個函數的fp入棧保存以後,當前函數的棧空間應該是空的,fp應該指向與sp相同的位置,然後纔會對sp做減法來分配棧空間保存臨時變量。而如果當前函數中沒有對其它函數的調用的時候,是不會對lr寄存器做修改的,所以也就不用保存了。
但是上面對main函數和func函數的fp指針所指向的位置也不完全相同,main函數中fp指向的是上個fp保存的內存地址,而func中的fp指向的是sp相同的位置。但是隻要恢復的時候相對應不出錯就可以了,上面也說過ATPCS這些規定都是人爲的一種約束,保證backtrace的時候可以把正確的內容恢復到寄存器中,具體怎麼實現並沒有特別死板的定義。
另外一個比較重要的東西就是出入棧的順序,在ARM指令系統中是地址遞減棧,入棧操作的參數入棧順序是從右到左依次入棧,而參數的出棧順序則是從左到右的你操作。包括push/pop和LDMFD/STMFD等。
比如指令 push {fp, sp, lr, pc}執行的結果就是圖1中棧的樣子,pc被首先入棧存在高地址,從右到左依次入棧,fp存在低地址。
這些是比較細節和基礎的東西,同時也是需要搞清楚的。
參考鏈接:
1. http://www.linuxidc.com/Linux/2013-03/81247.htm
2. http://www.cnblogs.com/chyl411/p/4579053.html
3. http://www.cnblogs.com/fanzhidongyzby/p/5250116.html