亚洲综合极品香蕉久久网,久久夜精品综合缴情五月 ,亚洲动漫成人一区二区,国产在线不卡视频,国产丝袜精品不卡,亚洲乱码日产精品bd,久久久久久国产精品免费网站,亚洲综合av一区二区三区
×

【Golang】圖解函數調用棧

分類:互聯網熱點 編輯:互聯網觀察 瀏覽量:43
2020-07-20 13:33:06
“ 不要小瞧函數調用棧哦,它可是理解參數傳遞、命名匿名返回值、Function Value、defer等面試常客的關鍵吶~”我們按照編程語言的語法定義的函數,會被編譯器編譯為一堆堆機器指令,寫入可執行文件。程序執行時可執行文件被加載到內存,這些機器指令對應到虛擬地址空間中,位于代碼段。如果在一個函數中調用另一個函數,編譯器就會對應生成一條call指令,程序執行到這條指令時,就會跳轉到被調用函數入口處開始執行,而每個函數的最后都有一條ret指令,負責在函數結束后跳回到調用處,繼續執行。
01—

函數棧幀

函數執行時需要有足夠的內存空間,供它存放局部變量、參數等數據,這段空間對應到虛擬地址空間的棧。棧,只有一個 口可供進出,先入棧的在底,后入棧的在頂,最后入棧的最早被取出。運行時棧,上面是高地址,向下增長,棧底通常被稱為“?;?,棧頂被稱為“棧指針”。

棧高地址向下增長?;鶙V羔?/p>

分配給函數的棧空間被稱為“函數棧幀”,Go語言中函數棧幀布局是這樣的,先是調用者棧基地址,然后是函數的局部變量,最后是被調用函數的返回值和參數。

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

圖:函數棧幀布局
BP of callee和SP of callee標識被調用函數執行時,棧基寄存器和棧指針寄存器指向的位置,但是注意“BP of caller”不一定會存在,有些情況下可能會被優化掉,也有可能是平臺不支持。我們只關注局部變量和參數、返回值的相對位置就好。舉個例子,函數A調用函數B,函數A有兩個局部變量,函數B有兩個參數和兩個返回值。
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}
函數A的棧幀布局如下圖所示,局部變量之后的空間用于存放被調用函數的返回值和參數,接下來要調用函數B,所以先有兩個int64類型的變量空間用作B的返回值,再有兩個int64類型的變量空間用于存放傳遞給B的參數。

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

圖:函數A棧幀布局
注意觀察參數的順序,先入棧第二個參數,再入棧第一個參數,返回值也是一樣,上面是第二個返回值的空間,然后才是第一個返回值的空間。其實這也好解釋,因為這些是被調用函數的返回值和參數,被調用函數是通過棧指針加上偏移值這樣相對尋址的方式來定位到自己的參數和返回值的,這樣由下至上正好先找到第一個參數,再找到第二個參數。所以參數和返回值采用由右至左的入棧順序比較合適?!巴ǔ?,我們認為返回值是通過寄存器傳遞的,但是Go語言支持多返回值,所以在棧上分配返回值空間更合適~”

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

圖:被調用函數相對尋址參數
我們知道對函數B的調用會被編譯器編譯為call指令。實際上call指令只做兩件事:第一:將下一條指令的地址入棧,被調用函數執行結束后會跳回到這個地址繼續執行,這就是函數調用的“返回地址”。第二:跳轉到被調用的函數B指令入口處執行,所以在“返回地址”下面就是函數B的棧幀了。

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

圖:函數B的棧幀
所有函數的棧幀布局都遵循統一的約定,函數B結束后它的棧幀被釋放,回到函數A中繼續執行。

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

圖:函數B結束
到了調用函數C的時候,它只有一個參數和一個返回值,它們會占用函數A棧幀中最下面的一部分空間,所以上面會空出來一塊,這是為了在被調用函數中可以用標準的相對地址定位到自己的參數和返回值,而無需顧慮其它。同樣的,call指令會壓入返回地址,并跳轉到函數C的指令入口處,所以下面就是函數C的棧幀了。

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

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

......棧g1g2g3free越界了

圖:棧訪問越界
其實,對于棧消耗較大的函數,go語言的編譯器還會在函數頭部插入檢測代碼,如果發現需要進行“棧增長”,就會另外分配一段足夠大的??臻g,并把原來棧上的數據拷過來,原來的這段??臻g就被釋放了。

......棧g1g2g3free

圖:g2棧增長示意圖
了解了函數棧幀布局,接下來,我們看幾個關于參數和返回值常見的問題。
02—

傳參

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

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

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

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

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

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

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

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

圖:交換成功
03—

返回值

直接看例子,這里main函數調用incr函數,然后把返回值賦給局部變量b,下面來看看函數調用棧的情況。
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函數棧幀,先是局部變量,a=0,b=0,然后是incr的返回值,初始化為類型零值,再然后是參數空間。到incr函數棧幀這里,保存調用者main的?;刂泛螅跏蓟植孔兞縝。

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

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

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

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

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

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

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

圖:執行defer函數
所以,incr結束后,返回值為1,賦給main函數局部變量b,最后會輸出0和1。

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

圖:incr返回值賦給b
這是匿名返回值的情況,下面再來個例子,其它都不變,只把這里的局部變量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函數棧幀,與上個例子完全相同,到incr函數棧幀這里,沒有局部變量,執行到a++時,參數a自增1。返回前,先把參數a賦給返回值b,要注意返回值的位置。

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

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

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

圖:執行defer函數
所以, main的局部變量b賦值為2,最后會輸出0和2。

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

圖:incr結束
命名返回值和匿名返回值相關的問題,最關鍵的還是函數棧幀布局,以及返回值被賦值的時機。最后,留給感興趣的同學看看,在匯編指令層面怎么實現函數跳轉與返回。
04—

函數跳轉與返回

程序執行時 CPU用特定寄存器來存儲運行時棧基與棧指針,同時也有指令指針寄存器用于存儲下一條要執行的指令地址。

......棧寄存器BPSPIP棧基棧指針指令指針指令push 3push 4

如果接下來要執行"push 3"這條指令,CPU讀取后,會將指令指針移向下一條指令,然后棧指針向下移動,數字3入棧。

......棧寄存器BPSPIP棧基棧指針指令指針指令push 3push 43

繼續執行下一條指令,再次移動棧指針入棧數字4。

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

前面我們提過Go語言中函數棧幀不是這樣逐步擴張的,而是一次性分配,也就是在分配棧幀時,直接將棧指針移動到所需最大棧空間的位置。

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

然后通過棧指針加上偏移值這種相對尋址方式使用函數棧幀。例如sp加16字節處存儲3,加8字節處存儲4,諸如此類。

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

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

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

圖:call指令執行前
然后到call指令這里,它的作用有兩點:第一,把返回地址a2入棧保存起來;第二,跳轉到指令地址b1處。

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

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

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

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

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

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

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

圖:ret指令執行后
現在可以從a2這里繼續執行了。簡單來說,函數通過call指令實現跳轉,而每個函數開始時會分配棧幀,結束前又釋放自己的棧幀,ret指令又會把?;謴偷絚all之前的樣子,通過這些指令的配合最終實現了函數跳轉與返回。

聲明:免責聲明:本文內容由互聯網用戶自發貢獻自行上傳,本網站不擁有所有權,也不承認相關法律責任。如果您發現本社區中有涉嫌抄襲的內容,請發

送郵件至:operations@xinnet.com進行舉報,并提供相關證據,一經查實,本站將立刻刪除涉嫌侵權內容。本站原創內容未經允許不得轉載,或轉載時

需注明出處:新網idc知識百科

免費咨詢獲取折扣

Loading
主站蜘蛛池模板: 国产成人综合亚洲欧美日韩| 在线播放五十路熟妇| 国产日韩精品视频无码| 久久精品高清一区二区三区| 久久精品国产99久久丝袜| 午夜影视啪啪免费体验区| 日本熟妇乱人伦a片免费高清| 国产亚洲日韩a欧美在线人成| 国产亚洲欧美另类一区二区三区 | 999久久国产精品免费人妻| 成人免费午夜无码视频| 九色精品国产成人综合网站| 成人无码嫩草影院| 国产性色av高清在线观看| 99国产精品欧美一区二区三区| 国产成人亚洲综合无码99| 99re66在线观看精品免费| 欧美人与动牲交免费观看| 少妇被粗大的猛烈进出动视频| 久久国国产免费999| 精品一卡2卡三卡4卡乱码精品视频| 亚洲精品熟女国产| 无遮掩无码h成人av动漫| 国产片av不卡在线观看国语| 精品三级久久久久电影网| 日韩亚洲国产高清免费视频| 午夜福利午夜福利1000| 中文字幕av无码专区第一页| 亚洲欧美在线一区中文字幕 | 欧美videos另类极品| 天堂va欧美va亚洲va好看va| 自拍 亚洲 欧美 卡通 另类| 亚洲日韩欧美在线成人| 欧美成人va免费大片视频| 国产精品普通话国语对白露脸| 日本高清色倩视频在线观看 | 人人爽人人模人人人爽人人爱| 天天摸夜夜添狠狠添高潮出水 | 色五月丁香五月综合五月 | 天天澡夜夜澡狠狠久久| 秋霞最新高清无码鲁丝片|