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