深入理解任務堆棧

本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/gumbour/archive/2009/08/30/4500424.aspx

 

先來看這一個小函數,猜猜他的運行結果(VC6環境)?

#include <stdio.h>

void  b()
{
    int data[10];
    printf("helloworld!/r/n");
    data[11]-=5;
}

int main()
{
    b();
    return 0;
}

堆棧溢出,肯定不正常,馬上有人叫起來了。

沒錯, 那麼結果是什麼呢,爲什麼會不停打印helloworld呢,我們將用堆棧揭開他的奧祕。

且看main函數彙編代碼。  

很簡單,  L12  調用b函數,   L13對返回值賦0.

這裏有個很關鍵的東東: call

call包含2部分操作,call的下一條指令地址入棧,跳轉,也就是從效果來說,包含push  0040108D 和 jmp  00401005兩條操作。 假如,你打開內存窗口,你會看到,堆棧裏已經有0040108D 這個值了。

10:   int main()
11:   {
         ...........
12:       b();
00401088   call        @ILT+0(b) (00401005)
13:       return 0;
0040108D   xor         eax,eax
14:   }

再來看函數b

當你把  printf("helloworld!/r/n"); 替換爲 printf("%08x!/r/n",data[11]);時,你會發現,程序在不停的打印0040108D!, 顯而易見,你修改的data[11]其實就是函數b的返回值地址,而data[11] -= 5;更是巧妙的利用 call    00401005 這條指令正好是5個字節的特點,將返回地址正好修改到了 0040108D ,也就是說函數返回時會再次調用函數b。每次b()都會把返回值改爲b返回的地址,導致b()被不停的調用。

爲什麼data[11]正好是函數的返回值呢,讓我們來看堆棧和任務有和關係
 

    任務(線程)都有一個堆棧,任務創建時創建,任務撤銷時撤銷。 任務的創建本質上包含2點。

    1  任務資源的分配(任務TCB和任務堆棧),很多嵌入式操作系統把TCB和堆棧是分配在一起的,比如Vxworks操作系統,其任務ID,堆棧基地址,TCB指針其實指向同一塊內存。 創建任務時要指定任務大小,分配堆棧空間其實是一個特殊的malloc函數,他從堆棧空間分配,而不是從系統空間分配內存。任務堆棧windows下默認比較大,嵌入式OS則比較小,經常64k左右。 而局部變量就保存在堆棧中,當訪問局部變量越界時,就發生了我們常說的"堆棧被踩了",堆棧被踩得話後果嚴重,輕則導致某次運行結果不對(這種問題很難定位),重則導致程序崩潰,例如把上面程序改爲data[11]-=4,則程序直接崩潰。

    2  任務的初始化,包含2部分,任務TCB的初始化,並且把TCB和操作系統關聯。

        TCB中包含任務的很多東西,   比如任務擁有的信號量的鏈表,文件描述符的鏈表,CPU寄存器的值(任務切換時用的),任務優先級,堆棧地址,任務名稱等等,這些都需要初始化。初始化完成之後,操作系統會把這個任務TCB假如調度隊列,如果加入調度隊列時任務狀態是就緒,那麼當他拿到CPU時就可以直接運行了。

    堆棧中包含任務的棧幀,也就是說在函數調用鏈(A call B,B call C,C call D,D call E),那麼堆棧中,ABCDE函數分別對應自己的一段棧幀。以E爲例  E的棧幀包含A函數的傳入參數,函數返回值,局部變量和臨時保存的寄存器值。

     函數棧幀在主調函數和被掉函數中分配,在函數返回時釋放,這就是爲什麼局部變量地址在函數返回後其值可能失效。

     例如 下面代碼FuncB分配的函數棧幀在FuncB執行完後又被分配給FuncC,FuncC中很可能會踩到FuncB曾經的局部變量。

      FuncA{

         FuncB();

         FuncC();

      }

    任務(線程)的棧以及上面函數b的棧爲下圖。

 

 

 

    *debug版本的函數b其實除了data[10],還在局部變量位置分配了一部分內存用來做調試,不過我們不用關係他。

    *爲什麼是data[11],而不是data[10]/data[12]或者其他? x86下編譯器函數入口一般會有2條指令。

      push  ebp

      move ebp,esp

      其實就是將ebp作爲幀指針來用(函數幀即爲棧中一個函數所擁有的一段內存)。

      而這樣就可以在函數中採用ebp-XXX表示局部變量,用ebp+XXX來表示傳入參數。 函數中經常會有一些push操作,

      採用esp對局部變量和參數尋址遠不如用ebp來的省事了,因爲esp是經常變化的,而ebp是相對橫的的。

    

 

本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/gumbour/archive/2009/08/30/4500424.aspx

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章