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

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) >

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

發(fā)布時(shí)間:2024/3/26 37 豆豆
生活随笔 收集整理的這篇文章主要介紹了 了解Go第一步:Go与Plan 9汇编语言 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

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

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

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

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

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

"".main STEXT size=88 args=0x0 locals=0x30 funcid=0x0# main函數(shù),ABIInternal代表使用了新的ABI,即不是所有參數(shù)都在棧中了,main函數(shù)棧幀占48字節(jié)0x0000 00000 (main.go:3) TEXT "".main(SB), ABIInternal, $48-0# 48可以計(jì)算出來(lái),看完后再來(lái)理解一下:48 = 局部變量a,b sum參數(shù)及返回地址 上一個(gè)棧幀BP 一共6個(gè)8B即48# 下面這幾行是判斷棧空間是否足夠。不夠進(jìn)行棧擴(kuò)容。同樣的,GC時(shí)可以進(jìn)行棧縮減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,即為當(dāng)前棧幀分配48字節(jié)。我們讀代碼時(shí)可以對(duì)稱讀,下面必定有個(gè)命令是加480x0006 00006 (main.go:3) SUBQ $48, SP# 先保存上一個(gè)棧幀的棧底(上一棧幀的起始)0x000a 00010 (main.go:3) MOVQ BP, 40(SP)# BP移動(dòng)到新的棧幀棧底。我們可以發(fā)現(xiàn),其實(shí)沒(méi)有使用FP,如果有FP的話FP的值會(huì)為48(SP)。沒(méi)有FP原因上面也說(shuō)了。我們需要注意的是不是任何時(shí)候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)# 可以看出就算沒(méi)有優(yōu)化,也是沒(méi)有定義再賦值,而是直接給a賦值100x0014 00020 (main.go:4) MOVQ $10, "".a+24(SP)# 給b賦值200x001d 00029 (main.go:5) MOVQ $20, "".b+16(SP)# sum參數(shù)之一放到AX寄存器中0x0026 00038 (main.go:6) MOVQ "".a+24(SP), AX# 第二個(gè)參數(shù)放到BX寄存器中0x002b 00043 (main.go:6) MOVL $20, BX0x0030 00048 (main.go:6) PCDATA $1, $0# 調(diào)用sum函數(shù)。此時(shí)我們發(fā)現(xiàn)b下面還有16字節(jié),其實(shí)是sum的調(diào)用參數(shù)0x0030 00048 (main.go:6) CALL "".sum(SB)# 返回結(jié)果存在寄存器AX中,這里存到棧中,可見(jiàn)在局部變量a上面0x0035 00053 (main.go:6) MOVQ AX, ""..autotmp_2+32(SP)# a值存在CX0x003a 00058 (main.go:6) MOVQ "".a+24(SP), CX# a與結(jié)果相加0x003f 00063 (main.go:6) ADDQ AX, CX# 相加結(jié)果賦值給a0x0042 00066 (main.go:6) MOVQ CX, "".a+24(SP)# BP變成上一個(gè)棧幀的棧底0x0047 00071 (main.go:7) MOVQ 40(SP), BP# 函數(shù)調(diào)用完成之前,SP回歸上一棧幀棧頂0x004c 00076 (main.go:7) ADDQ $48, SP# 返回,0x0050 00080 (main.go:7) RET# 下面這幾行對(duì)應(yīng)上面棧擴(kuò)容的跳轉(zhuǎn)行。可以看見(jiàn),棧擴(kuò)容后又跳轉(zhuǎn)回去重新判斷棧是否有爆棧可能性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# 可見(jiàn)sum的棧幀大小為16B,參數(shù)大小為16B,存在上一個(gè)棧幀0x0000 00000 (main.go:9) TEXT "".sum(SB), NOSPLIT|ABIInternal, $16-16# sum函數(shù)有NOSPLIT修飾,所以沒(méi)有棧擴(kuò)容階段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寄存器中的值移動(dòng)到了sum的參數(shù)a中。# 而sum的參數(shù)a存在main棧幀中,所以可以看出加24。# 一個(gè)偏移24一個(gè)偏移32。不是16和24的原因是,CALL和RET會(huì)進(jìn)行隱式的PC/IP寄存器的值存儲(chǔ)0x000e 00014 (main.go:9) MOVQ AX, "".a+24(SP)0x0013 00019 (main.go:9) MOVQ BX, "".b+32(SP)# 這個(gè)應(yīng)該是return a + b變成了 r2 = a + b; return r2。先把r2區(qū)域置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 .....
  • 可能你看了上面的匯編有疑問(wèn),不是說(shuō)1.17開(kāi)始一些架構(gòu)ABI改變了嗎。為什么還是有寄存器和棧空間中的來(lái)回復(fù)制。因?yàn)樯厦媸羌恿瞬粌?yōu)化參數(shù)的匯編。當(dāng)我們?nèi)サ?N。就可以看到。sum的棧幀占用內(nèi)存為0。main棧幀空間也大大縮小(連局部變量a , b都不占用空間了)
  • 個(gè)人覺(jué)得如果看上面的Go匯編沒(méi)什么阻礙,Go匯編就可以先學(xué)到這了,當(dāng)我們真要到匯編層面找Bug或提升性能時(shí)。看不懂再邊學(xué)邊做就行。上來(lái)就學(xué)習(xí)完Go匯編所有細(xì)節(jié),這個(gè)付出回報(bào)比相對(duì)于一般人來(lái)說(shuō)是有點(diǎn)低的
4. 最后我來(lái)繪制一下上面匯編代碼中棧內(nèi)存的情況
------ celler BP (8 bytes) ------ main函數(shù)棧幀 BP sum.ret (8 bytes) ------ main.a (8 bytes) ------ main.b (8 bytes) ------ sum.b (8 bytes) ------ sum.a (8 bytes) ------ main函數(shù)棧幀 SP ret addr (8 bytes) ------ caller(main) BP (8 bytes) ------ sum函數(shù)棧幀 BP 臨時(shí)變量 (8 bytes) ------ sum函數(shù)棧幀 SP

總結(jié)

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

如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。