一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

x86_64架構(gòu)下的函數(shù)調(diào)用及棧幀原理

 金剛光 2020-02-21

看雪學(xué)院 4天前

本文為看雪論壇優(yōu)秀文章

看雪論壇作者ID:有毒

一、x86_64寄存器

在分析函數(shù)調(diào)用時(shí),必須要對(duì)CPU的寄存器熟悉。在所有的體系架構(gòu)中,每個(gè)寄存器都有建議的使用方法,編譯器在對(duì)代碼進(jìn)行編譯時(shí),也通常按照體系架構(gòu)建議的寄存器的使用方法進(jìn)行編譯。

在x86_64體系架構(gòu)中,總共有16個(gè)64位通用寄存器,各寄存器及用途如下所示:

對(duì)上圖中的寄存器做簡(jiǎn)單說(shuō)明:

  • %rax :通常存儲(chǔ)函數(shù)調(diào)用的返回結(jié)果,也被用在idiv (除法)和imul(乘法)命令中。

  • %rsp :堆棧指針寄存器,指向棧頂位置。pop操作通過(guò)增大rsp的值實(shí)現(xiàn)出棧,push操作通過(guò)減小rsp的值實(shí)現(xiàn)入棧。

  • %rbp :棧幀指針,標(biāo)識(shí)當(dāng)前棧幀的起始位置。

  • %rdi, %rsi, %rdx, %rcx,%r8, %r9 :六個(gè)寄存器,當(dāng)參數(shù)少于7個(gè)時(shí), 參數(shù)從左到右放入寄存器: rdi, rsi, rdx, rcx, r8, r9;當(dāng)參數(shù)為7個(gè)以上時(shí),前 6 個(gè)與前面一樣, 但后面的依次從 “右向左” 放入棧中,即和32位匯編一樣。








; 參數(shù)>=7時(shí)Func(a, b, c, d, e, f, g, h);;參數(shù)的具體存放方式a->%rdi, b->%rsi, c->%rdx, d->%rcx, e->%r8, f->%r9h->8(%esp)g->(%esp)call Func

(備注:用棧在進(jìn)行參數(shù)傳遞時(shí),即便參數(shù)<8字節(jié),也要對(duì)齊放在8字節(jié)的空間中)

  • 函數(shù)執(zhí)行前后必須保持原始的寄存器有3個(gè):是rbx、rbp、rsp。rx寄存器中,最后4個(gè)必須保持原值:r12、r13、r14、r15。保持原值的意義是為了讓當(dāng)前函數(shù)有可信任的寄存器,減小在函數(shù)調(diào)用過(guò)程中的保存&恢復(fù)操作。除了rbp、rsp用于特定用途外,其余5個(gè)寄存器可隨意使用。

  • Caller Save 和 Callee Save寄存器 : 寄存器的值是由”調(diào)用者保存“ 還是由 ”被調(diào)用者保存“。當(dāng)進(jìn)行函數(shù)調(diào)用時(shí),子函數(shù)通常也會(huì)使用通用寄存器,但這些寄存器中可能保存著父函數(shù)(調(diào)用者)的值。如果是Caller Save 寄存器,在進(jìn)行子函數(shù)調(diào)用之前,需要由調(diào)用者提前保存寄存器中的值(入棧),然后在子函數(shù)中可以向這些寄存器中寫(xiě)入任何數(shù)據(jù);在完成調(diào)用后,恢復(fù)寄存器原來(lái)的值(出棧)。如果是Callee Save寄存器,父函數(shù)在進(jìn)行子函數(shù)調(diào)用前不會(huì)保存寄存器中的值,在調(diào)用子函數(shù)后,子函數(shù)會(huì)首先保存寄存器中的值(入棧);子函數(shù)完成功能后,恢復(fù)寄存器中的值,然后再返回到父函數(shù),結(jié)束調(diào)用。

二、函數(shù)調(diào)用時(shí)的棧幀

>>>>

1. 函數(shù)調(diào)用

在對(duì)子函數(shù)進(jìn)行調(diào)用時(shí),棧幀情況如下:

(注意此處棧幀增長(zhǎng)方向從上到下)

  • 調(diào)用者棧幀中,保存了被調(diào)用函數(shù)的參數(shù)以及調(diào)用者的返回地址,其流程大致如下:父函數(shù)將調(diào)用參數(shù)從右到左依次壓棧->返回地址入棧->跳轉(zhuǎn)到子函數(shù)起始地址->子函數(shù)將父函數(shù)棧幀起始地址(%rbp)壓棧->將%rbp 的值設(shè)置為當(dāng)前 %rsp 的值,開(kāi)辟棧幀空間

  • 函數(shù)調(diào)用時(shí)的匯編指令如下:









... ;參數(shù)壓棧call Func ;將返回地址壓棧,并跳轉(zhuǎn)到子函數(shù)處執(zhí)行... ;函數(shù)調(diào)用的返回位置 Func: ;子函數(shù)入口pushq %rbp ;保存父函數(shù)的幀指針,創(chuàng)建新棧幀movq %rsp, %rbp ;讓 %rbp 指向新棧幀的起始位置subq $N, %rsp ;開(kāi)辟棧幀空間供子程序使用

以上過(guò)程由編譯器自動(dòng)完成。需要注意的是,父函數(shù)中進(jìn)行參數(shù)壓棧時(shí),順序?yàn)閺挠业阶?,但并不是固定,要看編譯器的具體實(shí)現(xiàn)(gcc使用的是從右到左)。

>>>>

2. 函數(shù)返回

函數(shù)返回時(shí),我們需要的數(shù)據(jù)是函數(shù)的返回值(%rax),然后將棧結(jié)構(gòu)恢復(fù)到函數(shù)調(diào)用之前的狀態(tài),最后跳轉(zhuǎn)到父函數(shù)的返回地址繼續(xù)執(zhí)行。需要執(zhí)行以下兩條指令:



movq %rbp, %rsp ; 使 %rsp 和 %rbp 指向同一位置,即子棧幀的起始處, 收回子棧幀空間popq %rbp ; 將棧中保存的父棧幀的 %rbp 的值賦值給 %rbp,并且 %rsp 上移一個(gè)位置指向父棧幀的結(jié)尾處

為了便于棧幀恢復(fù),x86_64 架構(gòu)中提供了 leave 指令來(lái)實(shí)現(xiàn)上述兩條命令的功能。執(zhí)行 leave 后,前面圖中函數(shù)調(diào)用的棧幀結(jié)構(gòu)如下:

調(diào)用 leave 后,%rsp 指向返回地址;ret 指令,從棧頂彈出數(shù)據(jù),并跳轉(zhuǎn)到此數(shù)據(jù)指向的地址處。在leave 執(zhí)行后,%rsp 指向返回地址,因而 ret 的作用就是把 %rsp 上移一個(gè)位置,并跳轉(zhuǎn)到返回地址執(zhí)行。

所以,leave 指令用于恢復(fù)父函數(shù)的棧幀,ret 用于跳轉(zhuǎn)到返回地址處,leave 和ret 配合共同完成了子函數(shù)的返回。當(dāng)執(zhí)行完成 ret 后,%rsp 指向的是父棧幀的結(jié)尾處,父棧幀尾部存儲(chǔ)的調(diào)用參數(shù)由編譯器自動(dòng)釋放。

三、函數(shù)調(diào)用示例

程序源代碼如下:















int add(int a, int b, int c, int d, int e, int f, int g, int h) { // 8 個(gè)參數(shù) int sum = a + b + c + d + e + f + g + h; //相加求和 return sum;} int main(void) { int i = 10; int j = 20; int k = i + j; int sum = add(11, 22,33, 44, 55, 66, 77, 88); int m = k; // 為了觀察 %rax Caller Save 寄存器的恢復(fù) return 0;}

上面程序生成的和子函數(shù)調(diào)用相關(guān)的匯編程序如下:






















































add:.LFB2: pushq %rbp.LCFI0: movq %rsp, %rbp.LCFI1: movl %edi, -20(%rbp) movl %esi, -24(%rbp) movl %edx, -28(%rbp) movl %ecx, -32(%rbp) movl %r8d, -36(%rbp) movl %r9d, -40(%rbp) movl -24(%rbp), %eax addl -20(%rbp), %eax addl -28(%rbp), %eax addl -32(%rbp), %eax addl -36(%rbp), %eax addl -40(%rbp), %eax addl 16(%rbp), %eax addl 24(%rbp), %eax movl %eax, -4(%rbp) movl -4(%rbp), %eax leave ret main:.LFB3: pushq %rbp.LCFI2: movq %rsp, %rbp.LCFI3: subq $48, %rsp.LCFI4: movl $10, -20(%rbp) movl $20, -16(%rbp) movl -16(%rbp), %eax addl -20(%rbp), %eax movl %eax, -12(%rbp) movl $88, 8(%rsp) movl $77, (%rsp) movl $66, %r9d movl $55, %r8d movl $44, %ecx movl $33, %edxmovl $22, %esi movl $11, %edi call add movl %eax, -8(%rbp) movl -12(%rbp), %eax movl %eax, -4(%rbp) movl $0, %eax leave ret

首先看 main 函數(shù)的前三條匯編語(yǔ)句:







.LFB3: pushq %rbp.LCFI2: movq %rsp, %rbp.LCFI3: subq $48, %rsp

保存父函數(shù)棧幀,之后創(chuàng)建main 函數(shù)的棧幀并且分配了48 Byte 的空間。執(zhí)行完成后,main 函數(shù)的棧幀如下圖所示:

繼續(xù)往后走,可以看到對(duì)k=i+j的處理過(guò)程:















movl $10, -20(%rbp)movl $20, -16(%rbp)movl -16(%rbp), %eaxaddl -20(%rbp), %eaxmovl %eax, -12(%rbp) movl $88, 8(%rsp)movl $77, (%rsp)movl $66, %r9dmovl $55, %r8dmovl $44, %ecxmovl $33, %edxmovl $22, %esimovl $11, %edicall add

需要注意的是,傳統(tǒng)的棧空間的利用操作是使用一個(gè)??臻g,進(jìn)行一次push操作。

但是我們的代碼里沒(méi)有這樣進(jìn)行,而是利用之前分配的48字節(jié)的空間,以空間的縮減進(jìn)行分配,本質(zhì)上是和push操作是一樣的。最終的計(jì)算結(jié)果保存在%eax中。

后續(xù)進(jìn)行add函數(shù)調(diào)用,add的返回值將會(huì)放在%eax中,當(dāng)前%eax中保存的是k值。

所以需要首先保存%eax中的值,然后在add函數(shù)中進(jìn)行調(diào)用,最后再恢復(fù)%eax的值。%eax 是 Caller Save的,所以由父函數(shù)main函數(shù)來(lái)進(jìn)行保存(movl %eax,-12(%rbp))。

再往后,開(kāi)始參數(shù)入棧,前6個(gè)參數(shù)依次保存到對(duì)應(yīng)的寄存器中,最后兩個(gè)參數(shù)從右到左壓入棧中。

進(jìn)入 add 函數(shù)后之后的操作如下:

























add:.LFB2: pushq %rbp ; 保存父棧幀指針.LCFI0: movq %rsp, %rbp ; 創(chuàng)建新棧幀.LCFI1: movl %edi, -20(%rbp) ; 在寄存器中的參數(shù)壓棧 movl %esi, -24(%rbp) movl %edx, -28(%rbp) movl %ecx, -32(%rbp) movl %r8d, -36(%rbp) movl %r9d, -40(%rbp) movl -24(%rbp), %eax addl -20(%rbp), %eax addl -28(%rbp), %eax addl -32(%rbp), %eax addl -36(%rbp), %eax addl -40(%rbp), %eax addl 16(%rbp), %eax addl 24(%rbp), %eax movl %eax, -4(%rbp) movl -4(%rbp), %eax leave ret

首先創(chuàng)建新棧幀,然后進(jìn)行參數(shù)入棧。

在參數(shù)入棧時(shí),我們看到并未使用 push 之類(lèi)的指令,也沒(méi)有調(diào)整 %esp 指針的值,而是使用了 -N(%rbp) 這樣的指令來(lái)使用新的??臻g。這種使用”基地址+偏移量“ 來(lái)使用棧的方式和直接使用 %esp 指向棧頂?shù)姆绞狡鋵?shí)是一樣的。

當(dāng)add函數(shù)返回后,返回結(jié)果存儲(chǔ)在%eax 中,%rbp 和 %rsp 調(diào)整為指向 main 的棧幀,之后執(zhí)行main 函數(shù)中的如下指令:







movl %eax, -8(%rbp) ; 保存 add 函數(shù)返回值到棧中,對(duì)應(yīng) C 語(yǔ)句 int sum = add(...)movl -12(%rbp), %eax ; 恢復(fù)寄存器 %eax 的值,與調(diào)用add前保存 %eax 相對(duì)應(yīng)movl %eax, -4(%rbp) ; 對(duì)應(yīng) C 語(yǔ)句 m = k,%eax 中的值就是 k。movl $0, %eax ; main 函數(shù)返回值leave ; main 函數(shù)返回ret

add 函數(shù)返回時(shí),把返回值保存到了 %eax 中,使用完返回值后,會(huì)恢復(fù) caller save 寄存器 %eax的值,這時(shí)main 棧幀恢復(fù)到調(diào)用add函數(shù)之前的狀態(tài)。

需要注意的是,在調(diào)用 add 之前,main 中執(zhí)行了一條 subq 48, %rsp 的指令,這主要是因?yàn)閙ain函數(shù)并未再調(diào)用其他函數(shù),結(jié)尾處的leave、ret兩條指令直接覆蓋了%rsp的值從而回到了父棧幀中。

如果先調(diào)整 main 棧幀的 %rsp 值,之后 leave 再覆蓋 %rsp 的值,相當(dāng)于調(diào)整是多余的。因而省略main 中 add返回之后的 %rsp 的調(diào)整,而使用 leave 直接覆蓋%rsp更為合理。

之前對(duì)這一塊不是很懂,看了大佬的文章后就明白了~

x86-64 下函數(shù)調(diào)用及棧幀原理

一蓑一笠一扁舟,一丈絲綸一寸鉤。
一曲高歌一樽酒,一人獨(dú)釣一江秋。

—— 題秋江獨(dú)釣圖

緣起

在 C/C++ 程序中,函數(shù)調(diào)用是十分常見(jiàn)的操作。那么,這一操作的底層原理是怎樣的?編譯器幫我們做了哪些操作?CPU 中各寄存器及內(nèi)存堆棧在函數(shù)調(diào)用時(shí)是如何被使用的?棧幀的創(chuàng)建和恢復(fù)是如何完成的?針對(duì)上述問(wèn)題,本本文進(jìn)行了探索和研究。

通用寄存器使用慣例

函數(shù)調(diào)用時(shí),在硬件層面我們需要關(guān)注的通常是cpu 的通用寄存器。在所有 cpu 體系架構(gòu)中,每個(gè)寄存器通常都是有建議的使用方法的,而編譯器也通常依照CPU架構(gòu)的建議來(lái)使用這些寄存器,因而我們可以認(rèn)為這些建議是強(qiáng)制性的。

對(duì)于 x86-64 架構(gòu),共有16個(gè)64位通用寄存器,各寄存器及用途如下圖所示:

從上圖中,我們可以得到如下結(jié)論:

  • 每個(gè)寄存器的用途并不是單一的。

  • %rax 通常用于存儲(chǔ)函數(shù)調(diào)用的返回結(jié)果,同時(shí)也用于乘法和除法指令中。在imul 指令中,兩個(gè)64位的乘法最多會(huì)產(chǎn)生128位的結(jié)果,需要 %rax 與 %rdx 共同存儲(chǔ)乘法結(jié)果,在div 指令中被除數(shù)是128 位的,同樣需要%rax 與 %rdx 共同存儲(chǔ)被除數(shù)。

  • %rsp 是堆棧指針寄存器,通常會(huì)指向棧頂位置,堆棧的 pop 和push 操作就是通過(guò)改變 %rsp 的值即移動(dòng)堆棧指針的位置來(lái)實(shí)現(xiàn)的。

  • %rbp 是棧幀指針,用于標(biāo)識(shí)當(dāng)前棧幀的起始位置

  • %rdi, %rsi, %rdx, %rcx,%r8, %r9 六個(gè)寄存器用于存儲(chǔ)函數(shù)調(diào)用時(shí)的6個(gè)參數(shù)(如果有6個(gè)或6個(gè)以上參數(shù)的話(huà))。

  • 被標(biāo)識(shí)為 “miscellaneous registers” 的寄存器,屬于通用性更為廣泛的寄存器,編譯器或匯編程序可以根據(jù)需要存儲(chǔ)任何數(shù)據(jù)。

這里還要區(qū)分一下 “Caller Save” 和 ”Callee Save” 寄存器,即寄存器的值是由”調(diào)用者保存“ 還是由 ”被調(diào)用者保存“。當(dāng)產(chǎn)生函數(shù)調(diào)用時(shí),子函數(shù)內(nèi)通常也會(huì)使用到通用寄存器,那么這些寄存器中之前保存的調(diào)用者(父函數(shù))的值就會(huì)被覆蓋。為了避免數(shù)據(jù)覆蓋而導(dǎo)致從子函數(shù)返回時(shí)寄存器中的數(shù)據(jù)不可恢復(fù),CPU 體系結(jié)構(gòu)中就規(guī)定了通用寄存器的保存方式。

如果一個(gè)寄存器被標(biāo)識(shí)為”Caller Save”, 那么在進(jìn)行子函數(shù)調(diào)用前,就需要由調(diào)用者提前保存好這些寄存器的值,保存方法通常是把寄存器的值壓入堆棧中,調(diào)用者保存完成后,在被調(diào)用者(子函數(shù))中就可以隨意覆蓋這些寄存器的值了。如果一個(gè)寄存被標(biāo)識(shí)為“Callee Save”,那么在函數(shù)調(diào)用時(shí),調(diào)用者就不必保存這些寄存器的值而直接進(jìn)行子函數(shù)調(diào)用,進(jìn)入子函數(shù)后,子函數(shù)在覆蓋這些寄存器之前,需要先保存這些寄存器的值,即這些寄存器的值是由被調(diào)用者來(lái)保存和恢復(fù)的。

函數(shù)的調(diào)用

子函數(shù)調(diào)用時(shí),調(diào)用者與被調(diào)用者的棧幀結(jié)構(gòu)如下圖所示:

在子函數(shù)調(diào)用時(shí),執(zhí)行的操作有:父函數(shù)將調(diào)用參數(shù)從后向前壓棧 -> 將返回地址壓棧保存 -> 跳轉(zhuǎn)到子函數(shù)起始地址執(zhí)行 -> 子函數(shù)將父函數(shù)棧幀起始地址(%rpb) 壓棧 -> 將 %rbp 的值設(shè)置為當(dāng)前 %rsp 的值,即將 %rbp 指向子函數(shù)棧幀的起始地址。

上述過(guò)程中,保存返回地址和跳轉(zhuǎn)到子函數(shù)處執(zhí)行由 call 一條指令完成,在call 指令執(zhí)行完成時(shí),已經(jīng)進(jìn)入了子程序中,因而將上一棧幀%rbp 壓棧的操作,需要由子程序來(lái)完成。函數(shù)調(diào)用時(shí)在匯編層面的指令序列如下:

... # 參數(shù)壓棧 call FUNC # 將返回地址壓棧,并跳轉(zhuǎn)到子函數(shù) FUNC 處執(zhí)行 ... # 函數(shù)調(diào)用的返回位置 FUNC: # 子函數(shù)入口 pushq %rbp # 保存舊的幀指針,相當(dāng)于創(chuàng)建新的棧幀 movq %rsp, %rbp # 讓 %rbp 指向新棧幀的起始位置 subq $N, %rsp # 在新棧幀中預(yù)留一些空位,供子程序使用,用 (%rsp+K) 或 (%rbp-K) 的形式引用空位

保存返回地址和保存上一棧幀的%rbp 都是為了函數(shù)返回時(shí),恢復(fù)父函數(shù)的棧幀結(jié)構(gòu)。在使用高級(jí)語(yǔ)言進(jìn)行函數(shù)調(diào)用時(shí),由編譯器自動(dòng)完成上述整個(gè)流程。對(duì)于”Caller Save” 和 “Callee Save” 寄存器的保存和恢復(fù),也都是由編譯器自動(dòng)完成的。

需要注意的是,父函數(shù)中進(jìn)行參數(shù)壓棧時(shí),順序是從后向前進(jìn)行的。但是,這一行為并不是固定的,是依賴(lài)于編譯器的具體實(shí)現(xiàn)的,在gcc 中,使用的是從后向前的壓棧方式,這種方式便于支持類(lèi)似于 printf(“%d, %d”, i, j) 這樣的使用變長(zhǎng)參數(shù)的函數(shù)調(diào)用。

函數(shù)的返回

函數(shù)返回時(shí),我們只需要得到函數(shù)的返回值(保存在 %rax 中),之后就需要將棧的結(jié)構(gòu)恢復(fù)到函數(shù)調(diào)用之差的狀態(tài),并跳轉(zhuǎn)到父函數(shù)的返回地址處繼續(xù)執(zhí)行。由于函數(shù)調(diào)用時(shí)已經(jīng)保存了返回地址和父函數(shù)棧幀的起始地址,要恢復(fù)到子函數(shù)調(diào)用之前的父棧幀,我們只需要執(zhí)行以下兩條指令:

movq %rbp, %rsp # 使 %rsp 和 %rbp 指向同一位置,即子棧幀的起始處 popq %rbp # 將棧中保存的父棧幀的 %rbp 的值賦值給 %rbp,并且 %rsp 上移一個(gè)位置指向父棧幀的結(jié)尾處

為了便于棧幀恢復(fù),x86-64 架構(gòu)中提供了 leave 指令來(lái)實(shí)現(xiàn)上述兩條命令的功能。執(zhí)行 leave 后,前面圖中函數(shù)調(diào)用的棧幀結(jié)構(gòu)如下:

可以看出,調(diào)用 leave 后,%rsp 指向的正好是返回地址,x86-64 提供的 ret 指令,其作用就是從當(dāng)前 %rsp 指向的位置(即棧頂)彈出數(shù)據(jù),并跳轉(zhuǎn)到此數(shù)據(jù)代表的地址處,在leave 執(zhí)行后,%rsp 指向的正好是返回地址,因而 ret 的作用就是把 %rsp 上移一個(gè)位置,并跳轉(zhuǎn)到返回地址執(zhí)行??梢钥闯?,leave 指令用于恢復(fù)父函數(shù)的棧幀,ret 用于跳轉(zhuǎn)到返回地址處,leave 和ret 配合共同完成了子函數(shù)的返回。當(dāng)執(zhí)行完成 ret 后,%rsp 指向的是父棧幀的結(jié)尾處,父棧幀尾部存儲(chǔ)的調(diào)用參數(shù)由編譯器自動(dòng)釋放。

函數(shù)調(diào)用示例

為了更深入的了解函數(shù)調(diào)用原理,我們可以使用一個(gè)程序示例來(lái)觀察函數(shù)的調(diào)用和返回。程序如下:

int add(int a, int b, int c, int d, int e, int f, int g, int h) { // 8 個(gè)參數(shù)相加 int sum = a + b + c + d + e + f + g + h; return sum; } int main(void) { int i = 10; int j = 20; int k = i + j; int sum = add(11, 22,33, 44, 55, 66, 77, 88); int m = k; // 為了觀察 %rax Caller Save 寄存器的恢復(fù) return 0; }

在main 函數(shù)中,首先進(jìn)行了一個(gè) k=i+j 的加法,這是為了觀察 Caller Save 效果。因?yàn)榧臃〞?huì)用到 %rax,而下面 add 函數(shù)的返回值也會(huì)使用 %rax。由于 %rax 是 Caller Save 寄存器,在調(diào)用 add 子函數(shù)之前,程序應(yīng)該先保存 %rax 的值。

add 函數(shù)使用了 8 個(gè)參數(shù),這是為了觀察當(dāng)函數(shù)參數(shù)多于6個(gè)時(shí)程序的行為,前6個(gè)參數(shù)會(huì)保存到寄存器中,多于6個(gè)的參數(shù)會(huì)保存到堆棧中。但是,由于在子程序中可能會(huì)取參數(shù)的地址,而保存在寄存器中的前6個(gè)參數(shù)是沒(méi)有內(nèi)存地址的,因而我們可以猜測(cè),保存在寄存器中的前6個(gè)參數(shù),在子程序中也會(huì)被壓入到堆棧中,這樣才能取到這6個(gè)參數(shù)的內(nèi)存地址。上面程序生成的和子函數(shù)調(diào)用相關(guān)的匯編程序如下:

add: .LFB2: pushq %rbp .LCFI0: movq %rsp, %rbp .LCFI1: movl %edi, -20(%rbp) movl %esi, -24(%rbp) movl %edx, -28(%rbp) movl %ecx, -32(%rbp) movl %r8d, -36(%rbp) movl %r9d, -40(%rbp) movl -24(%rbp), %eax addl -20(%rbp), %eax addl -28(%rbp), %eax addl -32(%rbp), %eax addl -36(%rbp), %eax addl -40(%rbp), %eax addl 16(%rbp), %eax addl 24(%rbp), %eax movl %eax, -4(%rbp) movl -4(%rbp), %eax leave ret main: .LFB3: pushq %rbp .LCFI2: movq %rsp, %rbp .LCFI3: subq $48, %rsp .LCFI4: movl $10, -20(%rbp) movl $20, -16(%rbp) movl -16(%rbp), %eax addl -20(%rbp), %eax movl %eax, -12(%rbp) movl $88, 8(%rsp) movl $77, (%rsp) movl $66, %r9d movl $55, %r8d movl $44, %ecx movl $33, %edx movl $22, %esi movl $11, %edi call add movl %eax, -8(%rbp) movl -12(%rbp), %eax movl %eax, -4(%rbp) movl $0, %eax leave ret

在匯編程序中,如果使用的是64位通用寄存器的低32位,則寄存器以 ”e“ 開(kāi)頭,比如 %eax,%ebx 等,對(duì)于 %r8-%r15,其低32 位是在64位寄存后加 “d” 來(lái)表示,比如 %r8d, %r15d。如果操作數(shù)是32 位的,則指令以 ”l“ 結(jié)尾,例如 movl $11, %esi,指令和寄存器都是32位的格式。如果操作數(shù)是64 位的,則指令以 q 結(jié)尾,例如 “movq %rsp, %rbp”。由于示例程序中的操作數(shù)全部在32位的表示范圍內(nèi),因而上面的加法和移動(dòng)指令全部是用的32位指令和操作數(shù),只有在創(chuàng)建棧幀時(shí)為了地址對(duì)齊才使用的是64位指令及操作數(shù)。

首先看 main 函數(shù)的前三條匯編語(yǔ)句:

.LFB3: pushq %rbp .LCFI2: movq %rsp, %rbp .LCFI3: subq $48, %rsp

這三條語(yǔ)句保存了父函數(shù)的棧幀(注意main函數(shù)也有父函數(shù)),之后創(chuàng)建了main 函數(shù)的棧幀并且在棧幀中分配了48Byte 的空位,這三條語(yǔ)句執(zhí)行完成后,main 函數(shù)的棧幀如下圖所示:

之后,main 函數(shù)中就進(jìn)行了 k=i+j 的加法和 add 參數(shù)的處理:

movl $10, -20(%rbp) movl $20, -16(%rbp) movl -16(%rbp), %eax addl -20(%rbp), %eax movl %eax, -12(%rbp) # 調(diào)用子函數(shù)前保存 %eax 的值到棧中,caller save movl $88, 8(%rsp) movl $77, (%rsp) movl $66, %r9d movl $55, %r8d movl $44, %ecx movl $33, %edx movl $22, %esi movl $11, %edi call add

在進(jìn)行 k=i+j 加法時(shí),使用 main ??臻g的方式較為特別。并不是按照我們通常認(rèn)為的每使用一個(gè)??臻g就會(huì)進(jìn)行一次push 操作,而是使用之前預(yù)先分配的 48 個(gè)空位,并且用 -N(%rbp) 即從 %rbp 指向的位置向下計(jì)數(shù)的方式來(lái)使用空位的,本質(zhì)上這和每次進(jìn)行 push 操作是一樣的,最后計(jì)算 i+j 得到的結(jié)果 k 保存在了 %eax 中。之后就需要準(zhǔn)備調(diào)用 add 函數(shù)了。

我們知道,add 函數(shù)的返回值會(huì)保存在 %eax 中,即 %eax 一定會(huì)被子函數(shù) add 覆蓋,而現(xiàn)在 %eax 中保存的是 k 的值。在 C 程序中可以看到,在調(diào)用完成 add 后,我們又使用了 k 的值,因而在調(diào)用 add 中覆蓋%eax 之前,需要保存 %eax 值,在add 使用完%eax 后,需要恢復(fù) %eax 值(即k 的值),由于 %eax 是 Caller Save的,應(yīng)該由父函數(shù)main來(lái)保存 %eax 的值,因而上面匯編中有一句 “movl %eax, -12(%rbp)” 就是在調(diào)用 add 函數(shù)之前來(lái)保存 %eax 的值的。

對(duì)于8個(gè)參數(shù),可以看出,最后兩個(gè)參數(shù)是從后向前壓入了棧中,前6個(gè)參數(shù)全部保存到了對(duì)應(yīng)的參數(shù)寄存器中,與本文開(kāi)始描述的一致。

進(jìn)入 add 之后的操作如下:

add: .LFB2: pushq %rbp # 保存父棧幀指針 .LCFI0: movq %rsp, %rbp # 創(chuàng)建新棧幀 .LCFI1: movl %edi, -20(%rbp) # 在寄存器中的參數(shù)壓棧 movl %esi, -24(%rbp) movl %edx, -28(%rbp) movl %ecx, -32(%rbp) movl %r8d, -36(%rbp) movl %r9d, -40(%rbp) movl -24(%rbp), %eax addl -20(%rbp), %eax addl -28(%rbp), %eax addl -32(%rbp), %eax addl -36(%rbp), %eax addl -40(%rbp), %eax addl 16(%rbp), %eax addl 24(%rbp), %eax movl %eax, -4(%rbp) movl -4(%rbp), %eax leave ret

add 中最前面兩條指令實(shí)現(xiàn)了新棧幀的創(chuàng)建。之后把在寄存器中的函數(shù)調(diào)用參數(shù)壓入了棧中。在本文前面提到過(guò),由于子程序中可能會(huì)用到參數(shù)的內(nèi)存地址,這些參數(shù)放在寄存器中是無(wú)法取地址的,這里把參數(shù)壓棧,正好印證了我們之前的猜想。

在參數(shù)壓棧時(shí),我們看到并未使用 push 之類(lèi)的指令,也沒(méi)有調(diào)整 %esp 指針的值,而是使用了 -N(%rbp) 這樣的指令來(lái)使用新的棧空間。這種使用”基地址+偏移量“ 來(lái)使用棧的方式和直接使用 %esp 指向棧頂?shù)姆绞狡鋵?shí)是一樣的。

這里有兩個(gè)和編譯器具體實(shí)現(xiàn)相關(guān)的問(wèn)題:一是上面程序中,-8(%rbp) 和 -12(%rbp) 地址并未被使用到,這兩個(gè)地址之前的地址 -4(%rbp) 和之后的 -16(%rsp) 都被使用到了,這可能是由于編譯器具體的實(shí)現(xiàn)方式來(lái)決定的。另外一個(gè)就是如下兩條指令:

movl %eax, -4(%rbp) movl -4(%rbp), %eax

先是把 %eax 的值賦值給的 -4(%rbp),之后又逆向賦值了一次,猜測(cè)可能是編譯器為了通用性才如此操作的。以上兩個(gè)問(wèn)題需要后續(xù)進(jìn)一步研究。

當(dāng)add函數(shù)返回后,返回結(jié)果會(huì)存儲(chǔ)在%eax 中,%rbp 和 %rsp 會(huì)調(diào)整為指向 main 的棧幀,之后會(huì)執(zhí)行main 函數(shù)中的如下指令:

movl %eax, -8(%rbp) # 保存 add 函數(shù)返回值到棧中,對(duì)應(yīng) C 語(yǔ)句 int sum = add(...) movl -12(%rbp), %eax # 恢復(fù) call save 寄存器 %eax 的值,與調(diào)用add前保存 %eax 相對(duì)應(yīng) movl %eax, -4(%rbp) # 對(duì)應(yīng) C 語(yǔ)句 m = k,%eax 中的值就是 k。 movl $0, %eax # main 函數(shù)返回值 leave # main 函數(shù)返回 ret

可以看出,當(dāng) add 函數(shù)返回時(shí),把返回值保存到了 %eax 中,使用完返回值后,會(huì)恢復(fù) caller save 寄存器 %eax的值,這時(shí)main 棧幀與調(diào)用 add 之前完全一樣。

需要注意的是,在調(diào)用 add 之前,main 中執(zhí)行了一條 subq 48, %rsp 這樣的指令,原因就在于調(diào)用 add 之后,main 中并未調(diào)用其他函數(shù),而是執(zhí)行了兩條賦值語(yǔ)句后就直接從main返回了。 main 結(jié)尾處的 leave、ret 兩條指令會(huì)直接覆蓋 %rsp 的值從而回到 main 的父棧幀中。如果先調(diào)整 main 棧幀的 %rsp 值,之后 leave 再覆蓋 %rsp 的值,相當(dāng)于調(diào)整是多余的。因而省略main 中 add返回之后的 %rsp 的調(diào)整,而使用 leave 直接覆蓋%rsp更為合理。

結(jié)語(yǔ)

本文從匯編層面介紹了X86-64 架構(gòu)下函數(shù)調(diào)用時(shí)棧幀的切換原理,了解這些底層細(xì)節(jié)對(duì)于理解程序的運(yùn)行情況是十分有益的。并且在當(dāng)前許多程序中,為了實(shí)現(xiàn)程序的高效運(yùn)行,都使用了匯編語(yǔ)言,在了解了函數(shù)棧幀切換原理后,對(duì)于理解這些匯編也是非常有幫助的。

在下一篇文章中,將會(huì)詳細(xì)介紹 libco 庫(kù)中用匯編語(yǔ)言實(shí)現(xiàn)的協(xié)程上下文的切換,本文可以作為理解協(xié)程上下文切換的基礎(chǔ)。

The End.

我就是我,疾馳中的企鵝。

我就是我,不一樣的焰火。

編輯于 2017-06-10

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶(hù)發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買(mǎi)等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶(hù) 評(píng)論公約

    類(lèi)似文章 更多

    粉嫩国产美女国产av| 午夜福利黄片免费观看| 久久精品福利在线观看| 激情中文字幕在线观看 | 欧美日韩亚洲国产精品| 夜夜躁狠狠躁日日躁视频黑人| 东北老熟妇全程露脸被内射| 日韩美女偷拍视频久久| 91人妻丝袜一区二区三区| 日韩特级黄片免费观看| 中国日韩一级黄色大片| 国产成人精品一区在线观看| 九九热视频免费在线视频| 久久碰国产一区二区三区| 亚洲成人精品免费在线观看| 在线免费不卡亚洲国产| 免费特黄欧美亚洲黄片| 国产又粗又深又猛又爽又黄| 日本黄色录像韩国黄色录像| 精品日韩av一区二区三区| 色婷婷视频在线精品免费观看 | 黄男女激情一区二区三区| 国产精品午夜性色视频| 欧美一区二区在线日韩| 国产精品刮毛视频不卡| 国产欧美日产中文一区| 最新午夜福利视频偷拍| 中文字幕乱码亚洲三区| 激情综合五月开心久久| 久热99中文字幕视频在线| 黄片免费在线观看日韩| 久久免费精品拍拍一区二区| 亚洲人午夜精品射精日韩| 国产又大又黄又粗又免费| 亚洲精品福利视频在线观看| 日本人妻精品有码字幕| 成人精品视频在线观看不卡| 久久99精品国产麻豆婷婷洗澡| 日韩在线视频精品中文字幕| 国产一区二区精品高清免费| 国产精品欧美一区两区|