深刻Go的底層,帶你走近一羣有追求的人

上週六晚上,我參加了「Go夜讀」活動,這期主要講Go彙編語言,由滴滴大佬曹春暉大神主講。活動結束後,我感受打通了任督二脈。活動從晚上9點到深夜11點多,全程深度參與,大呼過癮,以致於活動結束以後,久久不能平靜。git

能夠說理解了Go彙編語言,就可讓咱們對Go的理解上一個臺階,不少之前模棱的東西,在彙編語言面前都無所遁形了。我在活動上收穫了不少,今天我來做一個總結,但願給你們帶來啓發!github

緣起

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

正式分享前,我又主題閱讀了不少文章,以求把defer講清楚。閱讀過程當中,我發現但凡深刻一點的文章,都會拋出Go彙編語言。因而就去搜索資料,無奈相關的資料太少,看得雲裏霧裏,最後到了真正要分享的時候也沒有徹底弄清楚。算法

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

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

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

這篇文章既是對此次夜讀的總結,也是爲了宣傳一下Go夜讀活動。那裏是一羣有追求的人,他們每週都會聚在一塊兒,經過網絡,探討Go語言的方方面面。我相信,參與的人都會有不少不一樣的收穫。ide

我直接參與的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源碼閱讀
曹大博客

相關文章
相關標籤/搜索