歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux基礎 >> 關於Linux >> Linux 下函數棧幀分析

Linux 下函數棧幀分析

日期:2017/3/1 11:52:26   编辑:關於Linux

1、關於棧

對於程序,編譯器會對其分配一段內存,在邏輯上可以分為代碼段,數據段,堆,棧

代碼段:保存程序文本,指令指針EIP就是指向代碼段,可讀可執行不可寫 數據段:保存初始化的全局變量和靜態變量,可讀可寫不可執行 BSS:未初始化的全局變量和靜態變量 堆(Heap):動態分配內存,向地址增大的方向增長,可讀可寫可執行 棧(Stack):存放局部變量,函數參數,當前狀態,函數調用信息等,向地址減小的方向增長,非常非常重要,可讀可寫可執行。如下圖所示:

這裡寫圖片描述vc/yz8LJ+rOkysfWuLTTxNq05rjftdjWty0mZ3Q7tc212Na3tcTCt7620dPJ7KOsxMfDtL7NutzD98/UwcujrNW709DVu7XXus3Vu7alo6zEx8O01bu2pbXEtdjWt9KqscjVu7XXtc2ho7bUeDg2zOXPtbXEQ1BVtvjR1KOsxuTW0DxiciAvPg0KJm1kYXNoOyZndDsgvMS05sb3ZWJwo6hiYXNlIHBvaW50ZXIgo6m/ybPGzqombGRxdW871qHWuNXrJnJkcXVvO7vyJmxkcXVvO7v51rfWuNXrJnJkcXVvO6OsxuTKtdPv0uLKx8/gzay1xKGjPGJyIC8+DQombWRhc2g7Jmd0OyC8xLTmxvdlc3CjqHN0YWNrIHBvaW50ZXKjqb/Js8bOqiZsZHF1bzsg1bvWuNXrJnJkcXVvO6GjPGJyIC8+DQrSqtaqtcC1xMrHo7o8YnIgLz4NCiZtZGFzaDsmZ3Q7IGVicCDU2s60yty4xLHk1q7HsMq81tXWuM/y1bvWobXEv6rKvKOs0rK+zcrH1bu116Osy/nS1GVicLXE08PNvsrH1Nq20dW71tDRsNa308O1xKGjPGJyIC8+DQombWRhc2g7Jmd0OyBlc3DKx7vhy+bXxcr9vt21xMjr1bu6zbP21bvSxravtcSjrNKyvs3Kx8u1o6xlc3DKvNbV1rjP8tW7tqWhozxiciAvPg0KvPvPws28o6y82cnouq/K/UG199PDuq/K/UKjrM7Sw8ezxkG6r8r9zqomcmRxdW87tffTw9XfJnJkcXVvOyxCuq/K/c6qJmxkcXVvO7G7tffTw9XfJnJkcXVvO9Tyuq/K/bX308O5/bPMv8nS1NXiw7TD6Mr2o7o8YnIgLz4NCqOoMaOpz8i9q7X308PV36OoQaOptcS20dW7tcS7+da3o6hlYnCjqcjr1bujrNLUsaO05taux7DIzs7xtcTQxc+ioaM8YnIgLz4NCqOoMqOpyLu6872rtffTw9Xfo6hBo6m1xNW7tqXWuNXro6hlc3CjqbXE1rW4s7j4ZWJwo6zX986q0MK1xLv51rejqLy0sbu199PD1d9CtcTVu7XXo6mhozxiciAvPg0Ko6gzo6nIu7rz1NrV4rj2u/nWt6Oosbu199PD1d9CtcTVu7XXo6nJz7+qsdmjqNK7sOPTw3N1Yta4we6jqc/g06a1xL/VvOTTw9f3sbu199PD1d9CtcTVu7/VvOShozxiciAvPg0Ko6g0o6m6r8r9Qre1u9i686OstNO1scew1bvWobXEZWJwvLS71ri0zqq199PD1d9BtcTVu7alo6hlc3CjqaOsyrnVu7alu9a4tLqvyv1Csbu199PDx7C1xM671sOju8i7uvO199PD1d9B1Nm007vWuLS687XE1bu2pb/Jta+z9taux7C1xGVicNa1o6i/ydLU1eLDtNf2ysfS8s6q1eK49ta11Nq6r8r9tffTw8ew0ruyvbG70bnI67bR1bujqaGj1eLR+aOsZWJwus1lc3C+zba8u9a4tMHLtffTw7qvyv1Cx7C1xM671sOjrNKyvs3Kx9W7u9a4tLqvyv1CtffTw8ewtcTXtMysoaM8YnIgLz4NCtXiuPa5/bPM1NpBVCZhbXA7VLvjseDW0M2ouf3Bvcz11rjB7s3qs8mjrLy0o7o8YnIgLz4NCmxlYXZlPGJyIC8+DQpyZXQ8YnIgLz4NCtXiwb3M9da4we64/NaxsNe1477Nz+C1sdPao7o8YnIgLz4NCm1vdiAlZWJwICwgJWVzcDxiciAvPg0KcG9wICVlYnA8YnIgLz4NCjxpbWcgYWx0PQ=="這裡寫圖片描述" src="http://www.2cto.com/uploadfile/Collfiles/20160512/20160512090944709.jpg" title="\" />

2、簡單實例

開發測試環境:
Linux ubuntu 3.11.0-12-generic
gcc版本:gcc version 4.8.1 (Ubuntu/Linaro 4.8.1-10ubuntu8)
下面我們用一段代碼說明上述過程:

int bar(int c, int d)
{
    int e = c + d;
    return e;
}

int foo(int a, int b)
{
    return bar(a, b);
}

int main(void)
{
    foo(2, 3);
    return 0;
}

gcc -g Code.c ,加上-g,那麼用objdump -S a.out 反匯編時可以把C代碼和匯編代碼穿插起來顯示,這樣C代碼和匯編代碼的對應關系看得更清楚。反匯編的結果很長,以下只列出我們關心的部分。

080483ed :
int bar(int c, int d)
{
 80483ed:   55                      push   %ebp
 80483ee:   89 e5                   mov    %esp,%ebp
 80483f0:   83 ec 10                sub    $0x10,%esp
    int e = c + d;
 80483f3:   8b 45 0c                mov    0xc(%ebp),%eax
 80483f6:   8b 55 08                mov    0x8(%ebp),%edx
 80483f9:   01 d0                   add    %edx,%eax
 80483fb:   89 45 fc                mov    %eax,-0x4(%ebp)
    return e;
 80483fe:   8b 45 fc                mov    -0x4(%ebp),%eax
}
 8048401:   c9                      leave  
 8048402:   c3                      ret    

08048403 :

int foo(int a, int b)
{
 8048403:   55                      push   %ebp
 8048404:   89 e5                   mov    %esp,%ebp
 8048406:   83 ec 08                sub    $0x8,%esp
    return bar(a, b);
 8048409:   8b 45 0c                mov    0xc(%ebp),%eax
 804840c:   89 44 24 04             mov    %eax,0x4(%esp)
 8048410:   8b 45 08                mov    0x8(%ebp),%eax
 8048413:   89 04 24                mov    %eax,(%esp)
 8048416:   e8 d2 ff ff ff          call   80483ed 
}
 804841b:   c9                      leave  
 804841c:   c3                      ret    

0804841d 
: int main(void) { 804841d: 55 push %ebp 804841e: 89 e5 mov %esp,%ebp 8048420: 83 ec 08 sub $0x8,%esp foo(2, 3); 8048423: c7 44 24 04 03 00 00 movl $0x3,0x4(%esp) 804842a: 00 804842b: c7 04 24 02 00 00 00 movl $0x2,(%esp) 8048432: e8 cc ff ff ff call 8048403 return 0; 8048437: b8 00 00 00 00 mov $0x0,%eax } 804843c: c9 leave 804843d: c3 ret

整個程序的執行過程是main調用foo,foo調用bar,我們用gdb跟蹤程序的執行,直到bar函數中的int e = c + d;語句執行完畢准備返回時,這時在gdb中打印函數棧幀,因為此時棧已經生長到最大。

ZP1015@ubuntu:~/Desktop/c/Machine_Code$ gdb a.out 
GNU gdb (GDB) 7.6.1-ubuntu
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later 
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-linux-gnu".
For bug reporting instructions, please see:
...
Reading symbols from /home/ZP1015/Desktop/c/Machine_Code/a.out...done.
(gdb) start
Temporary breakpoint 1 at 0x8048423: file Code.c, line 14.
Starting program: /home/ZP1015/Desktop/c/Machine_Code/a.out 

Temporary breakpoint 1, main () at Code.c:14
14      foo(2, 3);
(gdb) s
foo (a=2, b=3) at Code.c:9
9       return bar(a, b);
(gdb) s
bar (c=2, d=3) at Code.c:3
3       int e = c + d;
(gdb) disas
Dump of assembler code for function bar:
   0x080483ed <+0>: push   %ebp
   0x080483ee <+1>: mov    %esp,%ebp
   0x080483f0 <+3>: sub    $0x10,%esp
=> 0x080483f3 <+6>: mov    0xc(%ebp),%eax
   0x080483f6 <+9>: mov    0x8(%ebp),%edx
   0x080483f9 <+12>:    add    %edx,%eax
   0x080483fb <+14>:    mov    %eax,-0x4(%ebp)
   0x080483fe <+17>:    mov    -0x4(%ebp),%eax
   0x08048401 <+20>:    leave  
   0x08048402 <+21>:    ret    
End of assembler dump.
(gdb) si
0x080483f6  3       int e = c + d;
(gdb) 
0x080483f9  3       int e = c + d;
(gdb) 
0x080483fb  3       int e = c + d;
(gdb) 
4       return e;
(gdb) 
5   }
(gdb) bt
#0  bar (c=2, d=3) at Code.c:5
#1  0x0804841b in foo (a=2, b=3) at Code.c:9
#2  0x08048437 in main () at Code.c:14
(gdb) info registers
eax            0x5  5
ecx            0xbffff724   -1073744092
edx            0x2  2
ebx            0xb7fc4000   -1208205312
esp            0xbffff658   0xbffff658
ebp            0xbffff668   0xbffff668
esi            0x0  0
edi            0x0  0
eip            0x8048401    0x8048401 
eflags         0x206    [ PF IF ]
cs             0x73 115
ss             0x7b 123
ds             0x7b 123
es             0x7b 123
fs             0x0  0
gs             0x33 51
(gdb) x/20x $esp
0xbffff658: 0x0804a000  0x08048492  0x00000001  0x00000005
0xbffff668: 0xbffff678  0x0804841b  0x00000002  0x00000003
0xbffff678: 0xbffff688  0x08048437  0x00000002  0x00000003
0xbffff688: 0x00000000  0xb7e2d905  0x00000001  0xbffff724
0xbffff698: 0xbffff72c  0xb7fff000  0x0000002a  0x00000000
(gdb) 

下面從主函數開始,一步一步分析函數調用過程:

0804841d 
: int main(void) { 804841d: 55 push %ebp 804841e: 89 e5 mov %esp,%ebp 8048420: 83 ec 08 sub $0x8,%esp foo(2, 3); 8048423: c7 44 24 04 03 00 00 movl $0x3,0x4(%esp) 804842a: 00 804842b: c7 04 24 02 00 00 00 movl $0x2,(%esp) 8048432: e8 cc ff ff ff call 8048403

要調用函數foo先要把參數准備好,第二個參數保存在esp+4指向的內存位置,第一個參數保存在esp指向的內存位置,可見參數是從右向左依次壓棧的。然後執行call指令,這個指令有兩個作用:

foo函數調用完之後要返回到call的下一條指令繼續執行,所以把call的下一條指令的地址0x8048437壓棧,同時把esp的值減4 修改程序計數器eip,跳轉到foo函數的開頭執行。
int foo(int a, int b)
{
 8048403:   55                      push   %ebp
 8048404:   89 e5                   mov    %esp,%ebp
 8048406:   83 ec 08                sub    $0x8,%esp
    return bar(a, b);
 8048409:   8b 45 0c                mov    0xc(%ebp),%eax
 804840c:   89 44 24 04             mov    %eax,0x4(%esp)
 8048410:   8b 45 08                mov    0x8(%ebp),%eax
 8048413:   89 04 24                mov    %eax,(%esp)
 8048416:   e8 d2 ff ff ff          call   80483ed 

push %ebp指令把ebp寄存器的值壓棧,同時把esp的值減4。這兩條指令合起來是把原來ebp的值保存在棧上,然後又給ebp賦了新值。在每個函數的棧幀中,ebp指向棧底,而esp指向棧頂,在函數執行過程中esp隨著壓棧和出棧操作隨時變化,而ebp是不動的,函數的參數和局部變量都是通過ebp的值加上一個偏移量來訪問,例如foo函數的參數a和b分別通過ebp+8和ebp+12來訪問。所以下面的指令把參數a和b再次壓棧,為調用bar函數做准備,然後把返回地址壓棧,調用bar函數:

080483ed :
int bar(int c, int d)
{
 80483ed:   55                      push   %ebp  
 80483ee:   89 e5                   mov    %esp,%ebp
 80483f0:   83 ec 10                sub    $0x10,%esp
    int e = c + d;
 80483f3:   8b 45 0c                mov    0xc(%ebp),%eax
 80483f6:   8b 55 08                mov    0x8(%ebp),%edx
 80483f9:   01 d0                   add    %edx,%eax
 80483fb:   89 45 fc                mov    %eax,-0x4(%ebp)
    return e;
 80483fe:   8b 45 fc                mov    -0x4(%ebp),%eax
}
 8048401:   c9                      leave  
 8048402:   c3                      ret    

08048403 :

這次又把foo函數的ebp壓棧保存,然後給ebp賦了新值,指向bar函數棧幀的棧底,通過ebp+8和ebp+12分別可以訪問參數c和d。bar函數還有一個局部變量e,可以通過ebp-4來訪問。所以後面幾條指令的意思是把參數c和d取出來存在寄存器中做加法,計算結果保存在eax寄存器中,再把eax寄存器存回局部變量e的內存單元。bar函數有一個int型的返回值,這個返回值是通過eax寄存器傳遞的,所以首先把e的值讀到eax寄存器中,然後執行leave指令,最後是ret指令。

在gdb中可以用bt命令和frame命令查看每層棧幀上的參數和局部變量,現在可以解釋它的工作原理了:如果我當前在bar函數中,我可以通過ebp找到bar函數的參數和局部變量,也可以找到foo函數的ebp保存在棧上的值,有了foo函數的ebp,又可以找到它的參數和局部變量,也可以找到main函數的ebp保存在棧上的值,因此各層函數棧幀通過保存在棧上的ebp的值串起來了。

地址0x804841b處是foo函數的返回指令:

}
 804841b:   c9                      leave  
 804841c:   c3                      ret    

重復同樣的過程,又返回到了main函數。

    return 0;
 8048437:   b8 00 00 00 00          mov    $0x0,%eax
}
 804843c:   c9                      leave  
 804843d:   c3                      ret    

整個函數執行完畢。

函數調用和返回過程中的需要注意這些規則:

參數壓棧傳遞,並且是從右向左依次壓棧。 ebp總是指向當前棧幀的棧底。 返回值通過eax寄存器傳遞。

**在沒有溢出保護機制下的編譯時,我們可以發現,所有的局部變量入棧的順序(准確來說是系統為局部變量申請內存中棧空間的順序)是正向的,即哪個變量先申明哪個變量就先得到空間,
也就是說,編譯器給變量空間的申請是直接按照變量申請順序執行的。
在有溢出保護機制下的編譯時,情況有了順序上的變化,對於每一種類型的變量來說,棧空間申請的順序都與源代碼中相反,即哪個變量在源代碼中先出現則後申請空間;而對不同的變量來說,申請的順序也不同,有例子可以看出,int型總是在char的buf型之後申請,不管源代碼中的順序如何(這應該來源於編譯器在進行溢出保護時設下的規定)。**

Copyright © Linux教程網 All Rights Reserved