GoLang函數(shù)棧的使用詳細(xì)講解
函數(shù)棧幀
我們的代碼會(huì)被編譯成機(jī)器指令并寫入到可執(zhí)行文件,當(dāng)程序執(zhí)行時(shí),可執(zhí)行文件被加載到內(nèi)存,這些機(jī)器指令會(huì)被存儲(chǔ)到虛擬地址空間中的代碼段,在代碼段內(nèi)部,指令是低地址向高地址堆積的。堆區(qū)存儲(chǔ)的是需要程序員手動(dòng)alloc并free的空間,需要自己來(lái)控制。
虛擬內(nèi)存空間是對(duì)存儲(chǔ)器的一層抽象,是為了更好的來(lái)管理存儲(chǔ)器,虛擬內(nèi)存和存儲(chǔ)器之間存在映射關(guān)系。
如果在一個(gè)函數(shù)中調(diào)用了另外一個(gè)函數(shù),編譯器就會(huì)對(duì)應(yīng)生成一條call指令,當(dāng)call指令被執(zhí)行時(shí),就會(huì)跳轉(zhuǎn)到被調(diào)用函數(shù)入口處開始執(zhí)行,而每個(gè)函數(shù)的最后都有一條ret指令,負(fù)責(zé)在函數(shù)結(jié)束后跳回到調(diào)用處繼續(xù)執(zhí)行。
call 指令做了兩件事,將下一條指令的地址入棧,這就是IP寄存器中存儲(chǔ)的值,第二,跳轉(zhuǎn)到被調(diào)用函數(shù)入口處執(zhí)行。
函數(shù)執(zhí)行時(shí)需要有足夠的內(nèi)存空間用來(lái)存儲(chǔ)參數(shù),局部變量,返回值,這塊空間對(duì)應(yīng)的就是棧,棧區(qū)是從高地址向低地址生長(zhǎng)的,且先進(jìn)后出。分配給函數(shù)的??臻g被稱為函數(shù)棧幀。
C語(yǔ)言中,每個(gè)棧幀對(duì)應(yīng)著一個(gè)未運(yùn)行完的函數(shù)。棧幀中保存了該函數(shù)的返回地址和局部變量。
寄存器
ESP寄存器:ESP即 Extended stack pointer 的縮寫,直譯過來(lái)就是擴(kuò)展的棧指針寄存器。SP是16位的,ESP是32位的,RSP是64位的,存放的都是棧頂?shù)刂贰?/p>
EBP寄存器:EBP即 Extended base pointer 的縮寫,直譯過來(lái)就是擴(kuò)展的基址指針寄存器。該指針總是指向當(dāng)前棧幀的底部。
IP寄存器:指令指針,它指向代碼段中的地址,是一個(gè)16位專用寄存器,它指向當(dāng)前需要取出的指令字節(jié),也就是下一個(gè)將要執(zhí)行的指令在代碼段中的地址。
eax:累加(Accumulator)寄存器,常用于函數(shù)返回值
ebx:基址(Base)寄存器,以它為基址訪問內(nèi)存
ecx:計(jì)數(shù)器(Counter)寄存器,常用作字符串和循環(huán)操作中的計(jì)數(shù)器
edx:數(shù)據(jù)(Data)寄存器,常用于乘除法和I/O指針
esi:源地址寄存器
edi:目的地址寄存器
esp:堆棧指針
ebp:棧指針寄存器
當(dāng)然,以上功能并未限制寄存器的使用,特殊情況為了效率也可作其他用途。
這八個(gè)寄存器低16位分別有一個(gè)引用別名 ax, bx, cx, dx, bp, si, di, sp,
其中 ax, bx, cx, dx, 的高8位又引用至 ah, bh, ch, dh,低八位引用至 al, bl, cl, dl
在 64-bit 模式下,有16個(gè)通用寄存器,但是這16個(gè)寄存器是兼容32位模式的,
32位方式下寄存器名分別為 eax, ebx, ecx, edx, edi, esi, ebp, esp, r8d – r15d.
在64位模式下,他們被擴(kuò)展為 rax, rbx, rcx, rdx, rdi, rsi, rbp, rsp, r8 – r15.
其中 r8 – r15 這八個(gè)寄存器是64-bit模式下新加入的寄存器。
我們看到CPU在執(zhí)行代碼段中的指令,而這當(dāng)中又伴隨著內(nèi)存的分配,于是在函數(shù)棧幀上就會(huì)有相應(yīng)的變化。
int add(int a, int b) { int c = 4; c = a + b; return c; } int main() { int a = 1; int b = 2; int sum = 3; sum = add(a, b); return 0; }
生成的匯編代碼的方式
1、使用 gcc + objdump
gcc -save-temps -fverbose-asm -g -o b testasm.c
objdump -S --disassemble b > b.objdump
2、使用第三方網(wǎng)站來(lái)生成,進(jìn)入 https://godbolt.org/,選擇語(yǔ)言為C
,編譯器為x86-64 gcc 12.2
,粘貼進(jìn)你的代碼,就能看到匯編代碼,如下
add:
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-20], edi
mov DWORD PTR [rbp-24], esi
mov DWORD PTR [rbp-4], 4
mov edx, DWORD PTR [rbp-20]
mov eax, DWORD PTR [rbp-24]
add eax, edx
mov DWORD PTR [rbp-4], eax
mov eax, DWORD PTR [rbp-4]
pop rbp
ret
main:
push rbp
mov rbp, rsp
sub rsp, 16
mov DWORD PTR [rbp-4], 1
mov DWORD PTR [rbp-8], 2
mov DWORD PTR [rbp-12], 3
mov edx, DWORD PTR [rbp-8]
mov eax, DWORD PTR [rbp-4]
mov esi, edx
mov edi, eax
call add
mov DWORD PTR [rbp-12], eax
mov eax, 0
leave
ret
從main開始解讀
// 此時(shí)rbp存儲(chǔ)的還是上一層函數(shù)(調(diào)用者)的?;刂?,將rbp的值入棧保存起來(lái),因?yàn)閙ain函數(shù)也是被其他函 // 數(shù)調(diào)用的,運(yùn)行完main之后還得回到那個(gè)函數(shù)體中去。這里的地址指的是指令的地址,是代碼段中的位置。 // push指令會(huì)使rsp下移。 push rbp // 此時(shí)rbp存儲(chǔ)的還是上一個(gè)函數(shù)的基地址,而rsp則已經(jīng)游走到了main函數(shù)這里,mov指令將rsp中存儲(chǔ)的地址傳遞 // 給rbp,也就意味著執(zhí)行完之后rbp和rsp都處于main函數(shù)的開始位置,稱為初始化操作。 mov rbp, rsp // rsp下移16,就是分配??臻g sub rsp, 16
// DWORD 為雙字,即四個(gè)字節(jié),PTR為指針的意思,此句意為在rbp向下偏移4個(gè)字節(jié)的這段棧內(nèi)存中存儲(chǔ)0 // a mov DWORD PTR [rbp-4], 1 // b mov DWORD PTR [rbp-8], 2 // sum mov DWORD PTR [rbp-12], 3 // 將參數(shù)從右到左,依次存起來(lái),此處存到了 edx和eax,并拷貝了一份到esi和edi。 mov edx, DWORD PTR [rbp-8]` mov eax, DWORD PTR [rbp-4]` mov esi, edx` mov edi, eax`
// 執(zhí)行call指令
// 注意,call會(huì)使CPU跳入到add的棧幀中去,那么執(zhí)行完之后,我們需要跳回到被調(diào)用處繼續(xù)向下執(zhí)行,由
// 最前面的push指令我們已經(jīng)把調(diào)用者的?;媪讼聛?lái),可是我們還要精確到具體是回到哪個(gè)指令,這就是call
// 指令的額外工作,它會(huì)先將IP入棧(push ip),因?yàn)镮P中存的就是下一條指令(mov DWORD PTR [rbp-12], eax)
// 的地址,然后再去跳轉(zhuǎn)(jmp),將add函數(shù)的第一條指令寫入IP,此后就進(jìn)入add函數(shù)棧幀。
call add
// cpu執(zhí)行完運(yùn)算后會(huì)將結(jié)果存儲(chǔ)在寄存器中,至于它會(huì)把結(jié)果存儲(chǔ)在那個(gè)寄存器,這個(gè)由編譯器編譯出的指令 // 決定的,由add函數(shù)的指令來(lái)看,它選擇了eax // rbp-12 為sum的位置,這條指令將eax寄存器的值賦值給sum mov DWORD PTR [rbp-12], eax // 將eax置0,也就是main的返回值 mov eax, 0 // 意為 mov rsp, rbp 和 pop rbp 的組合 // 此時(shí)rbp為main函數(shù)的?;瑀sp為main函數(shù)的末尾了,將rbp賦值給rsp,于是它們都指向main函數(shù)的?;?,上 // 面解釋過,rbp寄存器存儲(chǔ)的地址指向的棧上的空間存儲(chǔ)的還是一個(gè)地址,此地址指向調(diào)用者的?;?, // pop rbp 將棧頂rsp的數(shù)據(jù)送入rbp,就意味著之后就回到了調(diào)用者的棧幀了,同時(shí)pop會(huì)伴隨著rsp的上移, // 于是rsp來(lái)到了EIP的位置。 leave // 相當(dāng)于 pop ip // 此函數(shù)執(zhí)行完需要跳回到調(diào)用者并繼續(xù)執(zhí)行下一條指令,由于call的時(shí)候已經(jīng)將下一條指令的地址入棧了,所以 // 此處值需要將其彈出即可。 ret
到此這篇關(guān)于GoLang函數(shù)棧的使用詳細(xì)講解的文章就介紹到這了,更多相關(guān)Go函數(shù)棧內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
通過Golang實(shí)現(xiàn)linux命令ls命令(命令行工具構(gòu)建)
這篇文章主要為大家詳細(xì)介紹了如何通過Golang實(shí)現(xiàn)一個(gè)linux命令ls命令(命令行工具構(gòu)建),文中的示例代碼講解詳細(xì),具有一定的學(xué)習(xí)價(jià)值,感興趣的可以了解一下2023-01-01Golang JSON的進(jìn)階用法實(shí)例講解
這篇文章主要給大家介紹了關(guān)于Golang JSON進(jìn)階用法的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用golang具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-09-09golang的匿名函數(shù)和普通函數(shù)的區(qū)別解析
匿名函數(shù)是不具名的函數(shù),可以在不定義函數(shù)名的情況下直接使用,通常用于函數(shù)內(nèi)部的局部作用域中,這篇文章主要介紹了golang的匿名函數(shù)和普通函數(shù)的區(qū)別,需要的朋友可以參考下2023-03-03gin自定義中間件解決requestBody不可重讀(請(qǐng)求體取值)
這篇文章主要介紹了gin自定義中間件解決requestBody不可重讀,確保控制器能夠獲取請(qǐng)求體值,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10go slice 數(shù)組和切片使用區(qū)別示例解析
這篇文章主要為大家介紹了go slice 數(shù)組和切片使用區(qū)別示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01golang中連接mysql數(shù)據(jù)庫(kù)
這篇文章主要介紹了golang中連接mysql數(shù)據(jù)庫(kù)的步驟,幫助大家更好的理解和學(xué)習(xí)go語(yǔ)言,感興趣的朋友可以了解下2020-12-12Go 自定義package包設(shè)置與導(dǎo)入操作
這篇文章主要介紹了Go 自定義package包設(shè)置與導(dǎo)入操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來(lái)看看吧2021-05-05