生活随笔
收集整理的這篇文章主要介紹了
了解Go第一步:Go与Plan 9汇编语言
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
本文快速閱讀需要一定的匯編、Go、編譯原理基礎
因水平極其有限,錯誤難以避免,歡迎批評指正
1. Go與Plan 9
- 一圖勝千言:
- 網傳,開發Go的一些重要人物也是Plan 9項目的重要人物,所以Go匯編和一些工具鏈是Plan 9項目搬過來的。因為這個匯編獨立與所有的CPU架構和操作系統(獨立于操作系統,其實生成的匯編已經要使用寄存器了,每個架構寄存器情況不同)。所以Go項目需要為具體架構和操作系統生成目標機器代碼。所以我們甚至可以把Go匯編理解成Go的一種IR。
- 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 main
func 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
=0x00x0000 00000
(main.go:3
) TEXT
"".main
(SB
), ABIInternal,
$48-00x0000 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, $-10x0006 00006
(main.go:3
) SUBQ
$48, SP0x000a 00010
(main.go:3
) MOVQ BP, 40
(SP
)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
)0x0014 00020
(main.go:4
) MOVQ
$10,
"".a+24
(SP
)0x001d 00029
(main.go:5
) MOVQ
$20,
"".b+16
(SP
)0x0026 00038
(main.go:6
) MOVQ
"".a+24
(SP
), AX0x002b 00043
(main.go:6
) MOVL
$20, BX0x0030 00048
(main.go:6
) PCDATA
$1,
$00x0030 00048
(main.go:6
) CALL
"".sum
(SB
)0x0035 00053
(main.go:6
) MOVQ AX,
""..autotmp_2+32
(SP
)0x003a 00058
(main.go:6
) MOVQ
"".a+24
(SP
), CX0x003f 00063
(main.go:6
) ADDQ AX, CX0x0042 00066
(main.go:6
) MOVQ CX,
"".a+24
(SP
)0x0047 00071
(main.go:7
) MOVQ 40
(SP
), BP0x004c 00076
(main.go:7
) ADDQ
$48, SP0x0050 00080
(main.go:7
) RET0x0051 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-160x0000 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
)0x000e 00014
(main.go:9
) MOVQ AX,
"".a+24
(SP
)0x0013 00019
(main.go:9
) MOVQ BX,
"".b+32
(SP
)0x0018 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汇编语言的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。