程序運行之棧空間


一般來講,應用程序使用的內存空間里有如下的默認區域:

1 棧:用於維護函數調用的上下文。棧通常在用戶空間的最高地址出分配,通常有數兆字節的大小

2 堆:堆是用來容納應用程序動態分配的內存區域。比如使用malloc和new分配內存就從堆里分配。

3 可執行文件鏡像:這里存儲着可執行文件在內存里的映射

首先來介紹棧:

在操作系統中,棧總是向下增長的,棧頂由稱為esp的寄存器進行定位,壓棧的操作使棧頂的地址減小,彈出的操作使棧頂的地址增大。棧保存了一個函數調用所需要維護的信息,這通常稱為堆棧幀或活動記錄。堆棧幀包括如下幾個方面的內容:

1 函數返回地址和參數

2 臨時變量:包括函數的非靜態局部變量以及編譯器自動生成的其他臨時變量

3 保存的上下文:包括在函數調用前后需要保持不變的寄存器。

 

在i386中,一個函數的活動記錄用ebp和esp這兩個寄存器划定范圍。esp寄存器始終指向棧的頂部,ebp寄存器指向了函數活動記錄的一個固定位置,ebp寄存器又稱為幀指針。常見的活動記錄如下圖所示:

在ebp之前首先是這個函數的返回地址,它的地址是ebp-4, 再往前是壓入棧中的參數,它們的地址分別是ebp-8,ebp-12等等。ebp所直接指向的的數據是調用該函數前ebp的值,這樣函數在返回的時候,ebp可以讀取這個值恢復到調用前的值。所以一個i386的程序調用順序如下:

1 把所有或者一部分參數壓入棧中,如果有其他參數沒有入棧,那么使用某些特定的寄存器傳遞

2 把當前指令的下一條指令的地址壓入棧中

3 跳轉到函數體執行

其中2,3由執行call一起執行。跳轉到函數體之后就開始執行函數。I386函數體的標准開頭過程如下:

1 push ebp: 把ebp壓入棧中,也就是old ebp

2 move ebp,esp: ebp=esp(ebp指向棧頂,此時棧頂就是old ebp)

3 sub esp,xxx  在棧上分配xxx字節的臨時空間

4 push xxx 保存名為xxx的寄存器

把ebp壓入棧中,是為了在函數返回的時候便於恢復以前的ebp值,函數返回的時候過程正好相反。

1 pop xxx

2 mov esp,ebp 恢復esp同時收回局部變量空間

3 pop ebp:從棧中恢復保存的ebp的值

4 ret: 從棧中取得返回地址,並跳轉到該位置。

我們用一個簡單的函數調用然后查看匯編代碼來看下這個過程

#include <stdio.h>

int foo()

{      

        return 123;

}      

int main()

{      

        int ret;

        ret=foo();

        return 1;

}  

objdump –d stack_test.o 可以看到如下結果

00000000000005fa <foo>:

 5fa:        55                           push   %ebp

 5fb:        48 89 e5             mov    %esp,%ebp

 5fe:        b8 7b 00 00 00         mov    $0x7b,%eax

 603:       5d                           pop    %ebp

 604:       c3                           retq  

 

在main中首先是將ebp進棧,然后是將esp賦值為ebp。同時將esp減去0x10,也就是開辟了0x10的棧空間。同樣的過程在foo對應的匯編也可以看到。mov    $0x7b,%eax 這條指令是將返回值123賦值給eax,同時將ebp值出棧

0000000000000605 <main>:

 605:       55                           push   %ebp

 606:       48 89 e5             mov    %esp,%ebp

 609:       48 83 ec 10            sub    $0x10,%esp

 60d:       b8 00 00 00 00         mov    $0x0,%eax

 612:       e8 e3 ff ff ff               callq  5fa <foo>

 617:       89 45 fc               mov    %eax,-0x4(%ebp)

 61a:       b8 01 00 00 00         mov    $0x1,%eax

 61f:        c9                           leaveq

 620:       c3                           retq  

我們在把函數變更下使得foo函數帶參數

#include <stdio.h>

int foo(int i, int j)

{      

        return 123;

}      

int main()

{      

        int ret;

        ret=foo(1,2);

        return 1;

}

再看下匯編代碼:可以看到參數i和j的入棧過程,首先在main中將參數值分別賦給esi和edi寄存器。然后在foo中分別將edi和esi的值存入到ebp+0x04和ebp-0x08的地址中。

00000000000005fa <foo>:

 5fa:        55                           push   %ebp

 5fb:        48 89 e5             mov    %esp,%ebp

 5fe:        89 7d fc               mov    %edi,-0x4(%ebp)

 601:       89 75 f8               mov    %esi,-0x8(%ebp)

 604:       b8 7b 00 00 00         mov    $0x7b,%eax

 609:       5d                           pop    %ebp

 60a:       c3                           retq  

 

000000000000060b <main>:

 60b:       55                           push   %ebp

 60c:       48 89 e5             mov    %esp,%ebp

 60f:        48 83 ec 10            sub    $0x10,%esp

 613:       be 02 00 00 00         mov    $0x2,%esi

 618:       bf 01 00 00 00          mov    $0x1,%edi

 61d:       e8 d8 ff ff ff               callq  5fa <foo>

 622:       89 45 fc               mov    %eax,-0x4(%ebp)

 625:       b8 01 00 00 00         mov    $0x1,%eax

 62a:       c9                           leaveq

 62b:       c3                           retq  

 62c:       0f 1f 40 00             nopl   0x0(%rax)

以一個框圖來表示調用關系

函數返回值傳遞

除了參數的傳遞外,函數與調用方的另一個交互就是返回值。前面可以看到eax寄存器是傳遞返回值的通道。但是eax本身只有4個字節,那么大於4字節的返回值是如何傳遞的呢。對於返回5-8字節的情況,需要聯合eax和edx聯合返回的方式進行。eax存儲低4字節,edx存儲高4字節。但是對於超過8個字節的情況,就比較復雜了。以下面的例子為例:

 

#include <stdio.h>

typedef struct big_thing

{

        char buf[128];

}big_thing;

 

big_thing return_test()

{

        big_thing b;

        b.buf[0]=0;

        return b;

}

 

int main()

{

        big_thing n=return_test();

}

 

對應的匯編代碼: 將棧上的一個地址ebp-1D0h存儲在eax中,然后將eax入棧,再調用return_test

lea eax,[ebp-1D0h]

push eax

call _return_test

這就相當於將eax的值作為了return_test的參數。但是實際上return_test是沒有參數的,因此這個也被稱為隱含參數。

下面這4行是一個整體,rep movs是一個復合指令,意思是重復movs指令知道ecx寄存器為0,於是rep movs a,b的意思就是將b指向位置的若干個雙字節拷貝到a指向的位置上。相當於memcpy(ebp-88h,eax,0x20*4) ebp-88h就是n的地址

mov ecx ,20h

mov esi,eax

lea edi,[ebp-88h]

rep movs  dword ptr es:[edi],dword ptr [esi]

 

再來看下return_test的實現。

lea esi, [ebp-88h]

mov edi,dword ptr [ebp+8]

rep movs dword ptr es:[edi], dword ptr [esi]

 

ebp+8指向函數的參數,ebp-88h指向的是變量b的位置,因此rep movs dword ptr es:[edi], dword ptr [esi]相當於memcpy([ebp+8],&b,128)。也就是將變量b地址的內容拷貝到傳入的參數也就是ebp-1D0h這個地址去。

那么整個流程如下:

1 main在棧中開辟一段空間,並將這塊空間的一部分作為傳遞返回值的臨時對象,稱為temp

2 將temp對象的地址作為隱藏參數傳遞給return_test參數

3 return_test將數據拷貝到temp對象

4 return_test返回之后,main函數將eax指向的temp對象拷貝給n

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM