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)
1 int 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 |
%esp |
6、L12 mov 0xc(%ebp),%eax; L13 mov-0x4(%ebp),%edx;L14 mov %edx, (%eax):功能分別對應get &b; %edx = temp;b = a。其中L13會修改棧內容,具體棧情況更改如下:
描述 | 內容 | 註釋 |
main: | 被保存的start函數的%ebp | 每個函數開始前,先保存之前函數的幀指針%ebp |
3 | c = 3 | |
5 | b = 5(修改了b的值) | |
9 | a = 9(修改了a的值) | |
&b | 變量b的地址 | |
&a | 變量a的地址 | |
804840f | 返回地址 | |
fun:%ebp | 被保存的main函數的%ebp | |
%ebp - 0x4 | 5 | a = 5 |
%esp |
7、然後就是L15,L16,L17,L18這4行分別得到&a, a, &b, b。這些都不會造成棧內容的變化。
L19 add %edx, %eax會計算出a + b的值,並把結果保存在寄存器%eax,也即返回值在%eax(這裏大家都清楚,函數如果有返回值,一般都是保存在%eax)
8、L10 leave:深入理解計算機系統一書P151這樣描述leave指令:
movl %ebp, %esp
popl %ebp
以下分兩步來描述:
即先把寄存器%ebp賦值給%esp,其中%ebp保存的是之前main函數的%ebp,這一步修改了%esp的內容,即棧情況會發生變化。這一步之後棧情況爲:
描述 | 內容 | 註釋 |
main: | 被保存的start函數的%ebp | 每個函數開始前,先保存之前函數的幀指針%ebp |
3 | c = 3 | |
5 | b = 5 | |
9 | a = 9 | |
&b | 變量b的地址 | |
&a | 變量a的地址 | |
804840f | 返回地址 | |
%esp | 被保存的main函數的%ebp |
然後是popl %ebp,即把%ebp的內容恢復爲之前main函數的幀指針,經過這一步之後%ebp指向了main棧的開始處:如下表示
描述 | 內容 | 註釋 |
main:%ebp | 被保存的start函數的%ebp | 每個函數開始前,先保存之前函數的幀指針%ebp |
3 | c = 3 | |
5 | b = 5 | |
9 |
a = 9 |
|
&b | 變量b的地址 | |
&a | 變量a的地址 | |
804840f | 返回地址 | |
%esp(%ebp) | 被保存的main函數的%ebp |
9、L21 ret:從棧中彈出地址,並跳轉到這個位置。棧即如下:
描述 | 內容 | 註釋 |
main:%ebp | 被保存的start函數的%ebp | 每個函數開始前,先保存之前函數的幀指針%ebp |
3 | c = 3 | |
5 | b = 5 | |
9 | a = 9 | |
&b | 變量b的地址 | |
%esp | &a | 變量a的地址 |
到這裏fun函數即執行完,然後又跳轉到main函數開始執行後續指令。後續L35行用到的%eax即之前fun函數的返回值,L35 L36 L37都用到了%ebp,此時%ebp已經指向了main函數的幀指針,後面已經沒有什麼可以描述的了,最後還會修改變量a b c 的值,只需要相應的修改棧中內容即可,沒有什麼可說的了。
到這裏全部分析過程就結束了。希望能夠幫助到跟我一樣對過程調用不熟悉的朋友