深刻理解linux核心呼叫棧、棧幀結構
摘自:https://blog.csdn.net/koozxcv/article/details/49998237
我們知道,棧溢位通常是因為遞迴呼叫層次太深導致,那麼為什麼遞迴呼叫層次太深回導致棧溢位呢,解決這個問題
之前我們先看一下與函式呼叫有關的棧的基本概念:
1. 每一個執行緒擁有一個呼叫棧結構(call stack),呼叫棧存放該執行緒的函式呼叫資訊
2. 程式中每一個未完成執行的函式對應一個棧幀(stack frame),或者一個更響亮的名字,過程的活動記錄,棧幀
中儲存函式區域性變數、傳遞給被調函式引數等資訊
3. 棧底對應高地址,棧頂對應低地址,棧由記憶體高地址向低地址生長
對於下面這段程式碼:
1 void f(int a) { 2 printf("%d\n", a); 3 } 4 void g() { 5 int a = 2; 6 f(a); 7 }
當程式執行到進入printf時,對應的呼叫棧(模擬)是這樣的:
可以看到,正常的函式呼叫會使棧幀指標向下增長,而每個程序的呼叫棧大小都有一個限制,當呼叫層次過深導致棧幀
指標越過呼叫棧的下界時,就是導致棧溢位. 因此我們寫遞迴呼叫的程式碼時千萬要注意,儘量保證遞迴呼叫的層次不要
太深!!!再一個就是不要在棧上定義太大的陣列!!!
1. 下界溢位
我權且稱其為下界溢位,是因為這種溢位是說棧幀指標到達了棧地址空間的下界,上面已經分析了這種溢位,下面通過一
段具體的程式碼來看一下:
1 #include <stdio.h> 2 #include <string.h> 3 4 void call() 5 { 6 int a[1024]; 7 printf("hello call! \n"); 8 call(); 9 } 10 int main(int argc, char *argv[]) { 11 call(); 12 }
編譯這段程式碼:
可以通過設定斷點,在gdb中檢視每一個呼叫棧幀指標的變化,這裡給個最終結果
gdb ./a.out
(gdb) r
Program received signal SIGSEGV, Segmentation fault.
(gdb) bt #0 call () at overflow.c:7 #1 0x08048426 in call () at overflow.c:8 #2 0x08048426 in call () at overflow.c:8 ... #36 0x08048426 in call () at overflow.c:8 #2032 0x08048426 in call () at overflow.c:8
(gdb) info frame Stack level 0, frame at 0xbf800520: eip = 0x8048415 in call (overflow.c:7); saved eip 0x8048426 called by frame at 0xbf801540 source language c. Arglist at 0xbf800518, args: Locals at 0xbf800518, Previous frame's sp is 0xbf800520 Saved registers: ebp at 0xbf800518, eip at 0xbf80051c (gdb) info proc mappings process 5560 Mapped address spaces: Start Addr End Addr Size Offset objfile 0x08048000 0x08049000 0x1000 0x0 a.out 0x08049000 0x0804a000 0x1000 0x0 a.out 0x0804a000 0x0804b000 0x1000 0x1000 a.out ... 0xb7fff000 0xb8000000 0x1000 0x20000 /lib/i386-linux-gnu/ld-2.15.so 0xbf801000 0xc0000000 0x7ff000 0x0 [stack]
a. 通過bt可以列印呼叫棧,可以看出共發生了2033次呼叫後棧溢位
b. 通過info proc mappings可以看出程序棧的地址空間為0xbf801000~0xc0000000
c. 通過info frame可以看出當前棧幀的情況,可以看出,最後一次呼叫棧幀指標已經指向0xbf800520,前一幀
指向0xbf801540,剩餘棧空間已經不足容納一個棧幀,導致訪問非法地址空間,發生段錯誤
2. 上界溢位
下界溢位比較常見,而且實現起來很簡單,只要遞迴層次足夠深或者在函式內定義足夠大的非靜態陣列就可以了,那麼
如何實現上界溢位,方法肯定還是需要足夠深的"呼叫",只不過需要每次“呼叫”棧幀指標都向棧底增長,可是正常的函式
呼叫都是向下增長的啊. 怎麼辦呢,我們想一下函式呼叫過程,向棧中壓入引數,返回地址,函式返回時彈出返回地址
方法就隱藏在這裡,我們模擬函式呼叫,通過修改函式返回地址指向自身函式入口地址,那麼每次函式返回時,都會彈
出返回地址,這樣其實我們並沒有呼叫函式,而是通過修改返回地址的方法模擬呼叫過程,因此不存在壓入返回地址,但
是函式返回時會"以為"自己是通過正常的函式呼叫被呼叫的,會主動從棧中彈出返回地址,這樣就繞過了規則,使得每次
"呼叫自身"都會將棧幀指標+4(32位系統). 分析到這,理論上可以實現棧指標向上增長了,光說不練假把式,上程式碼
1 #include <stdio.h> 2 #include <string.h> 3 4 #define ADDRESS(a, f) *(int*)a = ((int)f); 5 char t[] = { 0, 0, 0, 0, 0 }; 6 7 void call() 8 { 9 char c; 10 char *p = &c; 11 printf("hello call! \n"); 12 strcpy(p + 17, t); /*覆蓋返回地址*/ 13 } 14 int main() { 15 int a = 2; 16 ADDRESS(t, call); /*將call地址存入t*/ 17 call(); 18 }
肯定有人問,你是怎麼知道返回地址存放在哪的,那個+17是怎麼得到的,答案很簡單,反彙編
0804843c <call>: 804843c: 55 push %ebp 804843d: 89 e5 mov %esp,%ebp 804843f: 83 ec 28 sub $0x28,%esp 8048442: 8d 45 f3 lea -0xd(%ebp),%eax /*得到c的地址,看以看到c的地址是ebp - 13*/ 8048445: 89 45 f4 mov %eax,-0xc(%ebp) 8048448: c7 04 24 28 85 04 08 movl $0x8048528,(%esp) 804844f: e8 cc fe ff ff call 8048320 <puts@plt> 8048454: 8b 45 f4 mov -0xc(%ebp),%eax 8048457: 83 c0 11 add $0x11,%eax 804845a: c7 44 24 04 1c a0 04 movl $0x804a01c,0x4(%esp) 8048461: 08 8048462: 89 04 24 mov %eax,(%esp) 8048465: e8 a6 fe ff ff call 8048310 <strcpy@plt> 804846a: c9 leave 804846b: c3 ret
根據反彙編程式碼,可以得到當前棧幀:
由此分析返回地址等於ebp + 4 = ebp - 13 + 17 = &c + 17
看下執行結果:
Program received signal SIGSEGV, Segmentation fault. 0xb7ea6ea7 in ?? () from /lib/i386-linux-gnu/libc.so.6 (gdb) bt #0 0xb7ea6ea7 in ?? () from /lib/i386-linux-gnu/libc.so.6 #1 0x0804846a in call () at overflow.c:12 #2 0x0804843c in frame_dummy () Cannot access memory at address 0xc0000000
(gdb) info frame Stack level 0, frame at 0xbfffffd0: eip = 0xb7ea6ea7; saved eip 0x804846a called by frame at 0xc0000000 Arglist at 0xbffffff8, args: Locals at 0xbffffff8, Previous frame's sp is 0xbfffffd0 Saved registers: eip at 0xbfffffcc
(gdb) info proc mappings process 6332 Mapped address spaces: Start Addr End Addr Size Offset objfile 0x08048000 0x08049000 0x1000 0x0 a.out 0x08049000 0x0804a000 0x1000 0x0 a.out 0x0804a000 0x0804b000 0x1000 0x1000 a.out ... 0xb7fff000 0xb8000000 0x1000 0x20000 /lib/i386-linux-gnu/ld-2.15.so 0xbffdf000 0xc0000000 0x21000 0x0 [stack]
最終棧幀指標到了0xc0000000,導致上溢,和我們分析的過程是吻合的
實驗環境:
CPU指令集 | x86 |
作業系統 | Ubuntu 12.04 |
核心版本 | Linux 3.2.0 |
gcc版本 | gcc-4.7 |