上週六晚上,我參加了「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完成了自我介紹。包含了四個方面:我的基本信息
、出如今此時此地的緣由
、我能帶來的幫助
、我但願獲得的幫助
。
我的基本信息
包括你叫什麼名字,是哪裏人,在什麼地方工做,畢業於哪一個學校,有什麼興趣愛好……這些基本的屬性。這些信息可讓你們快速造成對你的直觀認識。
出如今此時此地的緣由
,能夠講解你的故事。你在什麼地方經過什麼人知道了這個活動,而後由於什麼打動你來參加……經過故事能夠迅速拉近與現場其餘參與者的距離。
我能帶來的幫助
,參加活動的人都是想獲取一些東西的:知識、經驗、見聞等等。可是,咱們不能只索取,不付出。所以,能夠講講你能夠提供的幫助。好比我能夠聯繫場地,我會寫宣傳文章等等,你能夠講出你獨特的價值。
我但願獲得的幫助
。每一個參與的人都但願從活動中得到本身想要的東西,正是由於此,這個活動對於參與者纔有意義,也纔會持續下去的動力。
這四個方面,能夠組成一個很是精彩的自我介紹。它最先是我在聽羅胖的《羅輯思惟》聽到的,我把它寫進了個人人生算法
裏,今天推薦給你們。但願你們之後在須要自我介紹的場合有話可說,並且能說的精彩。
咱們知道,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
的底層究竟是什麼。
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夜讀的github說明文件:
由一個主講人帶着你們一塊兒去閱讀 Go 源代碼,一塊兒去啃那些難啃的算法、學習代碼裏面的奇淫技巧,遇到問題或者有疑惑了,咱們能夠一塊兒去檢索,解答這些問題。咱們能夠一塊兒學習,共同成長。
咱們但願能夠推動你們深刻了解 Go ,快速成長爲資深的 Gopher 。咱們但願每次來了的人和沒來的人都可以有收穫,成長。
前面我說Go夜讀活動的小夥伴是一羣有追求的人,這裏我也指出一些問題吧。就我參與的三期來看,雖然zoom接入人數不少,高峯期50+人,可是全過程你們交流比較少,基本上是主講人一我的在那自嗨。春暉大佬講的那期,只有我全程提問。感受像是咱們兩我的在對話,個人問題弄清楚了,只是不知道其餘的參與同窗如何?
我再給分享者和參與者提一些建議吧:
對於分享者,事先作好充足的準備,能夠在文章裏列出主要的點,放在github裏,參考春暉大佬的plan9彙編講義;最重要的一點,分享前給你們提供一份預習資料。
對於參與者,能得到最多收穫的方式就是會前預習,會中積極提問,會後複習總結髮散。另外,強烈建議參與者會前要準備至少一個問題,有針對性地聽,纔會有收穫。會中也要積極提問,這也是對主講者的反饋,不至於主講者以爲只有本身在對着電腦講。
最後,歡迎每個學習Go語言的同窗都能來Go夜讀看看!點擊閱讀原文能夠看到文章裏提到的全部資料,包括上期曹大plan9彙編的視頻回放,不容錯過!
夜讀地址
《plan9 彙編入門,帶你打通應用和底層》講義
《plan9 彙編入門,帶你打通應用和底層》視頻地址
曹大的Go高級編程書,紙質書即將出版
曹大go源碼閱讀
曹大博客