上周六晚上,我參加了“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行將a
和b
相加,相加的結果搬到AX
;最后一行,將結果搬到返回參數的地址,這段匯編代碼非常簡單,來看一下函數調用者和被調者的棧幀圖:
(SP)指棧頂,b+16(SP)表示參數1的位置,從SP往上增加16個字節,注意,前面的b僅表示一個標號;同樣,a+8(SP)表示實參0;~r2+24(SP)則表示返回值的位置。
具體可以看下面的圖:
上面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
通過上面的匯編代碼,我們畫出函數調用的棧幀圖:
我們可以清晰地看到,一個slice本質上是用一個數據首地址,一個長度Len,一個容量Cap。所以在參數是slice的函數里,對slice的操作會影響到實參的slice。
正確參與Go夜讀活動的方式
最后再說一下Go夜讀活動的方式和目標。引自Go夜讀的github說明文件:
由一個主講人帶着大家一起去閱讀 Go 源代碼,一起去啃那些難啃的算法、學習代碼里面的奇淫技巧,遇到問題或者有疑惑了,我們可以一起去檢索,解答這些問題。我們可以一起學習,共同成長。
我們希望可以推進大家深入了解 Go ,快速成長為資深的 Gopher 。我們希望每次來了的人和沒來的人都能夠有收獲,成長。
前面我說Go夜讀活動的小伙伴是一群有追求的人,這里我也指出一些問題吧。就我參與的三期來看,雖然zoom接入人數很多,高峰期50+人,但是全過程大家交流比較少,基本上是主講人一個人在那自嗨。春暉大佬講的那期,只有我全程提問。感覺像是我們兩個人在對話,我的問題弄清楚了,只是不知道其他的參與同學如何?
我再給分享者和參與者提一些建議吧:
對於分享者,事先做好充足的准備,可以在文章里列出主要的點,放在github里,參考春暉大佬的plan9匯編講義;最重要的一點,分享前給大家提供一份預習資料。
對於參與者,能獲得最多收獲的方式就是會前預習,會中積極提問,會后復習總結發散。另外,強烈建議參與者會前要准備至少一個問題,有針對性地聽,才會有收獲。會中也要積極提問,這也是對主講者的反饋,不至於主講者覺得只有自己在對着電腦講。
最后,歡迎每一個學習Go語言的同學都能來Go夜讀看看!點擊閱讀原文可以看到文章里提到的所有資料,包括上期曹大plan9匯編的視頻回放,不容錯過!
閱讀原文
夜讀地址
《plan9 匯編入門,帶你打通應用和底層》講義
《plan9 匯編入門,帶你打通應用和底層》視頻地址
曹大的Go高級編程書,紙質書即將出版
曹大go源碼閱讀
曹大博客