深入理解任务堆栈
先來看這一個小函數(shù),猜猜他的運行結(jié)果(VC6環(huán)境)?
#include <stdio.h>
void? b()
{
??? int data[10];
????printf("helloworld!/r/n");
??? data[11]-=5;
}
int main()
{
??? b();
??? return 0;
}
堆棧溢出,肯定不正常,馬上有人叫起來了。
沒錯, 那么結(jié)果是什么呢,為什么會不停打印helloworld呢,我們將用堆棧揭開他的奧秘。
且看main函數(shù)匯編代碼。??
很簡單,? L12? 調(diào)用b函數(shù),?? L13對返回值賦0.
這里有個很關(guān)鍵的東東: call
call包含2部分操作,call的下一條指令地址入棧,跳轉(zhuǎn),也就是從效果來說,包含push? 0040108D 和 jmp? 00401005兩條操作。 假如,你打開內(nèi)存窗口,你會看到,堆棧里已經(jīng)有0040108D 這個值了。
10:?? int main()
11:?? {
???????? ...........
12:?????? b();
00401088?? call??????? @ILT+0(b) (00401005)
13:?????? return 0;
0040108D?? xor???????? eax,eax
14:?? }
再來看函數(shù)b
當(dāng)你把? printf("helloworld!/r/n"); 替換為 printf("%08x!/r/n",data[11]);時,你會發(fā)現(xiàn),程序在不停的打印0040108D!, 顯而易見,你修改的data[11]其實就是函數(shù)b的返回值地址,而data[11] -= 5;更是巧妙的利用 call????00401005 這條指令正好是5個字節(jié)的特點,將返回地址正好修改到了 0040108D ,也就是說函數(shù)返回時會再次調(diào)用函數(shù)b。每次b()都會把返回值改為b返回的地址,導(dǎo)致b()被不停的調(diào)用。
?
?
為什么data[11]正好是函數(shù)的返回值呢,讓我們來看堆棧和任務(wù)有和關(guān)系
?
??? 任務(wù)(線程)都有一個堆棧,任務(wù)創(chuàng)建時創(chuàng)建,任務(wù)撤銷時撤銷。 任務(wù)的創(chuàng)建本質(zhì)上包含2點。
??? 1? 任務(wù)資源的分配(任務(wù)TCB和任務(wù)堆棧),很多嵌入式操作系統(tǒng)把TCB和堆棧是分配在一起的,比如Vxworks操作系統(tǒng),其任務(wù)ID,堆棧基地址,TCB指針其實指向同一塊內(nèi)存。 創(chuàng)建任務(wù)時要指定任務(wù)大小,分配堆棧空間其實是一個特殊的malloc函數(shù),他從堆棧空間分配,而不是從系統(tǒng)空間分配內(nèi)存。任務(wù)堆棧windows下默認(rèn)比較大,嵌入式OS則比較小,經(jīng)常64k左右。 而局部變量就保存在堆棧中,當(dāng)訪問局部變量越界時,就發(fā)生了我們常說的"堆棧被踩了",堆棧被踩得話后果嚴(yán)重,輕則導(dǎo)致某次運行結(jié)果不對(這種問題很難定位),重則導(dǎo)致程序崩潰,例如把上面程序改為data[11]-=4,則程序直接崩潰。
?
??? 2? 任務(wù)的初始化,包含2部分,任務(wù)TCB的初始化,并且把TCB和操作系統(tǒng)關(guān)聯(lián)。
??????? TCB中包含任務(wù)的很多東西,?? 比如任務(wù)擁有的信號量的鏈表,文件描述符的鏈表,CPU寄存器的值(任務(wù)切換時用的),任務(wù)優(yōu)先級,堆棧地址,任務(wù)名稱等等,這些都需要初始化。初始化完成之后,操作系統(tǒng)會把這個任務(wù)TCB假如調(diào)度隊列,如果加入調(diào)度隊列時任務(wù)狀態(tài)是就緒,那么當(dāng)他拿到CPU時就可以直接運行了。
?
??? 堆棧中包含任務(wù)的棧幀,也就是說在函數(shù)調(diào)用鏈(A call B,B call C,C call D,D call E),那么堆棧中,ABCDE函數(shù)分別對應(yīng)自己的一段棧幀。以E為例? E的棧幀包含A函數(shù)的傳入?yún)?shù),函數(shù)返回值,局部變量和臨時保存的寄存器值。
?
???? 函數(shù)棧幀在主調(diào)函數(shù)和被掉函數(shù)中分配,在函數(shù)返回時釋放,這就是為什么局部變量地址在函數(shù)返回后其值可能失效。
???? 例如 下面代碼FuncB分配的函數(shù)棧幀在FuncB執(zhí)行完后又被分配給FuncC,FuncC中很可能會踩到FuncB曾經(jīng)的局部變量。
????? FuncA{
???????? FuncB();
???????? FuncC();
????? }
??? 任務(wù)(線程)的棧以及上面函數(shù)b的棧為下圖。
?
??? *debug版本的函數(shù)b其實除了data[10],還在局部變量位置分配了一部分內(nèi)存用來做調(diào)試,不過我們不用關(guān)系他。
??? *為什么是data[11],而不是data[10]/data[12]或者其他? x86下編譯器函數(shù)入口一般會有2條指令。
????? push? ebp
????? move ebp,esp
????? 其實就是將ebp作為幀指針來用(函數(shù)幀即為棧中一個函數(shù)所擁有的一段內(nèi)存)。
????? 而這樣就可以在函數(shù)中采用ebp-XXX表示局部變量,用ebp+XXX來表示傳入?yún)?shù)。 函數(shù)中經(jīng)常會有一些push操作,
????? 采用esp對局部變量和參數(shù)尋址遠(yuǎn)不如用ebp來的省事了,因為esp是經(jīng)常變化的,而ebp是相對橫的的。
?
總結(jié)
- 上一篇: USB OTG设计
- 下一篇: uCOS任务堆栈的深入分析(转)