小例子一步一步解釋“函式呼叫過程中棧的變化過程”
1 問題描述
在此之前,我對C中函式呼叫過程中棧的變化,僅限於瞭解有好幾種引數的入棧順序,其中的按照形參逆序入棧是比較常見的,也僅限於瞭解到這個程度,但到底在一個函式A裡面,呼叫另一個函式B的過程中,函式A的棧是怎麼變化的,實參是怎麼傳給函式B的,函式B又是怎麼給函式A返回值的,這些問題都不能很明白的一步一步解釋出來。下面,便是用一個小例子來解釋這個過程,主要回答的問題是如下幾個:
1、函式A在執行到呼叫函式B的語句之前,棧的結構是什麼樣子?
2、函式A執行呼叫函式B這一條語句的過程中,A的棧是怎樣的?
3、在執行呼叫函式B語句時,實參是呼叫函式A來傳入棧,還是被調函式B來進行入棧?
4、實參的入棧順序是怎樣的?
5、執行呼叫函式B的過程中,函式A的棧又是怎樣的,B的呢?
6、函式B執行完之後,發生了什麼事情,怎樣把結果傳給了函式A中的呼叫語句處的引數(比如:A中int c = B_fun(...)這樣的語句)?
7、呼叫函式的語句結束後,怎樣繼續執行A中之後的語句?
大概的問題也就這些,其實也就是整個過程中一些自己認為比較重要的步驟。接下來詳細描述這個過程,以下先給出自己的C測試程式碼,和對應的反彙編程式碼。
2 測試程式碼
2.1 C測試程式碼
C測試程式碼如下:(程式碼中自己關注的幾個地方是L14 15 16 17)
1int 2 fun(int *x, int *y) 3 { 4 int temp = *x; 5 *x = *y; 6 *y = temp; 7 8 return *x + *y; 9 } 10 11 int 12 main(void) 13 { 14 int a = 5; 15 int b = 9; 16 int c = 3; 17 c = fun(&a, &b); 18 a = 7; 19 b = 17; 20 return 0; 21 }
主要關注的地方是:
1、main中定義int變數 a b c 時,是怎樣的定義順序?
2、L17 的過程。
3、進入fun之後,的整個棧的結構。
2.2 彙編測試程式碼
1 080483b4 <fun>: 2 80483b4: 55 push %ebp 3 80483b5: 89 e5 mov %esp,%ebp 4 80483b7: 83 ec 10 sub $0x10,%esp 5 80483ba: 8b 45 08 mov 0x8(%ebp),%eax 6 80483bd: 8b 00 mov (%eax),%eax 7 80483bf: 89 45 fc mov %eax,-0x4(%ebp) 8 80483c2: 8b 45 0c mov 0xc(%ebp),%eax 9 80483c5: 8b 10 mov (%eax),%edx 10 80483c7: 8b 45 08 mov 0x8(%ebp),%eax 11 80483ca: 89 10 mov %edx,(%eax) 12 80483cc: 8b 45 0c mov 0xc(%ebp),%eax 13 80483cf: 8b 55 fc mov -0x4(%ebp),%edx 14 80483d2: 89 10 mov %edx,(%eax) 15 80483d4: 8b 45 08 mov 0x8(%ebp),%eax 16 80483d7: 8b 10 mov (%eax),%edx 17 80483d9: 8b 45 0c mov 0xc(%ebp),%eax 18 80483dc: 8b 00 mov (%eax),%eax 19 80483de: 01 d0 add %edx,%eax 20 80483e0: c9 leave 21 80483e1: c3 ret 22 23 080483e2 <main>: 24 80483e2: 55 push %ebp 25 80483e3: 89 e5 mov %esp,%ebp 26 80483e5: 83 ec 18 sub $0x18,%esp 27 80483e8: c7 45 f4 05 00 00 00 movl $0x5,-0xc(%ebp) 28 80483ef: c7 45 f8 09 00 00 00 movl $0x9,-0x8(%ebp) 29 80483f6: c7 45 fc 03 00 00 00 movl $0x3,-0x4(%ebp) 30 80483fd: 8d 45 f8 lea -0x8(%ebp),%eax 31 8048400: 89 44 24 04 mov %eax,0x4(%esp) 32 8048404: 8d 45 f4 lea -0xc(%ebp),%eax 33 8048407: 89 04 24 mov %eax,(%esp) 34 804840a: e8 a5 ff ff ff call 80483b4 <fun> 35 804840f: 89 45 fc mov %eax,-0x4(%ebp) 36 8048412: c7 45 f4 07 00 00 00 movl $0x7,-0xc(%ebp) 37 8048419: c7 45 f8 11 00 00 00 movl $0x11,-0x8(%ebp) 38 8048420: b8 00 00 00 00 mov $0x0,%eax 39 8048425: c9 leave 40 8048426: c3 ret
3 分析過程
3.1 main棧
1、L24 執行push %ebp:main函式先儲存之前函式(在執行到main之前的初始化函式,具體的細節可以參考程式設計師的自我修養這本書有講整個程式執行的流程)的幀指標%ebp。此時,即進入了main函式的棧,圖示描述如下
描述 |
內容 |
註釋 |
main:%esp |
被儲存的start函式的%ebp |
每個函式開始前,先儲存之前函式的幀指標%ebp |
2、L25 執行mov %esp,%ebp:步驟1已經儲存了之前函式的%ebp,接下來需要修改函式main的棧幀指標,指示main棧的開始,即修改%ebp,使其內容為暫存器%esp的內容(C描述為:%ebp = %esp),此時棧結構如下:
描述 |
內容 |
註釋 |
main:%esp(%ebp) |
被儲存的start函式的%ebp |
每個函式開始前,先儲存之前函式的幀指標%ebp |
3、L26 執行sub $0x18,%esp:此處即修改main函式棧的大小。由於linux裡,棧增長的方向是從大到小,所以這裡是%esp = %esp - $0x18;關於為什麼減去$0x18,即十進位制的24,深入理解計算機系統一書P154這樣描述:“GCC堅持一個x86程式設計指導方針,也就是一個函式使用的所有棧空間必須是16位元組的整數倍。包括儲存%ebp值的4個位元組和返回值的4個位元組,採用這個規則是為了保證訪問資料的嚴格對齊。”,所以這裡main函式棧的大小 = 24 + 4 + 4 = 32(分配的24,儲存%ebp的4,儲存返回值的4)。此時棧結構如下:
描述 | 內容 | 註釋 |
main:%ebp | 被儲存的start函式的%ebp | 每個函式開始前,先儲存之前函式的幀指標%ebp |
%esp |
4、 L27 movl $0x5,-0xc(%ebp);L28 movl $0x9,-0x8(%ebp);L29 movl $0x3,-0x4(%ebp)這三行是定義的變數a b c。此時棧結構如下,可以看出來,變數的定義順序不是按照在main裡面宣告的順序定義的,這個我不是很懂,求指導。
描述 | 內容 | 註釋 |
main:%ebp | 被儲存的start函式的%ebp | 每個函式開始前,先儲存之前函式的幀指標%ebp |
%ebp - 0x4 | 3 | c = 3 |
%ebp - 0x8 | 9 | b = 9 |
%ebp - 0xc | 5 | a = 5 |
%esp |
5、L30 lea -0x8(%ebp),%eax; L31 mov %eax,0x4(%esp)這兩行是把變數b的地址賦值到%esp + 4,棧結構如下:
描述 | 內容 | 註釋 |
main:%ebp | 被儲存的start函式的%ebp | 每個函式開始前,先儲存之前函式的幀指標%ebp |
%ebp - 0x4 | 3 | c = 3 |
%ebp - 0x8 | 9 | b = 9 |
%ebp - 0xc | 5 | a = 5 |
%esp + 0x4 | &b | 變數b的地址 |
%esp |
6、L32 lea -0xc(%ebp),%eax; L33 mov%eax,(%esp)這兩行是把變數a的地址賦值到%esp,棧結構如下:
描述 | 內容 | 註釋 |
main:%ebp | 被儲存的start函式的%ebp | 每個函式開始前,先儲存之前函式的幀指標%ebp |
%ebp - 0x4 | 3 | c = 3 |
%ebp - 0x8 | 9 | b = 9 |
%ebp - 0xc | 5 | a = 5 |
%esp + 0x4 | &b | 變數b的地址 |
%esp | &a | 變數a的地址 |
7、L34 call 80483b4 <fun>;可以看出這一行,即呼叫的是fun(int *, int *)函式,而且也從第6步知道實參是呼叫函式傳入棧,且是逆序傳入。這裡call指令會把之後指令的地址壓入棧,即L35的指令地址804840f。(從彙編程式碼看不出來這一步壓棧的過程,但根據後續分析,這樣是正確的,書上也是這麼描述call指令的,怎樣能直觀的看到棧的變化,我不懂,哪位知道可以留言告訴我)此時棧的結構如下:
描述 | 內容 | 註釋 |
main:%ebp | 被儲存的start函式的%ebp | 每個函式開始前,先儲存之前函式的幀指標%ebp |
%ebp - 0x4 | 3 | c = 3 |
%ebp - 0x8 | 9 | b = 9 |
%ebp - 0xc | 5 | a = 5 |
&b | 變數b的地址 | |
&a | 變數a的地址 | |
%esp | 804840f | 返回地址 |
到這一步,關於main函式棧的情況分析就到這裡,接下來進入fun函式進行分析。
3.2 fun函式棧
1、L2 push%ebp:同main函式第一步一樣,先儲存之前函式的棧幀,即儲存main函式的幀指標%ebp,此時棧情況如下:
描述 | 內容 | 註釋 |
main:%ebp | 被儲存的start函式的%ebp | 每個函式開始前,先儲存之前函式的幀指標%ebp |
%ebp - 0x4 | 3 | c = 3 |
%ebp - 0x8 | 9 | b = 9 |
%ebp - 0xc | 5 | a = 5 |
&b | 變數b的地址 | |
&a | 變數a的地址 | |
804840f | 返回地址 | |
fun棧開始 | 被儲存的main函式的%ebp |
2、L3 mov %esp,%ebp:同上述main描述裡面步驟2,修改暫存器%ebp。棧如下:
描述 | 內容 | 註釋 |
main: | 被儲存的start函式的%ebp | 每個函式開始前,先儲存之前函式的幀指標%ebp |
3 | c = 3 | |
9 | b = 9 | |
5 | a = 5 | |
&b | 變數b的地址 | |
&a | 變數a的地址 | |
804840f | 返回地址 | |
fun棧開始(%esp與%ebp) | 被儲存的main函式的%ebp |
3、L4 sub $0x10,%esp:同上述main描述步驟3,修改函式fun的棧大小,(不明白的是這裡怎麼修改的大小為十進位制16,這樣加上其他的最後不是16的整數倍?)此時棧如下:
描述 | 內容 | 註釋 |
main: | 被儲存的start函式的%ebp | 每個函式開始前,先儲存之前函式的幀指標%ebp |
3 | c = 3 | |
9 | b = 9 | |
5 | a = 5 | |
&b | 變數b的地址 | |
&a | 變數a的地址 | |
804840f | 返回地址 | |
fun棧開始(%ebp) | 被儲存的main函式的%ebp | |
%esp |
4、L5 mov 0x8(%ebp),%eax;L6 mov (%eax),%eax ;L7 mov%eax,-0x4(%ebp):這三行功能分別是把%eax = &a; %eax = a; %ebp - 0x4 = a;對應的是fun函式語句int temp = *a;其中,L7會改變棧的情況,此時棧如下:
描述 | 內容 | 註釋 |
main: | 被儲存的start函式的%ebp | 每個函式開始前,先儲存之前函式的幀指標%ebp |
3 | c = 3 | |
9 | b = 9 | |
5 | a = 5 | |
&b | 變數b的地址 | |
&a | 變數a的地址 | |
804840f | 返回地址 | |
fun:%ebp | 被儲存的main函式的%ebp | |
%ebp - 0x4 | 5 | a = 5 |
%esp |
5、L8 mov 0xc(%ebp),%eax;L9 mov (%eax),%edx;L10 mov 0x8(%ebp),%eax; L11 mov %edx,(%eax)對應功能分別是:get &b; get b; get &a; a = b。其中,只有L11會修改棧內容,棧內容如下:
描述 | 內容 | 註釋 |
main: | 被儲存的start函式的%ebp | 每個函式開始前,先儲存之前函式的幀指標%ebp |
3 | c = 3 | |
9 | b = 9 | |
9 | a = 9(修改了a的值) | |
&b | 變數b的地址 | |
&a | 變數a的地址 | |
804840f | 返回地址 | |
fun:%ebp | 被儲存的main函式的%ebp | |
%ebp - 0x4 | 5 | a = 5 |