日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

了解Go第一步:Go与Plan 9汇编语言

發布時間:2024/3/26 编程问答 49 豆豆
生活随笔 收集整理的這篇文章主要介紹了 了解Go第一步:Go与Plan 9汇编语言 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

本文快速閱讀需要一定的匯編、Go、編譯原理基礎
因水平極其有限,錯誤難以避免,歡迎批評指正

1. Go與Plan 9
  • 一圖勝千言:
  • 網傳,開發Go的一些重要人物也是Plan 9項目的重要人物,所以Go匯編和一些工具鏈是Plan 9項目搬過來的。因為這個匯編獨立與所有的CPU架構和操作系統(獨立于操作系統,其實生成的匯編已經要使用寄存器了,每個架構寄存器情況不同)。所以Go項目需要為具體架構和操作系統生成目標機器代碼。所以我們甚至可以把Go匯編理解成Go的一種IR。
  • Go匯編學習資料:
    • 官網
    • 《Go語言高級編程》第三章
  • 網上大部分書籍和資料的匯編停留在1.17以前的版本,但是1.17開始(最新的1.18支持更多架構)函數調用有了新ABI規范。所以如果我們的Go版本比較新,那么可能生成的匯編和網上各種教程里的不太一樣。其實也沒有關系,沒有太大區別。本文的匯編是基于Go1.17生成的。
2. 一段相對簡單的Go代碼學習Go匯編
  • 前置知識:簡單強調一下本文閱讀預備知識中的一些知識點
    • 編譯原理:一個程序編譯的過程為詞法分析,語法分析,語義分析,中間代碼生成,代碼分析和優化,目標代碼生成。對于其它語言的編譯器后端,生成的目標代碼一般就是對應平臺的匯編代碼。再由對應匯編器處理。而對于Go,可以認為生成的目標代碼在任何時候都是Plan 9匯編(屏蔽了操作系統帶來的差異,如系統調用規范,而CPU帶給Go匯編的主要差異就是寄存器數量和名字)。之后會再根據架構和操作系統翻譯成對應的機器代碼,所以也有人稱Go在這個層面是平臺無關性的。
    • 匯編基礎:這里說一下調用約定,我們程序員一般研究的對象是Linux/x86-64,其調用約定為函數參數只有6個能放在寄存器中,多于6個需要放入棧中。返回地址也在寄存器中。而Go1.17之前,Go調用約定是返回值和調用參數都存放在棧中。現在最新版本的函數調用參數是使用寄存器的,帶來了性能的提升。
      再說一下程序運行時候的內存布局,棧內存在內存中是由高地址向低地址延伸的,所以每個棧幀的棧低地址大于棧頂。
  • Go匯編與主流匯編較大區別介紹
    • 4個偽寄存器PC、FP、SP、SB。我們需要重點關注的是FP與SP。特別是SP也是部分架構中的實寄存器。以下內容如無特別表述,SP即表示偽SP。
      • FP:可以認為是當前棧幀的棧底(不包括參數返回值),當有寄存器放不下的調用參數或者有返回值時。這些對象的尋址會用到FP,且為正偏移(參數在FP高地址方向存儲)。
      • SP:一定要注意區分真偽SP寄存器。偽SP也可以認為是棧底(不包括參數返回值),而真SP認為是棧頂。一般局部變量的尋址會使用偽SP。且為負偏移。偽寄存器一般需要一個標識符和偏移量為前綴,如果沒有標識符前綴則是真寄存器。比如(SP)、+8(SP)沒有標識符前綴為真SP寄存器,而a(SP)、b+8(SP)有標識符為前綴表示偽寄存器。
      • 一般一個函數的棧幀可以認為是真偽SP所指地址中間部分。上面的表述中,可能有人認為FP和SP一定是在一起的,但是由于返回地址等內存需求和內存對齊等原因,不是一起的。
    • Go匯編的調用約定中,所有信息都是由調用者保護的,所以可以看出,每個函數棧幀中包含了調用別的函數的參數和返回值空間。
3. Go匯編閱讀
  • 閱讀Go匯編常用的命令為go tool compile -N -l -S 。-N代表不優化,不然Go匯編和我們想象的可能大不一樣,-l為不內聯,-S為打印匯編信息。還有其它命令也可以使用。在線網站gossa可以實時查看某個函數的匯編代碼
  • 源代碼:
package mainfunc main() {var a int64 = 10var b int64 = 20a += sum(a, b) }func sum(a int64, b int64) int64 {return a + b }
  • Go匯編及解讀:每行#開頭的代碼解釋下一行匯編含義
    • 函數定義:TEXT 函數名(SB), [flags,] $棧大小[-參數及返回值大小]。再次注意,函數自己的參數及返回值不在自己的棧幀中。而自己棧幀大小包括調用別的函數的返回值及參數。flags一般很多,遇到時搜索一下啥意思

    • FUNCDATA和PCDATA:記錄了函數中指針信息和調用信息等,panic時的調用情況及垃圾回收時的根對象都分別依賴它們。它們是編譯器自行插入的,閱讀時可以跳過

    • 使用go tool compile -S / go tool objdump命令輸出的匯編來說,所有的 SP 都是真SP即SP寄存器中的地址。所以從下面匯編(使用go tool compile -S -N -l)可以看出沒有負索引取值

    • a+24(SP)和40(SP):前者代表a的起始地址在SP上方24字節位置。后者代表的地址為SP上方40字節處。

"".main STEXT size=88 args=0x0 locals=0x30 funcid=0x0# main函數,ABIInternal代表使用了新的ABI,即不是所有參數都在棧中了,main函數棧幀占48字節0x0000 00000 (main.go:3) TEXT "".main(SB), ABIInternal, $48-0# 48可以計算出來,看完后再來理解一下:48 = 局部變量a,b sum參數及返回地址 上一個棧幀BP 一共6個8B即48# 下面這幾行是判斷棧空間是否足夠。不夠進行棧擴容。同樣的,GC時可以進行棧縮減0x0000 00000 (main.go:3) CMPQ SP, 16(R14)0x0004 00004 (main.go:3) PCDATA $0, $-20x0004 00004 (main.go:3) JLS 810x0006 00006 (main.go:3) PCDATA $0, $-1# SP(棧頂)減少48,即為當前棧幀分配48字節。我們讀代碼時可以對稱讀,下面必定有個命令是加480x0006 00006 (main.go:3) SUBQ $48, SP# 先保存上一個棧幀的棧底(上一棧幀的起始)0x000a 00010 (main.go:3) MOVQ BP, 40(SP)# BP移動到新的棧幀棧底。我們可以發現,其實沒有使用FP,如果有FP的話FP的值會為48(SP)。沒有FP原因上面也說了。我們需要注意的是不是任何時候FP和偽SP/BP的位置間隔都是一樣的。0x000f 00015 (main.go:3) LEAQ 40(SP), BP0x0014 00020 (main.go:3) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)0x0014 00020 (main.go:3) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)# 可以看出就算沒有優化,也是沒有定義再賦值,而是直接給a賦值100x0014 00020 (main.go:4) MOVQ $10, "".a+24(SP)# 給b賦值200x001d 00029 (main.go:5) MOVQ $20, "".b+16(SP)# sum參數之一放到AX寄存器中0x0026 00038 (main.go:6) MOVQ "".a+24(SP), AX# 第二個參數放到BX寄存器中0x002b 00043 (main.go:6) MOVL $20, BX0x0030 00048 (main.go:6) PCDATA $1, $0# 調用sum函數。此時我們發現b下面還有16字節,其實是sum的調用參數0x0030 00048 (main.go:6) CALL "".sum(SB)# 返回結果存在寄存器AX中,這里存到棧中,可見在局部變量a上面0x0035 00053 (main.go:6) MOVQ AX, ""..autotmp_2+32(SP)# a值存在CX0x003a 00058 (main.go:6) MOVQ "".a+24(SP), CX# a與結果相加0x003f 00063 (main.go:6) ADDQ AX, CX# 相加結果賦值給a0x0042 00066 (main.go:6) MOVQ CX, "".a+24(SP)# BP變成上一個棧幀的棧底0x0047 00071 (main.go:7) MOVQ 40(SP), BP# 函數調用完成之前,SP回歸上一棧幀棧頂0x004c 00076 (main.go:7) ADDQ $48, SP# 返回,0x0050 00080 (main.go:7) RET# 下面這幾行對應上面棧擴容的跳轉行。可以看見,棧擴容后又跳轉回去重新判斷棧是否有爆棧可能性0x0051 00081 (main.go:7) NOP0x0051 00081 (main.go:3) PCDATA $1, $-10x0051 00081 (main.go:3) PCDATA $0, $-20x0051 00081 (main.go:3) CALL runtime.morestack_noctxt(SB)0x0056 00086 (main.go:3) PCDATA $0, $-10x0056 00086 (main.go:3) JMP 00x0000 49 3b 66 10 76 4b 48 83 ec 30 48 89 6c 24 28 48 I;f.vKH..0H.l$(H0x0010 8d 6c 24 28 48 c7 44 24 18 0a 00 00 00 48 c7 44 .l$(H.D$.....H.D0x0020 24 10 14 00 00 00 48 8b 44 24 18 bb 14 00 00 00 $.....H.D$......0x0030 e8 00 00 00 00 48 89 44 24 20 48 8b 4c 24 18 48 .....H.D$ H.L$.H0x0040 01 c1 48 89 4c 24 18 48 8b 6c 24 28 48 83 c4 30 ..H.L$.H.l$(H..00x0050 c3 e8 00 00 00 00 eb a8 ........rel 49+4 t=7 "".sum+0rel 82+4 t=7 runtime.morestack_noctxt+0 "".sum STEXT nosplit size=56 args=0x10 locals=0x10 funcid=0x0# 可見sum的棧幀大小為16B,參數大小為16B,存在上一個棧幀0x0000 00000 (main.go:9) TEXT "".sum(SB), NOSPLIT|ABIInternal, $16-16# sum函數有NOSPLIT修飾,所以沒有棧擴容階段0x0000 00000 (main.go:9) SUBQ $16, SP0x0004 00004 (main.go:9) MOVQ BP, 8(SP)0x0009 00009 (main.go:9) LEAQ 8(SP), BP0x000e 00014 (main.go:9) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)0x000e 00014 (main.go:9) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)0x000e 00014 (main.go:9) FUNCDATA $5, "".sum.arginfo1(SB)# 這里注意一下,這里是把main的局部變量a存在AX寄存器中的值移動到了sum的參數a中。# 而sum的參數a存在main棧幀中,所以可以看出加24。# 一個偏移24一個偏移32。不是16和24的原因是,CALL和RET會進行隱式的PC/IP寄存器的值存儲0x000e 00014 (main.go:9) MOVQ AX, "".a+24(SP)0x0013 00019 (main.go:9) MOVQ BX, "".b+32(SP)# 這個應該是return a + b變成了 r2 = a + b; return r2。先把r2區域置00x0018 00024 (main.go:9) MOVQ $0, "".~r2(SP)# 加法0x0020 00032 (main.go:10) MOVQ "".a+24(SP), AX0x0025 00037 (main.go:10) ADDQ "".b+32(SP), AX0x002a 00042 (main.go:10) MOVQ AX, "".~r2(SP)0x002e 00046 (main.go:10) MOVQ 8(SP), BP0x0033 00051 (main.go:10) ADDQ $16, SP0x0037 00055 (main.go:10) RET0x0000 48 83 ec 10 48 89 6c 24 08 48 8d 6c 24 08 48 89 H...H.l$.H.l$.H.0x0010 44 24 18 48 89 5c 24 20 48 c7 04 24 00 00 00 00 D$.H.\$ H..$....0x0020 48 8b 44 24 18 48 03 44 24 20 48 89 04 24 48 8b H.D$.H.D$ H..$H.0x0030 6c 24 08 48 83 c4 10 c3 l$.H.... go.cuinfo.packagename. SDWARFCUINFO dupok size=00x0000 6d 61 69 6e main ""..inittask SNOPTRDATA size=240x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................0x0010 00 00 00 00 00 00 00 00 ........ gclocals·33cdeccccebe80329f1fdbee7f5874cb SRODATA dupok size=80x0000 01 00 00 00 00 00 00 00 ........ "".sum.arginfo1 SRODATA static dupok size=50x0000 00 08 08 08 ff .....
  • 可能你看了上面的匯編有疑問,不是說1.17開始一些架構ABI改變了嗎。為什么還是有寄存器和棧空間中的來回復制。因為上面是加了不優化參數的匯編。當我們去掉-N。就可以看到。sum的棧幀占用內存為0。main棧幀空間也大大縮小(連局部變量a , b都不占用空間了)
  • 個人覺得如果看上面的Go匯編沒什么阻礙,Go匯編就可以先學到這了,當我們真要到匯編層面找Bug或提升性能時。看不懂再邊學邊做就行。上來就學習完Go匯編所有細節,這個付出回報比相對于一般人來說是有點低的
4. 最后我來繪制一下上面匯編代碼中棧內存的情況
------ celler BP (8 bytes) ------ main函數棧幀 BP sum.ret (8 bytes) ------ main.a (8 bytes) ------ main.b (8 bytes) ------ sum.b (8 bytes) ------ sum.a (8 bytes) ------ main函數棧幀 SP ret addr (8 bytes) ------ caller(main) BP (8 bytes) ------ sum函數棧幀 BP 臨時變量 (8 bytes) ------ sum函數棧幀 SP

總結

以上是生活随笔為你收集整理的了解Go第一步:Go与Plan 9汇编语言的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。