前言
在學習C語言的程序中,大家是否會存在一些困惑?比如:
- 區域變數是如何創建的?
- 為什么說區域變數未初始化時,其中存盤的時隨機值?
- 函式到底時如何傳參的?實參傳遞的順序又是怎樣的?
- 形參和實參之間有著什么關系?
- 函式呼叫結束后,結果是如何回傳的?
這些問題大家有沒有感覺到,貌似每天都在接觸,但是真要去解答這些問題,還真不知道怎么去回答,
不過各位同志切莫慌張!本文將會詳細講解函式堆疊幀的創建與銷毀的程序,通過觀察整個程序,以上的這些問題都能得到答案,
函式堆疊幀是什么?
堆疊這個名詞相信學習過C語言或者資料結構的小伙伴不會陌生,C語言中的堆疊(也被稱作堆疊)其操作方式與資料結構中的堆疊相似,有著后進先出(LIFO)的特點,在C語言中,堆疊的地址是由高地址向低地址生長的,
那么函式堆疊幀和堆疊區有什么關系呢?當我們在函式呼叫時,系統會在在堆疊區中為該函式開辟的一塊空間,該函式中的區域變數就保存在這塊空間中,這塊空間就被稱作函式堆疊幀,函式堆疊幀主要由兩個指標暫存器esp和ebp來負責維護,
esp:堆疊指標暫存器,該暫存器指向了堆疊區最上面一個堆疊幀的頂部(低地址),esp指向的位置會隨著進堆疊(push)和出堆疊(pop)的操作而發生改變,ebp:基址指標暫存器,該暫存器指向了堆疊區最上面一個堆疊幀的底部(高地址),

通俗來講,函式堆疊幀在創建的程序中,esp會指向當前堆疊幀的堆疊頂空間,而ebp會指向當前堆疊幀的堆疊底空間,當然,除了這兩個暫存器,CPU中還有許多其它的幾個暫存器,如eax,ecx,ebx,esi,edi等等,當后面遇到時,再詳細介紹,
匯編指令簡介
在簡單了解函式堆疊幀的概念后,接下我們就要觀察函式堆疊幀的創建與銷毀程序了,而對于整個程序的觀察實際上就是觀察CPU執行的每條指令,C語言作為高級語言,其中簡單的幾行代碼,底層可能是幾十個CPU的指令構成,而這些CPU指令就是由匯編代碼組成的,所以這里先簡單介紹下后面會遇到的匯編指令,當遇到不理解的指令時可以回來查閱此表,
| 指令 | 中文名 | 格式 | 功能 | 備注 |
|---|---|---|---|---|
| PUSH | 進堆疊指令 | PUSH SRC | 將源運算元壓入堆疊中 | 源運算元允許為16位或32位通用暫存器、存盤器和立即數以及16位段暫存器, |
| POP | 彈堆疊指令 | POP DEST | 從堆疊中彈出雙字或字資料至目的運算元中 | 目的運算元允許為16或32位通用暫存器、存盤器和16位段暫存器, |
| MOV | 傳送指令 | MOV DEST,SRC | 把一個位元組,字,或雙字從源運算元傳送至目的運算元 | 字(WORD):表示2個位元組長度的數值, 雙字(DWORD):表示4個位元組長度的數值, |
| ADD | 加法指令 | ADD DEST,SRC | 目的運算元加源運算元,結果送至目的運算元, | 源運算元可以是通用暫存器、存盤器或立即數,目的運算元可以是通用暫存器或存盤器運算元, |
| SUB | 減法指令 | SUB DEST,SRC | 目的運算元減源運算元,結果送至目的運算元 | 源運算元可以是通用暫存器、存盤器或立即數,目的運算元可以是通用暫存器或存盤器運算元, |
| LEA(load effective address) | 取有效地址指令 | LEA REC,MEM | 將源運算元的有效地址傳送到通用暫存器, | 運算元REG為16位或32位通用暫存器,源運算元為16位或32位存盤器運算元, |
| STOS | 串存盤指標 | [REP] STOS DESTS | 將累加器eax中值存入es:[edi]所指的目的串存盤單元中,每傳遞一次,都按DF(Direction Flag)值以及串元素型別自動修改地址指標edi, | 若加重復前綴REP,則表示將累加器的值連續送目的串存盤單元,直到計數器ecx= 0時為止, |
| CALL | 程序呼叫指令 | CALL LABEL | 用來呼叫一個程序,指揮處理器從新的記憶體地址開始執行,(這里用來呼叫函式) | call指令會在呼叫程序之前保存回傳地址,然后再進行轉移, |
| RET | 段內程序回傳指令 | RET | 從呼叫程序回傳,繼續執行主程式, | |
| JMP | 無條件轉移指令 | JMP TARGET | 使程式無條件地轉移到指令規定的目的地址TARGET去執行指令, | 轉移分為短轉移、段內轉移(近程轉移)和段間轉移(遠程轉移) |
程序觀察
在開始觀察之前還需要注意一點,因為編譯器的不同,函式堆疊幀在創建和銷毀程序中存在一些細微的差別,這里我使用的時VS2013進行演示,
給出下面一段代碼來演示整個程序:
#include <stdio.h>
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int c = 0;
c = Add(a, b);
printf("%d\n", c);
return 0;
}
這段代碼基本上學過C的小伙伴都能看得懂,但是我們主要關心的不是這段代碼,而是這段代碼在運行程序中,內層中所發生的變化,以及對應的匯編指令指示cpu執行的操作,
呼叫main函式的函式
首先我先按下F10對這段代碼進行除錯,并查看呼叫堆疊視窗,

此時可以在呼叫堆疊視窗中查看當前被呼叫的函式堆疊幀,很明顯是main函式,那么問題來了,main函式又是被誰呼叫的呢?我們先直接運行到程式結尾,看看main函式的回傳值0被回傳到哪里了?

上圖中可以看到,當main函式結束時,呼叫堆疊視窗中顯示正在運行的堆疊幀為__tmainCRTStartup(),而左邊代碼行指向了main()函式的呼叫處,那么我們就大概知道原來main()函式之前還有被其他函式呼叫,而且main回傳值0是回傳給了mainret,
細心觀察呼叫堆疊視窗還可以發現,在__tmainCRTStartup()下面還有一個mainCRTStartup()的函式堆疊幀,這個函式就是呼叫__tmainCRTStartup()的函式,

所以,我們可以得出結論在vs2013中main()函式的堆疊幀被創建之前,其實還有兩個函式mainCRTStartup()和__tmainCRTStartup()的堆疊幀被創建,mainCRTStartup()呼叫了__tmainCRTStartup(),__tmainCRTStartup()再呼叫了main(),
那么我們一開始的堆疊區記憶體圖就可以改成下圖:

main函式堆疊幀創建
當我們了解到這些后,下面就繼續回到代碼中,我們再次按F10除錯代碼,并打開反匯編視窗,此時視窗中顯示的就是main函式中代碼所對應的匯編指令,

這些代碼是不是很熟悉,這不就是咱們上面表格里的匯編指令嘛?那么下面我們就一行一行運行看看再堆疊區中到底發生腎么事,
00961410 push ebp
// 首先來看第一行 push ebp, 這段代碼執行的操作是將ebp暫存器中的值壓入堆疊中, 也就是放在堆疊頂位置,
ebp 我們知道是指向當前堆疊幀底部的暫存器,那么它其中保存的就是當前堆疊幀底部的地址,此時main()函式還沒有在堆疊區中開辟空間,ebp和esp目前指向的其實是__tmainCRTStartup()函式堆疊幀的堆疊頂和堆疊底,通過呼叫記憶體視窗我們可以觀察,esp和ebp中此時存放的值,這兩個地址值之間的空間就是為__tmainCRTStartup()函式開辟的空間,


之前說過esp所指向的位置一定是堆疊頂元素,push和pop操作會改變esp所改變esp中存放的值,使得esp指向了新的堆疊幀元素,所以當push ebp后,esp指向的就是存放ebp中值的地址,通過觀察記憶體視窗,來看看esp中得值會發生什么變化,


這里需要額外說明一下,在push操作后esp減少的值是根據運算元的資料型別來決定的,當運算元為DWORD時,push操作會使得esp中的值減少4,DWORD其實就是unsigned int,而運算元是一個地址,我們知道在32位平臺中,地址占用4個位元組,是一個無符號整型,因此esp的值減少了4個位元組,

執行完push ebp后的記憶體圖:

00961411 mov ebp,esp
// 接著再看第二行代碼, 這段代碼執行的操作是將esp中的值傳給ebp
這說明ebp和esp現在同時指向了同一塊記憶體空間,我們可以在記憶體視窗中觀察執行這條指令后兩個暫存器中保存的地址,

記憶體圖如下:

00961413 sub esp,0E4h
// 第三段代碼,執行的操作是,讓esp中的值技減去0E4h, 0E4h是16進制數換算為10進制等于228
esp中的地址值減去228,也就是向下偏移了228個位元組,這個操作就是在劃分main函式的堆疊區空間,

當esp指向這塊新的空間后,ebp和esp之間的空間就是為main函式所開辟的記憶體空間,我們還是看記憶體視窗:

記憶體圖如下:

00961419 push ebx
0096141A push esi
0096141B push edi
// 接下來3次push 分別將ebx, esi, edi 三個暫存器中的壓進堆疊中,
// 那么這三個暫存器是用來做什么的呢?下面簡單介紹一下:
// ebx: 基地址(base)暫存器,它可作為存盤器指標來使用,
// esi/edi: 分別叫做"源/目標索引暫存器"(source/destination index),因為在很多字串操作指令中, DS:ESI指向源串,而ES:EDI指向目標串,
// 后面就會用到這個edi暫存器
上面三個暫存器的值存放在esp指向地址后的地址空間,同時esp中的地址值會減少12,指向存放edi值的空間,

記憶體圖如下:

// 再看接下來的4行代碼
0096141C lea edi,[ebp+FFFFFF1Ch] // lea在上面的表格中介紹過, 這句代碼執行的操作是將[ebp+FFFFFF1Ch] 這個有效地址,加載到edi中
00961422 mov ecx,39h // 將39h這個值,賦值給ecx暫存器
00961427 mov eax,0CCCCCCCCh // 將0CCCCCCCCh這個值,賦值給eax
0096142C rep stos dword ptr es:[edi] // 至于最后一段代碼執行的操作,我們得先具體了解上面3個代碼的作用
首先,第一段代碼賦給edi的值,通過計算我們可以得到為0x1005CFD60, edi是32位暫存器, 而這個值有36位,所以最高4位被截斷,得到0x005CFD60,而這個值恰好就是esp之前所指向的地址,

第二段代碼中的ecx暫存器,是計數暫存器,在回圈和字串操作時,需要使用它來控制回圈次數,這個暫存器是REP前綴指令的內定計數器,既然是個計數器,那么對于它的賦值大概率是用來回圈的,我們對39h進行換算發現對應的10進制數是57,57是這個數是干什么的呢?我們先暫且記住它,接著看后面的代碼,
第三段代碼中的eax暫存器,是累加暫存器,它是很多加法乘法指令的默認暫存器,同時這個暫存器也用來保存函式的回傳值,
再看第四段代碼:
rep stos dword ptr es:[edi]
rep stos這個指令在上面表格也提到過,它的作用是將累加器eax中值存入es:[edi]所指的目的串存盤單元中,每傳遞一次,都按DF(Direction Flag)值以及串元素型別自動修改地址指標edi,若加重復前綴REP,則表示將累加器的值連續送目的串存盤單元,直到計數器ecx= 0時為止,補充:DF決定了執行該指令后edi中的值是增加還是減少,沒有設定默認是增加,
現在再看這個指令的功能,再結合上面3段代碼的功能,是不是大家可以猜出來這四段代碼要執行的操作了?
沒錯,上面4段代碼的整體要執行的操作就是將eax中的值存入edi所指向的存盤單元,每次傳遞4個位元組的資料,并且每傳遞一次,edi的地址都會+4,重復執行直到ecx = 0 為止,ecx中的值是57,沒執行一次-1,也就是執行了57次回圈,每次回圈覆寫4個直接的內容,總共覆寫了228個位元組的內容,228這個數字是不是很熟悉?這不就是我們main函式的記憶體空間大小嘛?
所以,上面4段代碼最后實作的結果就是,對main函式的記憶體空間進行初始化,每個位元組的值都初始化位0xcc,0xcc對應的字符就是漢字“燙”,這也就是為什么我們使用printf()列印字符,如果越界訪問會列印出一堆燙燙燙的原因了,
同樣查看記憶體視窗可以看到,main函式的記憶體中間都被初始化為0xcc:

記憶體圖如下:

我們再來梳理一下前面執行的所有代碼:
00961410 push ebp // 將ebp的值壓堆疊
00961411 mov ebp,esp // 將esp的值賦給ebp, 此時esp與ebp指向了同一塊空間
00961413 sub esp,0E4h // 使得esp向下偏移0E4h(228)個位元組
00961419 push ebx // 將ebx的值壓堆疊
0096141A push esi // 將esi的值壓堆疊
0096141B push edi // 將edi的值壓堆疊
0096141C lea edi,[ebp+FFFFFF1Ch] // 使得edi指向了0x005CFD60這塊地址空間
00961422 mov ecx,39h // 給ecx賦值為39h(57)
00961427 mov eax,0CCCCCCCCh // 給eax賦值為0CCCCCCCCh
0096142C rep stos dword ptr es:[edi] // 對main函式開辟的記憶體空間進行初始化
main函式區域變數的創建
其實上面介紹的這10行代碼,都是在為main函式開辟空間所作準備,這些都是編譯器幫我們完成的,我們自己撰寫并沒有體現在其中,那么接下來,就繼續看看我們自己撰寫的代碼如何被執行的,
// 下面三行代碼就是為區域變數a,b,c開辟記憶體空間,并進行初始化
0096142E mov dword ptr [ebp-8],0Ah // 將0Ah的值賦值到ebp-8這塊空間上,0Ah就是10的16進制, ebp-8就是變數a的地址
00961435 mov dword ptr [ebp-14h],14h // 將14h的值賦值到ebp-14h這塊空間上,14h是20的16進制, ebp-14就是變數b的地址
0096143C mov dword ptr [ebp-20h],0 // 將0這個值復制到ebp-20h這塊空間上,ebp-20h就是變數c的地址
我們來看看,變數a,b,c對應的記憶體空間是都在那里,ebp我們知道指向了main函式的底部,那么 -8,-14h,-20h,就是分別在main函式開辟空間的底部向下移動了8個位元組,20個位元組,32個位元組(因為堆疊的地址是從高到低分配的所以是減,這里又說是從底部地址開始減,可能有些繞,需要注意下),
我們還是看記憶體視窗中,a,b,c三個區域變數的地址和其中的值:

記憶體圖如下:

補充:在VS2013編譯器中,區域變數的空間相差8個位元組,而在不同編譯器下會有一些差別,
Add函式的創建
當創建完3個區域變數后,接下來就是呼叫Add函式了,
// 下面的代碼,包含ADD函式呼叫和回傳后剩余的操作
00961443 mov eax,dword ptr [ebp-14h] // 將ebp-14h中的值, 傳遞給eax
00961446 push eax // 將eax中的值壓堆疊
00961447 mov ecx,dword ptr [ebp-8] // 將ebp-8中的值, 傳遞給ecx
0096144A push ecx // 將ecx中的值壓堆疊
0096144B call 009610E1 // 呼叫ADD函式, 處理器將從009610E1的地址處開始執行, call指標會在呼叫函式前, 將下面 add指令的地址保存, 也就是會將00961450的壓堆疊,
// 這兩段代碼等ADD函式回傳后再來關注
00961450 add esp,8
00961453 mov dword ptr [ebp-20h],eax
我們先看在函式呼叫前的四段代碼,執行了什么操作:

前兩段代碼中,ebp-14不就是區域變數b的地址嘛?那么也就是把變數b的值賦給eax,并將eax存盤的值壓堆疊,
后兩段代碼中,ebp-8不就是區域變數a的地址嘛?那么也就是把變數a的值賦給ecx,并將ecx存盤的值壓堆疊,
那么這四段代碼是在干什么呢?大家還有沒有印象在我們呼叫Add函式時,就是將a,b的值作為實參傳給了函式的形參,

那么這些操作其實就是函式傳參的操作,并且傳參的程序是先傳遞右邊的引數b,再傳遞左邊的引數a,因為a,b是區域變數,出了作用域就無法操作了,所以引數的傳遞是依靠兩個暫存器來實作的,
而且還有一點需要注意,我們目前還并沒有呼叫Add函式,也就不存在開辟為Add函式開辟空間,而這兩個變數的值被執行壓堆疊操作,其實不是定義在Add函式中的空間中的,它們的位置位于main函式和Add函式之間,這兩個值其實就只是變數a,b的一份臨時拷貝,并且壓堆疊后,esp指標也會指向新的堆疊頂元素,
記憶體視窗布局:

記憶體圖如下:

當函式傳參完畢后,下面就要開始呼叫Add函式了(因為誤操作導致VS被關掉,只能重新除錯,所以地址會和之前的不同,請大家見諒),
在呼叫Add函式前,我們需要關注call指令下一條指令的地址,上面說過這條地址會在函式呼叫前被push進堆疊,我們F11進行下一步來觀察是不是這樣的,


從上面的記憶體視窗可以看到,在執行call指令后,操作轉到006C10E1的地址處執行,而call指令的下一條指令的地址也被push進堆疊,那么將這個地址保存起來有什么用呢?其實它的作用很簡單,就是為了結束Add函式后,可以回傳main函式繼續執行操作做準備的,
記憶體圖如下:

那么我們接著再看這條jmp指令:
006C10E1 jmp 006C13C0 // 這條指令的操作是,跳轉到006C13C0的位置執行指令
再次按下F11,我們就跳轉到了Add函式所對應的匯編指令處:

這些指令看著是不是很眼熟?特別是前10行代碼幾乎和main函式創建時的指令一模一樣,所以這10行指令也是在為Add函式創建函式堆疊幀,并初始化,當我們執行完這十條指令后,ebp和esp這兩個指標就開始負責維護Add函式的堆疊幀空間了,
記憶體中的布局大概是這樣的:

記憶體圖如下:

Add()函式區域變數的創建
當初始化Add堆疊幀的空間后,接下來就是創建區域變數z:
int z = 0;
006C13DE mov dword ptr [ebp-8],0 // 將0的值,傳遞到ebp-8的位置中,也就是變數z的空間中,

記憶體圖如下:

接著執行 z = x + y 的操作:
z = x + y;
006C13E5 mov eax,dword ptr [ebp+8] // 將ebp+8的值以雙字傳遞到eax中
006C13E8 add eax,dword ptr [ebp+0Ch] // 讓eax中的值 加上 ebp+0ch中的值, 并賦值給eax
006C13EB mov dword ptr [ebp-8],eax // 將eax的值以雙字傳遞到ebp-8中
ebp+8其實就是函式傳參時,存放變數a中值的那塊空間,所以這塊空間其實代表了形參x的記憶體空間,

而ebp+0ch是存放變數b中值的那塊空間,也就是形參y的記憶體空間,

因為這兩塊空間其實并不在Add函式的空間內,它們的作用域也沒有覆寫到這個位置,所以借助了eax將兩個值相加并重新賦給了eax,

第三行指令,將eax的值以雙字傳遞到ebp-8中,ebp-8不就是我們剛才為變數z開辟的記憶體空間嘛?所以這兩個值相加的結果就被賦值給了變數z,

當完成上面三行指令后,就等于完成了 z = x + y的操作,這樣我們就知道,原來形參的在函式中是這樣被使用的,是不是很精妙?
完成加法操作后的記憶體圖:

Add函式的銷毀
當變數z得到了相加后的結果,下一步就是要將這個結果回傳給呼叫處,那么是如何將這個結果進行回傳的呢?
下面就要進行回傳結果和銷毀Add函式的操作了:
//一下是return z 所要執行的操作
006C13EE mov eax,dword ptr [ebp-8] // 將ebp-8的值傳遞給eax
}
006C13F1 pop edi // 將堆疊頂元素彈出至edi中
006C13F2 pop esi // 將堆疊頂元素彈出至esi中
006C13F3 pop ebx // 將堆疊頂元素彈出至ebx中
006C13F4 mov esp,ebp // 將ebp的值賦給esp, 即esp重新指向了ebp的位置
006C13F6 pop ebp // 將堆疊幀元素彈出ebp中
006C13F7 ret // 執行ret指令

第一行指令,把ebp-8即變數z的值存盤在了eax中,我們知道到Add函式被銷毀后,變數z就被銷毀了,那么這個回傳值其實就是通過eax回傳給呼叫處的,

第二至四行,都是彈堆疊指令,并且將堆疊幀元素的值放到對應的暫存器中,每次pop,esp就會+4指向上一個元素,而除了edi的值因為初始化操作發生了變化,其他2個暫存器并沒有改變,所以執行3次pop后,edi的值會改變為初始化前的值,其他兩個暫存器的值不變,并且esp重新指向了之前的空間,

記憶體圖如下:


第五行指令,使得esp重新指向了ebp所指向的位置,此時,為Add函式所開辟的空間,就等于被回收了,我們就不能對這塊空間進行有效訪問了,
第六行指令,將堆疊頂元素的值彈至ebp中,此時的堆疊幀元素就是esp和ebp所指向的那塊空間,其中保存的值就是main函式的堆疊幀底部的地址,把這個地址交給ebp就使得ebp重新指向了main函式的堆疊幀底部,這里大家就應該明白為什么每次創建函式堆疊幀要把ebp之前指向的地址保存在當前堆疊幀的底部了吧?其目的就是為了當前堆疊幀被銷毀后,ebp可以重新指向主調函式堆疊幀底部的位置,我只能說這個設計太精妙了!
執行指令后ebp和esp所指向的地址:![[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-sIjEry7j-1628999861647)(D:\Code_library\C_Language\c-language-learning-code\notes\11-函式堆疊幀\image-20210815104037121.png)]](https://img.uj5u.com/2021/08/17/257282170806568.png)
記憶體圖如下:


最后一行指令ret,會回傳到主程式繼續執行下面的指令中,那么應該回傳到哪里呢?大家還記不記得在執行call指令的時候,我們是將call指令的下一條指令,也就是add指令保存起來,而esp此時正指向了這塊空間,所以當執行ret指令后,cpu會回傳繼續執行add指令,

當執行ret后,esp+4,此時esp又指向了保存數字10的那塊空間(形參x的空間),

我們再接著看后面的add指令:
006C1450 add esp,8 // 將esp中的值+8, 并賦給esp
這行指令使得esp向上跳過8個位元組,指向了保存原來edi中值的地址,跳過的8個位元組就是形參x,y的空間,所以形參x,y都被回收,并且此時ebp和esp又重新開始維護main函式的堆疊幀,

執行到這里Add函式就徹底被銷毀了,大家是不是很奇怪,被銷毀了為什么保存的值都還在?其實這里說的銷毀,其實是指這空間在當前程式中不能在進行訪問,如果訪問就屬于非法訪問,這也就是為什么,當我們越界訪問后,會出現一些隨機值,因為這些值可能就是在創建堆疊幀的程序中使用過的空間,
Add函式回傳值執行的操作
大家可能就有疑問了,那Add函式的回傳值去哪兒了?Add函式的空間都被銷毀了它的回傳值怎么傳遞回來?大家先不要慌張,我們接著看下面的指令:
006C1453 mov dword ptr [ebp-20h],eax // 將eax的值以雙字傳遞到 ebp-20h這塊空間中
我們回憶一下,eax中的值是什么?不就是之前變數z的值嘛?也就是30,
而ebp-20h是哪塊空間?ebp現在指向了main函式堆疊幀的底部,它的值減32(十進制),咦~好像是變數c的空間啊,哦那么我們就知道了,原來這條指令的操作就是將Add函式的回傳值通過eax賦值給變數c啊!
可以看到執行完這條指令后,c中的值被賦值為0x0000001e也就是30,

記憶體圖如下:

剩下的指令就是呼叫printf()函式進行列印,和銷毀main()函式的操作,這里就不再介紹,主要是printf()涉及到原始碼了,自己還沒達到這個水平不敢瞎寫,而銷毀main()函式的操作,大體上和銷毀Add()函式一致,所以這里也不再詳細說明,
總結
通過對整個函式堆疊幀創建于銷毀程序的觀察,我們現在就可以回答文章開頭提出的幾個問題:
-
區域變數是如何創建的?
區域變數的創建是在堆疊幀中,以
ebp所指向的高地址為基礎,向低地址處給變數分配空間,并且在vs2013中,兩個變數之間會有8個位元組的空隙, -
為什么說區域變數未初始化時,其中存盤的時隨機值?
這是因為函式堆疊幀在創建時,會默認對堆疊幀中的空間進行初始化,這個初始化的值并不是固定的,而是由編譯器決定的,所以不同的編譯器初始化的值也不一定相同,并且在堆疊幀創建的程序中,我們還在堆疊區中保存了很多暫存器變數中的值,這些值隨著堆疊幀的銷毀并不會真的銷毀,依然保存在堆疊區空間中,只是我們無法進行有效訪問了,而當我們不對區域變數進行初始化時,其中保存的值,也就不可知了,
-
函式到底時如何傳參的?實參傳遞的順序又是怎樣的?
函式傳參實際上就是在兩個函式堆疊幀中間,臨時創建了一塊空間用來保存傳遞的值,并且值傳遞是依靠暫存器變數來實作的,形參并沒有在被調函式的記憶體空間中創建變數,實際上也是通過
ebp+n來找到這些引數的,傳遞的順序是從右往左傳遞,
-
形參和實參之間有著什么關系?
形參其實就是實參的一份臨時拷貝罷了,當被調函式堆疊幀銷毀后,形參會緊接著被銷毀,
-
函式呼叫結束后,結果是如何回傳的?
函式呼叫結束后,會將回傳值保存到暫存器
eax當中,當執行ret指令后,就會將eax中保存的值傳遞給呼叫處的空間,這就是函式回傳值的具體實作,
以上就是函式堆疊幀創建與銷毀的全部內容,這篇文章博主真的是實打實肝了一天,寫著寫著自己都寫懵了,所以,如果文章能幫助到您還請能給個大拇指👍,
當然因為本人水平有限,目前也還在學習當中,文章中難免會出現紕漏,還懇請各位大佬發現問題后,能幫忙指出,感激不盡![抱拳]
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/294270.html
標籤:其他
下一篇:C++ 吃透動態規劃演算法
