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型之後申請,不管源代碼中的順序如何(這應該來源於編譯器在進行溢出保護時設下的規定)。**