<samp id="tdkee"></samp>

    <mark id="tdkee"><acronym id="tdkee"></acronym></mark>

  1. 免费看又黄又无码的网站_久久久高清国产免费观看_亚洲一区二区三区在线免费观看_免费欧洲美女a视频一级毛片_日本最新免费二区三区

    ×

    【Golang】圖解函數(shù)調(diào)用棧

    分類(lèi):互聯(lián)網(wǎng)熱點(diǎn) 編輯:互聯(lián)網(wǎng)觀察 瀏覽量:43
    2020-07-20 13:33:06
    “ 不要小瞧函數(shù)調(diào)用棧哦,它可是理解參數(shù)傳遞、命名匿名返回值、Function Value、defer等面試??偷年P(guān)鍵吶~”我們按照編程語(yǔ)言的語(yǔ)法定義的函數(shù),會(huì)被編譯器編譯為一堆堆機(jī)器指令,寫(xiě)入可執(zhí)行文件。程序執(zhí)行時(shí)可執(zhí)行文件被加載到內(nèi)存,這些機(jī)器指令對(duì)應(yīng)到虛擬地址空間中,位于代碼段。如果在一個(gè)函數(shù)中調(diào)用另一個(gè)函數(shù),編譯器就會(huì)對(duì)應(yīng)生成一條call指令,程序執(zhí)行到這條指令時(shí),就會(huì)跳轉(zhuǎn)到被調(diào)用函數(shù)入口處開(kāi)始執(zhí)行,而每個(gè)函數(shù)的最后都有一條ret指令,負(fù)責(zé)在函數(shù)結(jié)束后跳回到調(diào)用處,繼續(xù)執(zhí)行。
    01—

    函數(shù)棧幀

    函數(shù)執(zhí)行時(shí)需要有足夠的內(nèi)存空間,供它存放局部變量、參數(shù)等數(shù)據(jù),這段空間對(duì)應(yīng)到虛擬地址空間的棧。棧,只有一個(gè) 口可供進(jìn)出,先入棧的在底,后入棧的在頂,最后入棧的最早被取出。運(yùn)行時(shí)棧,上面是高地址,向下增長(zhǎng),棧底通常被稱(chēng)為“?;?,棧頂被稱(chēng)為“棧指針”。

    棧高地址向下增長(zhǎng)?;鶙V羔?/p>

    分配給函數(shù)的??臻g被稱(chēng)為“函數(shù)棧幀”,Go語(yǔ)言中函數(shù)棧幀布局是這樣的,先是調(diào)用者?;刂罚缓笫呛瘮?shù)的局部變量,最后是被調(diào)用函數(shù)的返回值和參數(shù)。

    函數(shù)棧幀BP of calleeSP of calleeBP of caller局部變量返回值參數(shù)

    圖:函數(shù)棧幀布局
    BP of callee和SP of callee標(biāo)識(shí)被調(diào)用函數(shù)執(zhí)行時(shí),?;拇嫫骱蜅V羔樇拇嫫髦赶虻奈恢?,但是注意“BP of caller”不一定會(huì)存在,有些情況下可能會(huì)被優(yōu)化掉,也有可能是平臺(tái)不支持。我們只關(guān)注局部變量和參數(shù)、返回值的相對(duì)位置就好。舉個(gè)例子,函數(shù)A調(diào)用函數(shù)B,函數(shù)A有兩個(gè)局部變量,函數(shù)B有兩個(gè)參數(shù)和兩個(gè)返回值。
    func A() { var a1, a2, r1, r2 int64 a1, a2 = 1, 2 r1, r2 = B(a1, a2) r1 = C(a1) println(r1, r2)}func B(p1, p2 int64) (int64, int64) { return p2, p1}func C(p1 int64) int64 { return p1}
    函數(shù)A的棧幀布局如下圖所示,局部變量之后的空間用于存放被調(diào)用函數(shù)的返回值和參數(shù),接下來(lái)要調(diào)用函數(shù)B,所以先有兩個(gè)int64類(lèi)型的變量空間用作B的返回值,再有兩個(gè)int64類(lèi)型的變量空間用于存放傳遞給B的參數(shù)。

    ......return1棧a1a2r1r2局部變量BP of ASP of A返回值BP of callerreturn2param1參數(shù)param2

    圖:函數(shù)A棧幀布局
    注意觀察參數(shù)的順序,先入棧第二個(gè)參數(shù),再入棧第一個(gè)參數(shù),返回值也是一樣,上面是第二個(gè)返回值的空間,然后才是第一個(gè)返回值的空間。其實(shí)這也好解釋?zhuān)驗(yàn)檫@些是被調(diào)用函數(shù)的返回值和參數(shù),被調(diào)用函數(shù)是通過(guò)棧指針加上偏移值這樣相對(duì)尋址的方式來(lái)定位到自己的參數(shù)和返回值的,這樣由下至上正好先找到第一個(gè)參數(shù),再找到第二個(gè)參數(shù)。所以參數(shù)和返回值采用由右至左的入棧順序比較合適。“通常,我們認(rèn)為返回值是通過(guò)寄存器傳遞的,但是Go語(yǔ)言支持多返回值,所以在棧上分配返回值空間更合適~”

    ......return1棧a1a2r1r2局部變量BP of ASP of A返回值BP of callerreturn2param1參數(shù)param2SP of B+偏移......SP of B

    圖:被調(diào)用函數(shù)相對(duì)尋址參數(shù)
    我們知道對(duì)函數(shù)B的調(diào)用會(huì)被編譯器編譯為call指令。實(shí)際上call指令只做兩件事:第一:將下一條指令的地址入棧,被調(diào)用函數(shù)執(zhí)行結(jié)束后會(huì)跳回到這個(gè)地址繼續(xù)執(zhí)行,這就是函數(shù)調(diào)用的“返回地址”。第二:跳轉(zhuǎn)到被調(diào)用的函數(shù)B指令入口處執(zhí)行,所以在“返回地址”下面就是函數(shù)B的棧幀了。

    ......return1棧a1a2r1r2局部變量BP of ASP of A返回值BP of callerreturn2param1參數(shù)param2SP of B 返回地址BP of A......

    圖:函數(shù)B的棧幀
    所有函數(shù)的棧幀布局都遵循統(tǒng)一的約定,函數(shù)B結(jié)束后它的棧幀被釋放,回到函數(shù)A中繼續(xù)執(zhí)行。

    ......return1棧a1a2r1r2局部變量BP of ASP of A返回值BP of callerreturn2param1參數(shù)param2SP of B返回地址BP of A......

    圖:函數(shù)B結(jié)束
    到了調(diào)用函數(shù)C的時(shí)候,它只有一個(gè)參數(shù)和一個(gè)返回值,它們會(huì)占用函數(shù)A棧幀中最下面的一部分空間,所以上面會(huì)空出來(lái)一塊,這是為了在被調(diào)用函數(shù)中可以用標(biāo)準(zhǔn)的相對(duì)地址定位到自己的參數(shù)和返回值,而無(wú)需顧慮其它。同樣的,call指令會(huì)壓入返回地址,并跳轉(zhuǎn)到函數(shù)C的指令入口處,所以下面就是函數(shù)C的棧幀了。

    ......return1棧a1a2r1r2局部變量BP of ASP of A返回值BP of callerparam1參數(shù)SP of C 返回地址BP of A......

    圖:調(diào)用函數(shù)C
    Go語(yǔ)言中,函數(shù)棧幀是一次性分配的,也就是在函數(shù)開(kāi)始執(zhí)行的時(shí)候分配足夠大的棧幀空間。就像上例中函數(shù)A一樣,它要調(diào)用兩個(gè)函數(shù),除了調(diào)用者棧基地址、局部變量以外,再有四個(gè)int64的空間用作被調(diào)用函數(shù)的參數(shù)與返回值就足夠了。一次性分配函數(shù)棧幀的主要原因是避免棧訪問(wèn)越界,如下圖所示,三個(gè)goroutine初始分配的??臻g是一樣的,如果g2剩余的??臻g不夠執(zhí)行接下來(lái)的函數(shù),若函數(shù)棧幀是逐步擴(kuò)張的,那么執(zhí)行期間就可能發(fā)生棧訪問(wèn)越界。

    ......棧g1g2g3free越界了

    圖:棧訪問(wèn)越界
    其實(shí),對(duì)于棧消耗較大的函數(shù),go語(yǔ)言的編譯器還會(huì)在函數(shù)頭部插入檢測(cè)代碼,如果發(fā)現(xiàn)需要進(jìn)行“棧增長(zhǎng)”,就會(huì)另外分配一段足夠大的??臻g,并把原來(lái)?xiàng)I系臄?shù)據(jù)拷過(guò)來(lái),原來(lái)的這段??臻g就被釋放了。

    ......棧g1g2g3free

    圖:g2棧增長(zhǎng)示意圖
    了解了函數(shù)棧幀布局,接下來(lái),我們看幾個(gè)關(guān)于參數(shù)和返回值常見(jiàn)的問(wèn)題。
    02—

    傳參

    下面有一個(gè)swap函數(shù),接收兩個(gè)整型參數(shù),main函數(shù)想要通過(guò)swap來(lái)交換兩個(gè)局部變量的值,但是失敗了......
    func swap(a,b int) { a,b = b,a} func main() { a,b := 1,2 swap(a,b) println(a,b) //1,2}
    我們通過(guò)函數(shù)調(diào)用棧,看看失敗的原因到底在哪兒?main函數(shù)棧幀中,先分配局部變量存儲(chǔ)空間,a=1,b=2。因?yàn)槔又姓{(diào)用的函數(shù)沒(méi)有返回值,所以局部變量后面就是給被調(diào)用函數(shù)傳入的參數(shù)。需要傳入兩個(gè)整型參數(shù),Go語(yǔ)言中傳參都是值拷貝,參數(shù)是整型,所以拷貝整型變量值。注意參數(shù)入棧順序:由右至左。先入棧第二個(gè)參數(shù),再入棧第一個(gè)參數(shù)。

    ......棧a=1b=2b=2a=1局部變量SP of main參數(shù)BP of caller......BP of main返回地址SP of swap值拷貝

    圖:函數(shù)main棧幀布局
    調(diào)用者棧幀后面是call指令存入的返回地址,swap開(kāi)始執(zhí)行,再下面分配的就是swap函數(shù)棧幀了。swap函數(shù)要交換兩個(gè)參數(shù)的值,但是注意,swap的參數(shù)在哪里?main的局部變量a和b又在哪里?找到它們,交換失敗的原因就找到了,想要交換的局部變量a和b在局部變量空間,但實(shí)際上交換的是參數(shù)空間的a和b。

    ......棧a=1b=2b=1a=2局部變量SP of main參數(shù)BP of caller...... BP of main 返回地址SP of swap交換

    圖:交換失敗的原因
    再來(lái)個(gè)例子,依然要交換兩個(gè)整型變量的值,但是參數(shù)類(lèi)型改為整型指針。這次交換成功了,同樣通過(guò)函數(shù)調(diào)用棧,看看和上次有什么不同。
    func swap(a,b *int) { *a,*b = *b,*a} func main() { a,b := 1,2 swap(&a,&b) println(a,b) //2,1}
    main函數(shù)棧幀中,先分配局部變量,然后分配參數(shù)空間,參數(shù)是指針,傳參都是值拷貝,這里拷貝的是a和b的地址。依然由右至左,先入棧b的地址, 再入棧a的地址。再后面是返回地址,以及swap函數(shù)棧幀。

    ......棧a=1b=2&b&a局部變量SP of main參數(shù)BP of caller...... BP of main 返回地址SP of swap值拷貝地址

    圖:函數(shù)棧幀布局
    swap要交換的是這兩個(gè)參數(shù)指針指向的數(shù)據(jù),也就是局部變量空間這里的a和b,所以這一次能夠交換成功!

    ......棧a=2b=1&b&a局部變量SP of main參數(shù)BP of caller...... BP of main 返回地址SP of swap交換

    圖:交換成功
    03—

    返回值

    直接看例子,這里main函數(shù)調(diào)用incr函數(shù),然后把返回值賦給局部變量b,下面來(lái)看看函數(shù)調(diào)用棧的情況。
    func incr(a int) int { var b int defer func(){ a++ b++ }() a++ b = a return b}func main(){ var a,b int b = incr(a) println(a,b) //0,1}
    main函數(shù)棧幀,先是局部變量,a=0,b=0,然后是incr的返回值,初始化為類(lèi)型零值,再然后是參數(shù)空間。到incr函數(shù)棧幀這里,保存調(diào)用者main的?;刂泛?,初始化局部變量b。

    ......棧a=0b=00a=0局部變量SP of main參數(shù)BP of callerb=0 BP of main 返回地址SP of incr返回值......

    圖:a++執(zhí)行前
    incr函數(shù)會(huì)把參數(shù)a自增一,然后賦值給局部變量b,要注意它們的位置。

    ......棧a=0b=00a=1局部變量SP of main參數(shù)BP of caller...... BP of main 返回地址SP of incr返回值b=1

    圖:a++; b=a
    到incr函數(shù)的return這里,必須要明確一個(gè)關(guān)鍵問(wèn)題。incr函數(shù)返回之前要給返回值賦值并執(zhí)行defer函數(shù),那誰(shuí)先?誰(shuí)后?答案是:“先賦值”所以incr函數(shù)返回前,會(huì)先把局部變量b的值拷貝到返回值空間,然后再執(zhí)行注冊(cè)的defer函數(shù)。

    ......棧a=0b=01a=1局部變量SP of main參數(shù)BP of caller...... BP of main 返回地址SP of incr返回值b=1拷貝返回值

    圖:incr返回前先拷貝返回值
    在defer函數(shù)里,a再次自增1,局部變量b也自增1。

    ......棧a=0b=01a=2局部變量SP of main參數(shù)BP of caller...... BP of main 返回地址SP of incr返回值b=2

    圖:執(zhí)行defer函數(shù)
    所以,incr結(jié)束后,返回值為1,賦給main函數(shù)局部變量b,最后會(huì)輸出0和1。

    ......棧a=0b=11a=2局部變量SP of main參數(shù)BP of caller...... BP of main 返回地址SP of incr返回值b=2b=incr(a)

    圖:incr返回值賦給b
    這是匿名返回值的情況,下面再來(lái)個(gè)例子,其它都不變,只把這里的局部變量b改成命名返回值,看看有什么不同。
    func incr(a int) (b int) { defer func(){ a++ b++ }() a++ return a}func main(){ var a,b int b = incr(a) println(a,b) //0,2}
    main函數(shù)棧幀,與上個(gè)例子完全相同,到incr函數(shù)棧幀這里,沒(méi)有局部變量,執(zhí)行到a++時(shí),參數(shù)a自增1。返回前,先把參數(shù)a賦給返回值b,要注意返回值的位置。

    ......棧a=0b=0b=1a=1局部變量SP of main參數(shù)BP of caller...... BP of main 返回地址返回值拷貝返回值SP of incr

    圖:拷貝返回值
    然后執(zhí)行defer函數(shù),參數(shù)a再次自增1,返回值b也自增1,然后incr結(jié)束,返回值最終為2。

    ......棧a=0b=0b=2a=2局部變量SP of main參數(shù)BP of caller......BP of main返回地址返回值SP of incr

    圖:執(zhí)行defer函數(shù)
    所以, main的局部變量b賦值為2,最后會(huì)輸出0和2。

    ......棧a=0b=2b=2a=2局部變量SP of main參數(shù)BP of caller...... BP of main 返回地址SP of incr返回值b=incr(a)

    圖:incr結(jié)束
    命名返回值和匿名返回值相關(guān)的問(wèn)題,最關(guān)鍵的還是函數(shù)棧幀布局,以及返回值被賦值的時(shí)機(jī)。最后,留給感興趣的同學(xué)看看,在匯編指令層面怎么實(shí)現(xiàn)函數(shù)跳轉(zhuǎn)與返回。
    04—

    函數(shù)跳轉(zhuǎn)與返回

    程序執(zhí)行時(shí) CPU用特定寄存器來(lái)存儲(chǔ)運(yùn)行時(shí)?;c棧指針,同時(shí)也有指令指針寄存器用于存儲(chǔ)下一條要執(zhí)行的指令地址。

    ......棧寄存器BPSPIP?;鶙V羔樦噶钪羔樦噶頿ush 3push 4

    如果接下來(lái)要執(zhí)行"push 3"這條指令,CPU讀取后,會(huì)將指令指針移向下一條指令,然后棧指針向下移動(dòng),數(shù)字3入棧。

    ......棧寄存器BPSPIP?;鶙V羔樦噶钪羔樦噶頿ush 3push 43

    繼續(xù)執(zhí)行下一條指令,再次移動(dòng)棧指針入棧數(shù)字4。

    ......棧寄存器BPSPIP?;鶙V羔樦噶钪羔樦噶頿ush 3push 4......34

    前面我們提過(guò)Go語(yǔ)言中函數(shù)棧幀不是這樣逐步擴(kuò)張的,而是一次性分配,也就是在分配棧幀時(shí),直接將棧指針移動(dòng)到所需最大??臻g的位置。

    ......棧寄存器BPSPIP?;鶙V羔樦噶钪羔樦噶畎?移動(dòng)到SP+16處把4移動(dòng)到SP+8處

    然后通過(guò)棧指針加上偏移值這種相對(duì)尋址方式使用函數(shù)棧幀。例如sp加16字節(jié)處存儲(chǔ)3,加8字節(jié)處存儲(chǔ)4,諸如此類(lèi)。

    ......棧寄存器BPSPIP?;鶙V羔樦噶钪羔樦噶畎?移動(dòng)到SP+16處把4移動(dòng)到SP+8處......34

    接下來(lái)我們看看call指令和ret指令,是怎樣實(shí)現(xiàn)函數(shù)跳轉(zhuǎn)與返回的。
    func A(){ a,b := 1,2 B(a,b) return}func B(c,d int){ println(c,d) return}
    調(diào)用函數(shù)B之前函數(shù)A棧幀如下圖所示,注意函數(shù)A和函數(shù)B的指令分布在代碼段,而且函數(shù)A調(diào)用函數(shù)B的call指令在地址a1處,函數(shù)B入口地址在b1處。

    ......棧a=1b=2......代碼段............a1call b1~~~~~~~~~~~~~~~~b1~~~~~~~~......~~~~~~~~~~~~~~~~RETAB寄存器BPSPa1IPs1s2d=2c=1s3s4s5s6

    圖:call指令執(zhí)行前
    然后到call指令這里,它的作用有兩點(diǎn):第一,把返回地址a2入棧保存起來(lái);第二,跳轉(zhuǎn)到指令地址b1處。

    ......棧a=1b=2......代碼段............a1call b1~~~~~~~~~~~~~~~~b1~~~~~~~~......~~~~~~~~~~~~~~~~RETAB寄存器BPSPb1IPa2a2d=2c=1s1s2s3s4s5s6

    圖:call指令執(zhí)行后
    call指令結(jié)束。函數(shù)B開(kāi)始執(zhí)行,我們先看它最開(kāi)始的三條指令:第一條指令,把SP向下移動(dòng)24字節(jié)(從s6挪到s9),為自己分配足夠大的棧幀;第二條指令,要把調(diào)用者?;鵶1存到SP+16字節(jié)的地方(s7那里);第三條指令,把s7(SP+16)存入BP寄存器。

    棧a=1b=2............代碼段............a1call b1~~~~~~~~~~~~~~~~b1~~~~~~~~......~~~~~~~~~~~~~~~~RETAB寄存器BPSPb4IPa2a2s1b4d=2c=1s1s2s3s4s5s6s7s8s9

    圖:執(zhí)行函數(shù)B入口處插入的三條指令
    接下來(lái)就是執(zhí)行函數(shù)B剩下的指令了,沒(méi)有局部變量,只有被調(diào)用者的參數(shù)空間。在最后的ret指令之前,編譯器還會(huì)插入兩條指令:第1條指令:恢復(fù)調(diào)用者A的?;刂?,它之前被存儲(chǔ)在SP+16字節(jié)(s7)這里,所以BP恢復(fù)到s1;第2條指令:釋放自己的棧幀空間,分配時(shí)向下移動(dòng)多少(從s6到s9)釋放時(shí)就向上移動(dòng)多少(從s9到s6)。

    棧a=1b=2......代碼段............a1call b1~~~~~~~~~~~~~~~~b1~~~~~~~~......~~~~~~~~~~~~~~~~RETAB寄存器BPSPbnIPa2a2s1bnc=1d=2c=1d=2......s1s2s3s4s5s6s7s8s9

    圖:執(zhí)行ret指令之前插入的兩條指令
    然后就到ret指令了,它的作用也有兩點(diǎn):第一,彈出call指令壓棧的返回地址a2;第二,跳轉(zhuǎn)到call指令壓棧的返回地址a2處。

    ......代碼段............a1call b1~~~~~~~~~~~~~~~~b1~~~~~~~~......~~~~~~~~~~~~~~~~RETAB寄存器BPSPa2IPa2棧a=1b=2a2s1c=1d=2c=1d=2......s1s2s3s4s5s6s7s8s9

    圖:ret指令執(zhí)行后
    現(xiàn)在可以從a2這里繼續(xù)執(zhí)行了。簡(jiǎn)單來(lái)說(shuō),函數(shù)通過(guò)call指令實(shí)現(xiàn)跳轉(zhuǎn),而每個(gè)函數(shù)開(kāi)始時(shí)會(huì)分配棧幀,結(jié)束前又釋放自己的棧幀,ret指令又會(huì)把?;謴?fù)到call之前的樣子,通過(guò)這些指令的配合最終實(shí)現(xiàn)了函數(shù)跳轉(zhuǎn)與返回。

    聲明:免責(zé)聲明:本文內(nèi)容由互聯(lián)網(wǎng)用戶(hù)自發(fā)貢獻(xiàn)自行上傳,本網(wǎng)站不擁有所有權(quán),也不承認(rèn)相關(guān)法律責(zé)任。如果您發(fā)現(xiàn)本社區(qū)中有涉嫌抄襲的內(nèi)容,請(qǐng)發(fā)

    送郵件至:operations@xinnet.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),本站將立刻刪除涉嫌侵權(quán)內(nèi)容。本站原創(chuàng)內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)

    需注明出處:新網(wǎng)idc知識(shí)百科

    免費(fèi)咨詢(xún)獲取折扣

    Loading
    国产精品视频白浆免费视频_久久久高清国产免费观看_亚洲一区二区三区在线免费观看_免费欧洲美女a视频一级毛片

    <samp id="tdkee"></samp>

      <mark id="tdkee"><acronym id="tdkee"></acronym></mark>