Arm彙編學習筆記(六)——函數調用棧空間以及fp寄存器

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

發佈了59 篇原創文章 · 獲贊 42 · 訪問量 32萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章