深入Go的底層,帶你走近一群有追求的人


上周六晚上,我參加了“Go夜讀”活動,這期主要講Go匯編語言,由滴滴大佬曹春暉大神主講。活動結束后,我感覺打通了任督二脈。活動從晚上9點到深夜11點多,全程深度參與,大呼過癮,以至於活動結束之后,久久不能平靜。

可以說理解了Go匯編語言,就可以讓我們對Go的理解上一個台階,很多以前模棱的東西,在匯編語言面前都無所遁形了。我在活動上收獲了很多,今天我來作一個總結,希望給大家帶來啟發!

緣起

幾周前我寫了一篇關於defer的文章:《Golang之如何輕松化解defer的溫柔陷阱》。這篇文章發出后不久就被GoCN的每日新聞收錄了,然后就被Go夜讀群的大佬楊文看到了,之后被邀請去夜讀活動分享。

正式分享前,我又主題閱讀了很多文章,以求把defer講清楚。閱讀過程中,我發現但凡深入一點的文章,都會拋出Go匯編語言。於是就去搜索資料,無奈相關的資料太少,看得雲里霧里,最后到了真正要分享的時候也沒有完全弄清楚。

夜讀活動結束之后,楊大發布了由春暉大神帶來的夜讀分享預告:《plan9 匯編入門,帶你打通應用和底層》。我得知這個消息后,非常激動!終於有牛人可以講講Go匯編語言了,聽完之后估計會有很大提升,也能搞懂defer的底層原理了!

接着,我發現,春暉大神竟然和我在同一個公司!我在公司內網上搜到了他寫的plan9匯編相關文章,發布到Go夜讀的github上。我提前花時間預習完了文章,整理出了遇到的問題。

周六晚上9點准時開講,曹大的准備很充分!原來1個小時的時間被拉長到了2個多小時,而曹大精力和反應一直很迅速,問的問題很快就能得到回答。我全程和曹大直接對話,感覺簡直不要太爽!

這篇文章既是對這次夜讀的總結,也是為了宣傳一下Go夜讀活動。那里是一群有追求的人,他們每周都會聚在一起,通過網絡,探討Go語言的方方面面。我相信,參與的人都會有很多不同的收獲。

我直接參與的Go夜讀活動有三期,一期分享,兩期聽講,每次都有很多的收獲。

自我介紹的技巧

很多人都不知道怎么做好一個自我介紹,要么含糊其辭,介紹完大家都不知道你講了什么;要么說了半天無效的信息,大家並不關心的事情,搞得很尷尬。 其實自我介紹沒那么難,掌握套路后,是可以做得很好的!

我在上上期Go夜讀分享的時候,用一張PPT完成了自我介紹。包含了四個方面:個人基本信息出現在此時此地的原因我能帶來的幫助我希望得到的幫助

個人基本信息包括你叫什么名字,是哪里人,在什么地方工作,畢業於哪個學校,有什么興趣愛好……這些基本的屬性。這些信息可以讓大家快速形成對你的直觀認識。

出現在此時此地的原因,可以講解你的故事。你在什么地方通過什么人知道了這個活動,然后因為什么打動你來參加……通過故事可以迅速拉近與現場其他參與者的距離。

我能帶來的幫助,參加活動的人都是想獲取一些東西的:知識、經驗、見聞等等。但是,我們不能只索取,不付出。因此,可以講講你可以提供的幫助。比如我可以聯系場地,我會寫宣傳文章等等,你可以講出你獨特的價值。

我希望得到的幫助。每個參與的人都希望從活動中獲得自己想要的東西,正是因為此,這個活動對於參與者才有意義,也才會持續下去的動力。

這四個方面,可以組成一個非常精彩的自我介紹。它最早是我在聽羅胖的《羅輯思維》聽到的,我把它寫進了我的人生算法里,今天推薦給大家。希望大家以后在需要自我介紹的場合有話可說,而且能說的精彩。

自我介紹

硬核知識點

什么是plan9匯編

我們知道,CPU是只認二進制指令的,也就是一串的0101;人類無法記住這些二進制碼,於是發明了匯編語言。匯編語言實際上是二進制指令的文本形式,它與指令可以一一對應。

每一種CPU指令都是不一樣的,因此對應的匯編語言也就不一樣。人類寫完匯編語言后,把它轉換成二進制碼,就可以被機器執行了。轉換的動作由編譯器完成。

Go語言的編譯器和匯編器都帶了一個-S參數,可以查看生成的最終目標代碼。通過對比目標代碼和原始的Go語言或Go匯編語言代碼的差異可以加深對底層實現的理解。

Go匯編語言實際上來源於plan9匯編語言,而plan9匯編語言最初來源於Go語言作者之一的Ken Thompson為plan9系統所寫的C語言編譯器輸出的匯編偽代碼。這里強烈推薦一下春暉大神的新書《Go語言高級編程》,即將上市,電子版的點擊閱讀原文可以看到地址,書中有一整個章節講Go的匯編語言,非常精彩!

理解Go的匯編語言,哪怕只是一點點,都能對Go的運行機制有更深入的理解。比如我們以前講的defer,如果從Go源碼編譯后的匯編代碼來看,就能深刻地掌握它的底層原理。再比如,很多文章都會分析Go的函數參數傳遞都是值傳遞,如果把匯編代碼秀出來,很容易就能得出結論。

匯編角度看函數調用及返回過程

假設我們有一個這樣年幼無知的例子,求兩個int的和,Go源碼如下:

package main

func main() {
	_ = add(3,5)
}

func add(a, b int) int {
	return a+b
}

使用如下命令得到匯編代碼:

go tool compile -S main.go

go tool compile命令用於調用Go語言提供的底層命令工具,其中-S參數表示輸出匯編格式。

我們現在只關心add函數的匯編代碼:

"".add STEXT nosplit size=19 args=0x18 locals=0x0
        0x0000 00000 (main.go:7)        TEXT    "".add(SB), NOSPLIT, $0-24
        0x0000 00000 (main.go:7)        FUNCDATA        $0, gclocals·54241e171da8af6ae173d69da0236748(SB)
        0x0000 00000 (main.go:7)        FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x0000 00000 (main.go:7)        MOVQ    "".b+16(SP), AX
        0x0005 00005 (main.go:7)        MOVQ    "".a+8(SP), CX
        0x000a 00010 (main.go:8)        ADDQ    CX, AX
        0x000d 00013 (main.go:8)        MOVQ    AX, "".~r2+24(SP)
        0x0012 00018 (main.go:8)        RET

看不懂沒關系,我目前也不是全部都懂,但是對於理解一個函數調用的整體過程而言,足夠了。

0x0000 00000 (main.go:7)        TEXT    "".add(SB), NOSPLIT, $0-24

這一行表示定義add這個函數,最后的數字$0-24,其中0表示函數棧幀大小為0;24表示參數及返回值的大小:參數是2個int型變量,返回值是1個int型變量,共24字節。

再看中間這四行:

        0x0000 00000 (main.go:7)        MOVQ    "".b+16(SP), AX
        0x0005 00005 (main.go:7)        MOVQ    "".a+8(SP), CX
        0x000a 00010 (main.go:8)        ADDQ    CX, AX
        0x000d 00013 (main.go:8)        MOVQ    AX, "".~r2+24(SP)

代碼片段中的第1行,將第2個參數b搬到AX寄存器;第2行將1個參數a搬到寄存器CX;第3行將ab相加,相加的結果搬到AX;最后一行,將結果搬到返回參數的地址,這段匯編代碼非常簡單,來看一下函數調用者和被調者的棧幀圖:

(SP)指棧頂,b+16(SP)表示參數1的位置,從SP往上增加16個字節,注意,前面的b僅表示一個標號;同樣,a+8(SP)表示實參0;~r2+24(SP)則表示返回值的位置。

具體可以看下面的圖:

add函數棧幀

上面add函數的棧幀大小為0,其實更一般的調用者與被調用者的棧幀示意圖如下:

棧幀

最后,執行RET指令。這一步把被調用函數add棧幀清零,接着,彈出棧頂的返回地址,把它賦給指令寄存器rip,而返回地址就是main函數里調用add函數的下一行。

於是,又回到了main函數的執行環境,add函數的棧幀也被銷毀了。但是注意,這塊內存是沒有被清零的,清零動作是之后再次申請這塊內存的時候要做的事。比如,聲明了一個int型變量,它的默認值是0,清零的動作是在這里完成的。

這樣,main函數完成了函數調用,也拿到了返回值,完美。

匯編角度看slice

再來看一個例子,我們來看看slice的底層到底是什么。

package main

func main() {
	s := make([]int, 3, 10)
	_ = f(s)
}

func f(s []int) int {
	return s[1]
}

用上面同樣的命令得到匯編代碼,我們只關注f函數的匯編代碼:

"".f STEXT nosplit size=53 args=0x20 locals=0x8
        // 棧幀大小為8字節,參數和返回值為32字節
        0x0000 00000 (main.go:8)        TEXT    "".f(SB), NOSPLIT, $8-32
        // SP棧頂指針下移8字節
        0x0000 00000 (main.go:8)        SUBQ    $8, SP
        // 將BP寄存器的值入棧
        0x0004 00004 (main.go:8)        MOVQ    BP, (SP)
        // 將新的棧頂地址保存到BP寄存器
        0x0008 00008 (main.go:8)        LEAQ    (SP), BP
        0x000c 00012 (main.go:8)        FUNCDATA        $0, gclocals·4032f753396f2012ad1784f398b170f4(SB)
        0x000c 00012 (main.go:8)        FUNCDATA        $1, gclocals·69c1753bd5f81501d95132d08af04464(SB)
        // 取出slice的長度len
        0x000c 00012 (main.go:8)        MOVQ    "".s+24(SP), AX
        // 比較索引1是否超過len
        0x0011 00017 (main.go:9)        CMPQ    AX, $1
        // 如果超過len,越界了。跳轉到46
        0x0015 00021 (main.go:9)        JLS     46
        // 將slice的數據首地址加載到AX寄存器
        0x0017 00023 (main.go:9)        MOVQ    "".s+16(SP), AX
        // 將第8byte地址的元素保存到AX寄存器,也就是salaries[1]
        0x001c 00028 (main.go:9)        MOVQ    8(AX), AX
        // 將結果拷貝到返回參數的位置(y)
        0x0020 00032 (main.go:9)        MOVQ    AX, "".~r1+40(SP)
        // 恢復BP的值
        0x0025 00037 (main.go:9)        MOVQ    (SP), BP
        // SP向上移動8個字節
        0x0029 00041 (main.go:9)        ADDQ    $8, SP
        // 返回
        0x002d 00045 (main.go:9)        RET
        0x002e 00046 (main.go:9)        PCDATA  $0, $1
        // 越界,panic
        0x002e 00046 (main.go:9)        CALL    runtime.panicindex(SB)
        0x0033 00051 (main.go:9)        UNDEF
        0x0000 48 83 ec 08 48 89 2c 24 48 8d 2c 24 48 8b 44 24  H...H.,$H.,$H.D$
        0x0010 18 48 83 f8 01 76 17 48 8b 44 24 10 48 8b 40 08  .H...v.H.D$.H.@.
        0x0020 48 89 44 24 28 48 8b 2c 24 48 83 c4 08 c3 e8 00  H.D$(H.,$H......
        0x0030 00 00 00 0f 0b                                   .....
        rel 47+4 t=8 runtime.panicindex+0

通過上面的匯編代碼,我們畫出函數調用的棧幀圖:

f函數棧幀

我們可以清晰地看到,一個slice本質上是用一個數據首地址,一個長度Len,一個容量Cap。所以在參數是slice的函數里,對slice的操作會影響到實參的slice。

正確參與Go夜讀活動的方式

最后再說一下Go夜讀活動的方式和目標。引自Go夜讀的github說明文件:

由一個主講人帶着大家一起去閱讀 Go 源代碼,一起去啃那些難啃的算法、學習代碼里面的奇淫技巧,遇到問題或者有疑惑了,我們可以一起去檢索,解答這些問題。我們可以一起學習,共同成長。

我們希望可以推進大家深入了解 Go ,快速成長為資深的 Gopher 。我們希望每次來了的人和沒來的人都能夠有收獲,成長。

前面我說Go夜讀活動的小伙伴是一群有追求的人,這里我也指出一些問題吧。就我參與的三期來看,雖然zoom接入人數很多,高峰期50+人,但是全過程大家交流比較少,基本上是主講人一個人在那自嗨。春暉大佬講的那期,只有我全程提問。感覺像是我們兩個人在對話,我的問題弄清楚了,只是不知道其他的參與同學如何?

我再給分享者和參與者提一些建議吧:

對於分享者,事先做好充足的准備,可以在文章里列出主要的點,放在github里,參考春暉大佬的plan9匯編講義;最重要的一點,分享前給大家提供一份預習資料。

對於參與者,能獲得最多收獲的方式就是會前預習,會中積極提問,會后復習總結發散。另外,強烈建議參與者會前要准備至少一個問題,有針對性地聽,才會有收獲。會中也要積極提問,這也是對主講者的反饋,不至於主講者覺得只有自己在對着電腦講。

最后,歡迎每一個學習Go語言的同學都能來Go夜讀看看!點擊閱讀原文可以看到文章里提到的所有資料,包括上期曹大plan9匯編的視頻回放,不容錯過!

QR

閱讀原文

夜讀地址
《plan9 匯編入門,帶你打通應用和底層》講義
《plan9 匯編入門,帶你打通應用和底層》視頻地址
曹大的Go高級編程書,紙質書即將出版
曹大go源碼閱讀
曹大博客


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM