国产粉嫩无码不卡在线观看,酒店大战丝袜高跟鞋人妻,特级精品毛片免费观看,欧美亚洲日本国产综合在线

萬字詳文:Golang 匯編入門知識總結(jié),看這一篇就夠了(golang 匯編的理解)

作者:ivansli,騰訊 IEG 運營開發(fā)工程師

在深入學(xué)習(xí) Golang 的 runtime 和標(biāo)準(zhǔn)庫實現(xiàn)的時候發(fā)現(xiàn),如果對 Golang 匯編沒有一定了解的話,很難深入了解其底層實現(xiàn)機制。在這里整理總結(jié)了一份基礎(chǔ)的 Golang 匯編入門知識,通過學(xué)習(xí)之后能夠?qū)ζ涞讓訉崿F(xiàn)有一定的認(rèn)識。

0. 為什么寫本文

平時業(yè)務(wù)中一直使用 PHP 編寫代碼,但是一直對 Golang 比較感興趣,閑暇、周末之余會看一些 Go 底層源碼。

近日在分析 go 的某些特性底層功能實現(xiàn)時發(fā)現(xiàn):有些又跟 runtime 運行時有關(guān),而要掌握這一部分的話,有一道坎是繞不過去的,那就是 Go 匯編。索性就查閱了很多大佬們寫的資料,在閱讀之余整理總結(jié)了一下,并在這里分享給大家。

本文使用 Go 版本為 go1.14.1

1. 為什么需要匯編

眾所周知,在計算機的世界里,只有 2 種類型。那就是:0 和 1。

計算機工作是由一系列的機器指令進行驅(qū)動的,這些指令又是一組二進制數(shù)字,其對應(yīng)計算機的高低電平。而這些機器指令的集合就是機器語言,這些機器語言在最底層是與硬件一一對應(yīng)的。

顯而易見,這樣的機器指令有一個致命的缺點:可閱讀性太差(恐怕也只有天才和瘋子才有能力把控得了)。

為了解決可讀性的問題以及代碼編輯的需求,于是就誕生了最接近機器的語言:匯編語言(在我看來,匯編語言更像一種助記符,這些人們?nèi)菀子涀〉拿恳粭l助記符都映射著一條不容易記住的由 0、1 組成的機器指令。你覺得像不像域名與 IP 地址的關(guān)系呢?)。

1.1 程序的編譯過程

以 C 語言為例來說,從 hello.c 的源碼文件到 hello 可執(zhí)行文件,經(jīng)過編譯器處理,大致分為幾個階段:

萬字詳文:Golang 匯編入門知識總結(jié),看這一篇就夠了(golang 匯編的理解)

編譯器在不同的階段會做不同的事情,但是有一步是可以確定的,那就是:源碼會被編譯成匯編,最后才是二進制。

2. 程序與進程

源碼經(jīng)過編譯之后,得到一個二進制的可執(zhí)行文件。文件這兩個字也就表明,目前得到的這個文件跟其他文件對比,除了是具有一定的格式(Linux 中是 ELF 格式,即:可運行可鏈接。executable linkable formate)的二進制組成,并沒什么區(qū)別。

在 Linux 中文件類型大致分為 7 種:

b: 塊設(shè)備文件c:字符設(shè)備文件d:目錄-:普通文件l:鏈接s:socketp:管道

萬字詳文:Golang 匯編入門知識總結(jié),看這一篇就夠了(golang 匯編的理解)

通過上面可以看到,可執(zhí)行文件 main 與源碼文件 main.go,都是同一種類型,屬于普通文件。(當(dāng)然了,在 Unix 中有一句很經(jīng)典的話:一切皆文件)。

那么,問題來了:

  1. 什么是程序?
  2. 什么是進程?

2.1 程序

維基百科告訴我們:程序是指一組指示計算機或其他具有消息處理能力設(shè)備每一步動作的指令,通常用某種程序設(shè)計語言編寫,運行于某種目標(biāo)體系結(jié)構(gòu)上。

從某個層面來看,可以把程序分為靜態(tài)程序、動態(tài)程序:靜態(tài)程序:單純的指具有一定格式的可執(zhí)行二進制文件。動態(tài)程序:則是靜態(tài)可執(zhí)行程序文件被加載到內(nèi)存之后的一種運行時模型(又稱為進程)。

2.2 進程

首先,要知道的是,進程是分配系統(tǒng)資源的最小單位,線程(帶有時間片的函數(shù))是系統(tǒng)調(diào)度的最小單位。進程包含線程,線程所屬于進程。

創(chuàng)建進程一般使用 fork 方法(通常會有個拉起程序,先 fork 自身生成一個子進程。然后,在該子進程中通過 exec 函數(shù)把對應(yīng)程序加載進來,進而啟動目標(biāo)進程。當(dāng)然,實際上要復(fù)雜得多),而創(chuàng)建線程則是使用 pthread 線程庫。

以 32 位 Linux 操作系統(tǒng)為例,進程經(jīng)典的虛擬內(nèi)存結(jié)構(gòu)模型如下圖所示:

萬字詳文:Golang 匯編入門知識總結(jié),看這一篇就夠了(golang 匯編的理解)

其中,有兩處結(jié)構(gòu)是靜態(tài)程序所不具有的,那就是運行時堆(heap)與運行時棧(stack)。

運行時堆從低地址向高地址增長,申請的內(nèi)存空間需要程序員自己或者由 GC 釋放。運行時棧從高地址向低地址增長,內(nèi)存空間在當(dāng)前棧楨調(diào)用結(jié)束之后自動釋放(并不是清除其所占用內(nèi)存中數(shù)據(jù),而是通過棧頂指針 SP 的移動,來標(biāo)識哪些內(nèi)存是正在使用的)。

3. Go 匯編

對于 Go 編譯器而言,其輸出的結(jié)果是一種抽象可移植的匯編代碼,這種匯編(Go 的匯編是基于 Plan9 的匯編)并不對應(yīng)某種真實的硬件架構(gòu)。Go 的匯編器會使用這種偽匯編,再為目標(biāo)硬件生成具體的機器指令。

偽匯編這一個額外層可以帶來很多好處,最主要的一點是方便將 Go 移植到新的架構(gòu)上。

相關(guān)的信息可以參考 Rob Pike 的 The Design of the Go Assembler。

要了解 Go 的匯編器最重要的是要知道 Go 的匯編器不是對底層機器的直接表示,即 Go 的匯編器沒有直接使用目標(biāo)機器的匯編指令。Go 匯編器所用的指令,一部分與目標(biāo)機器的指令一一對應(yīng),而另外一部分則不是。這是因為編譯器套件不需要匯編器直接參與常規(guī)的編譯過程。

相反,編譯器使用了一種半抽象的指令集,并且部分指令是在代碼生成后才被選擇的。匯編器基于這種半抽象的形式工作,所以雖然你看到的是一條 MOV 指令,但是工具鏈針對對這條指令實際生成可能完全不是一個移動指令,也許會是清除或者加載。也有可能精確的對應(yīng)目標(biāo)平臺上同名的指令。概括來說,特定于機器的指令會以他們的本尊出現(xiàn), 然而對于一些通用的操作,如內(nèi)存的移動以及子程序的調(diào)用以及返回通常都做了抽象。細(xì)節(jié)因架構(gòu)不同而不一樣,我們對這樣的不精確性表示歉意,情況并不明確。

匯編器程序的工作是對這樣半抽象指令集進行解析并將其轉(zhuǎn)變?yōu)榭梢暂斎氲芥溄悠鞯闹噶睢?/p>

The most important thing to know about Go’s assembler is that it is not a direct representation of the underlying machine. Some of the details map precisely to the machine, but some do not. This is because the compiler suite needs no assembler pass in the usual pipeline. Instead, the compiler operates on a kind of semi-abstract instruction set, and instruction selection occurs partly after code generation. The assembler works on the semi-abstract form, so when you see an instruction like MOV what the toolchain actually generates for that operation might not be a move instruction at all, perhaps a clear or load.

Or it might correspond exactly to the machine instruction with that name. In general, machine-specific operations tend to appear as themselves, while more general concepts like memory move and subroutine call and return are more abstract. The details vary with architecture, and we apologize for the imprecision; the situation is not well-defined.

The assembler program is a way to parse a description of that semi-abstract instruction set and turn it into instructions to be input to the linker.

Go 匯編使用的是caller-save模式,被調(diào)用函數(shù)的入?yún)?shù)、返回值都由調(diào)用者維護、準(zhǔn)備。因此,當(dāng)需要調(diào)用一個函數(shù)時,需要先將這些工作準(zhǔn)備好,才調(diào)用下一個函數(shù),另外這些都需要進行內(nèi)存對齊,對齊的大小是 sizeof(uintptr)。

3.1 幾個概念

在深入了解 Go 匯編之前,需要知道的幾個概念:

  • 棧:進程、線程、goroutine 都有自己的調(diào)用棧,先進后出(FILO)
  • 棧幀:可以理解是函數(shù)調(diào)用時,在棧上為函數(shù)所分配的內(nèi)存區(qū)域
  • 調(diào)用者:caller,比如:A 函數(shù)調(diào)用了 B 函數(shù),那么 A 就是調(diào)用者
  • 被調(diào)者:callee,比如:A 函數(shù)調(diào)用了 B 函數(shù),那么 B 就是被調(diào)者

3.2 Go 的核心寄存器

go 匯編中有 4 個核心的偽寄存器,這 4 個寄存器是編譯器用來維護上下文、特殊標(biāo)識等作用的:

寄存器說明SB(Static base pointer)global symbolsFP(Frame pointer)arguments and localsPC(Program counter)jumps and branchesSP(Stack pointer)top of stack

  • FP: 使用如 symbol offset(FP)的方式,引用 callee 函數(shù)的入?yún)?shù)。例如 arg0 0(FP),arg1 8(FP),使用 FP 必須加 symbol ,否則無法通過編譯(從匯編層面來看,symbol 沒有什么用,加 symbol 主要是為了提升代碼可讀性)。另外,需要注意的是:往往在編寫 go 匯編代碼時,要站在 callee 的角度來看(FP),在 callee 看來,(FP)指向的是 caller 調(diào)用 callee 時傳遞的第一個參數(shù)的位置。假如當(dāng)前的 callee 函數(shù)是 add,在 add 的代碼中引用 FP,該 FP 指向的位置不在 callee 的 stack frame 之內(nèi)。而是在 caller 的 stack frame 上,指向調(diào)用 add 函數(shù)時傳遞的第一個參數(shù)的位置,經(jīng)常在 callee 中用symbol offset(FP)來獲取入?yún)⒌膮?shù)值。
  • SB: 全局靜態(tài)基指針,一般用在聲明函數(shù)、全局變量中。
  • SP: 該寄存器也是最具有迷惑性的寄存器,因為會有偽 SP 寄存器和硬件 SP 寄存器之分。plan9 的這個偽 SP 寄存器指向當(dāng)前棧幀第一個局部變量的結(jié)束位置(為什么說是結(jié)束位置,可以看下面寄存器內(nèi)存布局圖),使用形如 symbol offset(SP) 的方式,引用函數(shù)的局部變量。offset 的合法取值是 [-framesize, 0),注意是個左閉右開的區(qū)間。假如局部變量都是 8 字節(jié),那么第一個局部變量就可以用 localvar0-8(SP) 來表示。與硬件寄存器 SP 是兩個不同的東西,在棧幀 size 為 0 的情況下,偽寄存器 SP 和硬件寄存器 SP 指向同一位置。手寫匯編代碼時,如果是 symbol offset(SP)形式,則表示偽寄存器 SP。如果是 offset(SP)則表示硬件寄存器 SP。務(wù)必注意:對于編譯輸出(go tool compile -S / go tool objdump)的代碼來講,所有的 SP 都是硬件 SP 寄存器,無論是否帶 symbol(這一點非常具有迷惑性,需要慢慢理解。往往在分析編譯輸出的匯編時,看到的就是硬件 SP 寄存器)。
  • PC: 實際上就是在體系結(jié)構(gòu)的知識中常見的 pc 寄存器,在 x86 平臺下對應(yīng) ip 寄存器,amd64 上則是 rip。除了個別跳轉(zhuǎn)之外,手寫 plan9 匯編代碼時,很少用到 PC 寄存器。

通過上面的講解,想必已經(jīng)對 4 個核心寄存器的區(qū)別有了一定的認(rèn)識(或者是更加的迷惑、一頭霧水)。那么,需要留意的是:如果是在分析編譯輸出的匯編代碼時,要重點看 SP、SB 寄存器(FP 寄存器在這里是看不到的)。如果是,在手寫匯編代碼,那么要重點看 FP、SP 寄存器。

3.2.1 偽寄存器的內(nèi)存模型

下圖描述了棧楨與各個寄存器的內(nèi)存關(guān)系模型,值得注意的是要站在 callee 的角度來看。

萬字詳文:Golang 匯編入門知識總結(jié),看這一篇就夠了(golang 匯編的理解)

有一點需要注意的是,return addr 也是在 caller 的棧上的,不過往棧上插 return addr 的過程是由 CALL 指令完成的(在分析匯編時,是看不到關(guān)于 addr 相關(guān)空間信息的。在分配??臻g時,addr 所占用空間大小不包含在棧幀大小內(nèi))。

在 AMD64 環(huán)境,偽 PC 寄存器其實是 IP 指令計數(shù)器寄存器的別名。偽 FP 寄存器對應(yīng)的是 caller 函數(shù)的幀指針,一般用來訪問 callee 函數(shù)的入?yún)?shù)和返回值。偽 SP 棧指針對應(yīng)的是當(dāng)前 callee 函數(shù)棧幀的底部(不包括參數(shù)和返回值部分),一般用于定位局部變量。偽 SP 是一個比較特殊的寄存器,因為還存在一個同名的 SP 真寄存器,真 SP 寄存器對應(yīng)的是棧的頂部。

在編寫 Go 匯編時,當(dāng)需要區(qū)分偽寄存器和真寄存器的時候只需要記住一點:偽寄存器一般需要一個標(biāo)識符和偏移量為前綴,如果沒有標(biāo)識符前綴則是真寄存器。比如(SP)、 8(SP)沒有標(biāo)識符前綴為真 SP 寄存器,而 a(SP)、b 8(SP)有標(biāo)識符為前綴表示偽寄存器。

3.2.2 幾點說明

我們這里對容易混淆的幾點簡單進行說明:

  • 偽 SP 和硬件 SP 不是一回事,在手寫匯編代碼時,偽 SP 和硬件 SP 的區(qū)分方法是看該 SP 前是否有 symbol。如果有 symbol,那么即為偽寄存器,如果沒有,那么說明是硬件 SP 寄存器。
  • 偽 SP 和 FP 的相對位置是會變的,所以不應(yīng)該嘗試用偽 SP 寄存器去找那些用 FP offset 來引用的值,例如函數(shù)的入?yún)⒑头祷刂怠?/li>
  • 官方文檔中說的偽 SP 指向 stack 的 top,可能是有問題的。其指向的局部變量位置實際上是整個棧的棧底(除 caller BP 之外),所以說 bottom 更合適一些。
  • 在 go tool objdump/go tool compile -S 輸出的代碼中,是沒有偽 SP 和 FP 寄存器的,我們上面說的區(qū)分偽 SP 和硬件 SP 寄存器的方法,對于上述兩個命令的輸出結(jié)果是沒法使用的。在編譯和反匯編的結(jié)果中,只有真實的 SP 寄存器。

3.2.3 IA64 和 plan9 的對應(yīng)關(guān)系

在 plan9 匯編里還可以直接使用的 amd64 的通用寄存器,應(yīng)用代碼層面會用到的通用寄存器主要是: rax, rbx, rcx, rdx, rdi, rsi, r8~r15 這些寄存器,雖然 rbp 和 rsp 也可以用,不過 bp 和 sp 會被用來管理棧頂和棧底,最好不要拿來進行運算。

plan9 中使用寄存器不需要帶 r 或 e 的前綴,例如 rax,只要寫 AX 即可: MOVQ $101, AX = mov rax, 101

下面是通用通用寄存器的名字在 IA64 和 plan9 中的對應(yīng)關(guān)系:

萬字詳文:Golang 匯編入門知識總結(jié),看這一篇就夠了(golang 匯編的理解)

3.3 常用操作指令

下面列出了常用的幾個匯編指令(指令后綴Q 說明是 64 位上的匯編指令)

助記符指令種類用途示例MOVQ傳送數(shù)據(jù)傳送MOVQ 48, AX // 把 48 傳送到 AXLEAQ傳送地址傳送LEAQ AX, BX // 把 AX 有效地址傳送到 BXPUSHQ傳送棧壓入PUSHQ AX // 將 AX 內(nèi)容送入棧頂位置POPQ傳送棧彈出POPQ AX // 彈出棧頂數(shù)據(jù)后修改棧頂指針ADDQ運算相加并賦值A(chǔ)DDQ BX, AX // 等價于 AX =BXSUBQ運算相減并賦值SUBQ BX, AX // 等價于 AX-=BXCMPQ運算比較大小CMPQ SI CX // 比較 SI 和 CX 的大小CALL轉(zhuǎn)移調(diào)用函數(shù)CALL runtime.printnl(SB) // 發(fā)起調(diào)用JMP轉(zhuǎn)移無條件轉(zhuǎn)移指令JMP 0x0185 //無條件轉(zhuǎn)至 0x0185 地址處JLS轉(zhuǎn)移條件轉(zhuǎn)移指令JLS 0x0185 //左邊小于右邊,則跳到 0x0185

4. 匯編分析

說了那么多,it is code show time。

4.1 如何輸出 Go 匯編

對于寫好的 go 源碼,生成對應(yīng)的 Go 匯編,大概有下面幾種

  • 方法 1 先使用 go build -gcflags "-N -l" main.go 生成對應(yīng)的可執(zhí)行二進制文件 再使用 go tool objdump -s "main." main 反編譯獲取對應(yīng)的匯編

反編譯時"main." 表示只輸出 main 包中相關(guān)的匯編"main.main" 則表示只輸出 main 包中 main 方法相關(guān)的匯編

  • 方法 2 使用 go tool compile -S -N -l main.go 這種方式直接輸出匯編
  • 方法 3 使用go build -gcflags="-N -l -S" main.go 直接輸出匯編

注意:在使用這些命令時,加上對應(yīng)的 flag,否則某些邏輯會被編譯器優(yōu)化掉,而看不到對應(yīng)完整的匯編代碼

-l 禁止內(nèi)聯(lián) -N 編譯時,禁止優(yōu)化 -S 輸出匯編代碼

4.2 Go 匯編示例

go 示例代碼

package mainfunc add(a, b int) int{        sum := 0 // 不設(shè)置該局部變量sum,add??臻g大小會是0        sum = a b        return sum}func main(){        println(add(1,2))}

編譯 go 源代碼,輸出匯編

go tool compile -N -l -S main.go

截取主要匯編如下:

"".add STEXT nosplit size=60 args=0x18 locals=0x10 0x0000 00000 (main.go:3) TEXT "".add(SB), NOSPLIT, $16-24 0x0000 00000 (main.go:3) SUBQ $16, SP ;;生成add??臻g 0x0004 00004 (main.go:3) MOVQ BP, 8(SP) 0x0009 00009 (main.go:3) LEAQ 8(SP), BP ;; ...omitted FUNCDATA stuff... 0x000e 00014 (main.go:3) MOVQ $0, "".~r2 40(SP) ;;初始化返回值 0x0017 00023 (main.go:4) MOVQ $0, "".sum(SP) ;;局部變量sum賦為0 0x001f 00031 (main.go:5) MOVQ "".a 24(SP), AX ;;取參數(shù)a 0x0024 00036 (main.go:5) ADDQ "".b 32(SP), AX ;;等價于AX=a b 0x0029 00041 (main.go:5) MOVQ AX, "".sum(SP) ;;賦值局部變量sum 0x002d 00045 (main.go:6) MOVQ AX, "".~r2 40(SP) ;;設(shè)置返回值 0x0032 00050 (main.go:6) MOVQ 8(SP), BP 0x0037 00055 (main.go:6) ADDQ $16, SP ;;清除add棧空間 0x003b 00059 (main.go:6) RET ......"".main STEXT size=107 args=0x0 locals=0x28 0x0000 00000 (main.go:9) TEXT "".main(SB), $40-0 ...... 0x000f 00015 (main.go:9) SUBQ $40, SP ;; 生成main??臻g 0x0013 00019 (main.go:9) MOVQ BP, 32(SP) 0x0018 00024 (main.go:9) LEAQ 32(SP), BP ;; ...omitted FUNCDATA stuff... 0x001d 00029 (main.go:10) MOVQ $1, (SP) ;;add入?yún)ⅲ? 0x0025 00037 (main.go:10) MOVQ $2, 8(SP) ;;add入?yún)ⅲ? 0x002e 00046 (main.go:10) CALL "".add(SB) ;;調(diào)用add函數(shù) 0x0033 00051 (main.go:10) MOVQ 16(SP), AX 0x0038 00056 (main.go:10) MOVQ AX, ""..autotmp_0 24(SP) 0x003d 00061 (main.go:10) CALL runtime.printlock(SB) 0x0042 00066 (main.go:10) MOVQ ""..autotmp_0 24(SP), AX 0x0047 00071 (main.go:10) MOVQ AX, (SP) 0x004b 00075 (main.go:10) CALL runtime.printint(SB) 0x0050 00080 (main.go:10) CALL runtime.printnl(SB) 0x0055 00085 (main.go:10) CALL runtime.printunlock(SB) 0x005a 00090 (main.go:11) MOVQ 32(SP), BP 0x005f 00095 (main.go:11) ADDQ $40, SP ;;清除main??臻g 0x0063 00099 (main.go:11) RET ......

這里列舉了一個簡單的 int 類型加法示例,實際開發(fā)中會遇到各種參數(shù)類型,要復(fù)雜的多,這里只是拋磚引玉 ??

4.3 Go 匯編解析

針對 4.2 輸出匯編,對重要核心代碼進行分析。

4.3.1 add 函數(shù)匯編解析

  • TEXT "".add(SB), NOSPLIT|ABIInternal, $16-24

TEXT "".add TEXT 指令聲明了 "".add 是 .text 代碼段的一部分,并表明跟在這個聲明后的是函數(shù)的函數(shù)體。在鏈接期,""這個空字符會被替換為當(dāng)前的包名: 也就是說,"".add在鏈接到二進制文件后會變成 main.add

(SB) SB 是一個虛擬的偽寄存器,保存靜態(tài)基地址(static-base) 指針,即我們程序地址空間的開始地址。"".add(SB) 表明我們的符號位于某個固定的相對地址空間起始處的偏移位置 (最終是由連接器計算得到的)。換句話來講,它有一個直接的絕對地址: 是一個全局的函數(shù)符號。

NOSPLIT: 向編譯器表明不應(yīng)該插入 stack-split 的用來檢查棧需要擴張的前導(dǎo)指令。在我們 add 函數(shù)的這種情況下,編譯器自己幫我們插入了這個標(biāo)記: 它足夠聰明地意識到,由于 add 沒有任何局部變量且沒有它自己的棧幀,所以一定不會超出當(dāng)前的棧。不然,每次調(diào)用函數(shù)時,在這里執(zhí)行棧檢查就是完全浪費 CPU 時間了。

$0-16

24 指定了調(diào)用方傳入的參數(shù) 返回值大?。?4 字節(jié)=入?yún)?a、b 大小8字節(jié)*2 返回值8字節(jié))

通常來講,幀大小后一般都跟隨著一個參數(shù)大小,用減號分隔。(這不是一個減法操作,只是一種特殊的語法) 幀大小 $24-8 意味著這個函數(shù)有 24 個字節(jié)的幀以及 8 個字節(jié)的參數(shù),位于調(diào)用者的幀上。如果 NOSPLIT 沒有在 TEXT 中指定,則必須提供參數(shù)大小。對于 Go 原型的匯編函數(shù),go vet 會檢查參數(shù)大小是否正確。

In the general case, the frame size is followed by an argument size, separated by a minus sign. (It’s not a subtraction, just idiosyncratic syntax.) The frame size $24-8 states that the function has a 24-byte frame and is called with 8 bytes of argument, which live on the caller’s frame. If NOSPLIT is not specified for the TEXT, the argument size must be provided. For assembly functions with Go prototypes, go vet will check that the argument size is correct.

  • SUBQ $16, SPSP 為棧頂指針,該語句等價于 SP-=16(由于棧空間是向下增長的,所以開辟棧空間時為減操作),表示生成 16 字節(jié)大小的棧空間。
  • MOVQ $0, "".~r2 40(SP)此時的 SP 為 add 函數(shù)棧的棧頂指針,40(SP)的位置則是 add 返回值的位置,該位置位于 main 函數(shù)??臻g內(nèi)。該語句設(shè)置返回值類型的 0 值,即初始化返回值,防止得到臟數(shù)據(jù)(返回值類型為 int,int 的 0 值為 0)。
  • MOVQ "".a 24(SP), AX從 main 函數(shù)棧空間獲取入?yún)?a 的值,存到寄存器 AX
  • ADDQ "".b 32(SP), AX從 main 函數(shù)??臻g獲取入?yún)?b 的值,與寄存器 AX 中存儲的 a 值相加,結(jié)果存到 AX。相當(dāng)于 AX=a b
  • MOVQ AX, "".~r2 40(SP)把 a b 的結(jié)果放到 main 函數(shù)棧中, add(a b)返回值所在的位置
  • ADDQ $16, SP歸還 add 函數(shù)占用的??臻g

4.3.2 函數(shù)棧楨結(jié)構(gòu)模型

根據(jù) 4.2 對應(yīng)匯編繪制的函數(shù)棧楨結(jié)構(gòu)模型

萬字詳文:Golang 匯編入門知識總結(jié),看這一篇就夠了(golang 匯編的理解)

還記得前面提到的,Go 匯編使用的是caller-save模式,被調(diào)用函數(shù)的參數(shù)、返回值、棧位置都需要由調(diào)用者維護、準(zhǔn)備嗎?

在函數(shù)棧楨結(jié)構(gòu)中可以看到,add()函數(shù)的入?yún)⒁约胺祷刂刀加烧{(diào)用者 main()函數(shù)維護。也正是因為如此,GO 有了其他語言不具有的,支持多個返回值的特性。

4.4 Go 匯編語法

這里重點講一下函數(shù)聲明、變量聲明。

4.4.1 函數(shù)聲明

來看一個典型的 Go 匯編函數(shù)定義

// func add(a, b int) int// 該add函數(shù)聲明定義在同一個 package name 下的任意 .go文件中// 只有函數(shù)頭,沒有實現(xiàn)// add函數(shù)的Go匯編實現(xiàn)// pkgname 默認(rèn)是  ""TEXT pkgname·add(SB), NOSPLIT, $16-24    MOVQ a 0(FP), AX    ADDQ b 8(FP), AX    MOVQ AX, ret 16(FP)    RET

Go 匯編實現(xiàn)為什么是 TEXT 開頭?仔細(xì)觀察上面的進程內(nèi)存布局圖就會發(fā)現(xiàn),我們的代碼在是存儲在.text 段中的,這里也就是一種約定俗成的起名方式。實際上在 plan9 中 TEXT 是一個指令,用來定義一個函數(shù)。

定義中的 pkgname 是可以省略的,(非想寫也可以寫上,不過寫上 pkgname 的話,在重命名 package 之后還需要改代碼,默認(rèn)為"") 編譯器會在鏈接期自動加上所屬的包名稱。

中點 · 比較特殊,是一個 unicode 的中點,該點在 mac 下的輸入方法是 option shift 9。在程序被鏈接之后,所有的中點·都會被替換為句號.,比如你的方法是runtime·main,在編譯之后的程序里的符號則是runtime.main。

簡單總結(jié)一下, Go 匯編實現(xiàn)函數(shù)聲明,格式為:

 靜態(tài)基地址(static-base) 指針    |                  |         add函數(shù)入?yún)?返回值總大小                  |               |TEXT pkgname·add(SB),NOSPLIT,$16-24      |      |                |函數(shù)所屬包名  函數(shù)名          add函數(shù)棧幀大小

  • 函數(shù)棧幀大?。壕植孔兞?可能需要的額外調(diào)用函數(shù)的參數(shù)空間的總大小,不包括調(diào)用其它函數(shù)時的 ret address 的大小。
  • (SB): SB 是一個虛擬寄存器,保存了靜態(tài)基地址(static-base) 指針,即我們程序地址空間的開始地址。"".add(SB) 表明我們的符號位于某個固定的相對地址空間起始處的偏移位置 (最終是由鏈接器計算得到的)。換句話來講,它有一個直接的絕對地址: 是一個全局的函數(shù)符號。
  • NOSPLIT: 向編譯器表明,不應(yīng)該插入 stack-split 的用來檢查棧需要擴張的前導(dǎo)指令。在我們 add 函數(shù)的這種情況下,編譯器自己幫我們插入了這個標(biāo)記: 它足夠聰明地意識到,add 不會超出當(dāng)前的棧,因此沒必要調(diào)用函數(shù)時在這里執(zhí)行棧檢查。

4.4.2 變量聲明

匯編里的全局變量,一般是存儲在.rodata或者.data段中。對應(yīng)到 Go 代碼,就是已初始化過的全局的 const、var 變量/常量。

使用 DATA 結(jié)合 GLOBL 來定義一個變量。

DATA 的用法為:

DATA symbol offset(SB)/width, value

大多數(shù)參數(shù)都是字面意思,不過這個 offset 需要注意:其含義是該值相對于符號 symbol 的偏移,而不是相對于全局某個地址的偏移。

GLOBL 匯編指令用于定義名為 symbol 的全局變量,變量對應(yīng)的內(nèi)存寬度為 width,內(nèi)存寬度部分必須用常量初始化。

GLOBL ·symbol(SB), width

下面是定義了多個變量的例子:

DATA ·age 0(SB)/4, $8  ;; 數(shù)值8為 4字節(jié)GLOBL ·age(SB), RODATA, $4DATA ·pi 0(SB)/8, $3.1415926 ;; 數(shù)值3.1415926為float64, 8字節(jié)GLOBL ·pi(SB), RODATA, $8DATA ·year 0(SB)/4, $2020 ;; 數(shù)值2020為 4字節(jié)GLOBL ·year(SB), RODATA, $4;; 變量hello 使用2個DATA來定義DATA ·hello 0(SB)/8, $"hello my" ;; `hello my` 共8個字節(jié)DATA ·hello 8(SB)/8, $"   world" ;; `   world` 共8個字節(jié)(3個空格)GLOBL ·hello(SB), RODATA, $16 ;; `hello my   world`  共16個字節(jié)DATA ·hello<> 0(SB)/8, $"hello my" ;; `hello my` 共8個字節(jié)DATA ·hello<> 8(SB)/8, $"   world" ;; `   world` 共8個字節(jié)(3個空格)GLOBL ·hello<>(SB), RODATA, $16 ;; `hello my   world`  共16個字節(jié)

大部分都比較好理解,不過這里引入了新的標(biāo)記<>,這個跟在符號名之后,表示該全局變量只在當(dāng)前文件中生效,類似于 C 語言中的 static。如果在另外文件中引用該變量的話,會報 relocation target not found 的錯誤。

5. 手寫匯編實現(xiàn)功能

在 Go 源碼中會看到一些匯編寫的代碼,這些代碼跟其他 go 代碼一起組成了整個 go 的底層功能實現(xiàn)。下面,我們通過一個簡單的 Go 匯編代碼示例來實現(xiàn)兩數(shù)相加功能。

5.1 使用 Go 匯編實現(xiàn) add 函數(shù)

萬字詳文:Golang 匯編入門知識總結(jié),看這一篇就夠了(golang 匯編的理解)

Go 代碼

package mainfunc add(a, b int64) int64func main(){        println(add(2,3))}

Go 源碼中 add()函數(shù)只有函數(shù)簽名,沒有具體的實現(xiàn)(使用 GO 匯編實現(xiàn))

使用 Go 匯編實現(xiàn)的 add()函數(shù)

TEXT ·add(SB), $0-24 ;; add棧空間為0,入?yún)?返回值大小=24字節(jié)        MOVQ x 0(FP), AX ;; 從main中取參數(shù):2        ADDQ y 8(FP), AX ;; 從main中取參數(shù):3        MOVQ AX, ret 16(FP) ;; 保存結(jié)果到返回值        RET

把 Go 源碼與 Go 匯編編譯到一起(我這里,這兩個文件在同一個目錄)

go build -gcflags "-N -l" .

我這里目錄為 demo1,所以得到可執(zhí)行程序 demo1,運行得到結(jié)果:5

5.2 反編譯可執(zhí)行程序

對 5.1 中得到的可執(zhí)行程序 demo1 使用 objdump 進行反編譯,獲取匯編代碼

go tool objdump -s "main." demo1

得到匯編

......TEXT main.main(SB) /root/go/src/demo1/main.go  main.go:5   0x4581d0     64488b0c25f8ffffff      MOVQ FS:0xfffffff8, CX  main.go:5   0x4581d9     483b6110                CMPQ 0x10(CX), SP  main.go:5   0x4581dd     7655                    JBE 0x458234  main.go:5   0x4581df     4883ec28                SUBQ $0x28, SP ;;生成main棧楨  main.go:5   0x4581e3     48896c2420              MOVQ BP, 0x20(SP)  main.go:5   0x4581e8     488d6c2420              LEAQ 0x20(SP), BP  main.go:6   0x4581ed     48c7042402000000        MOVQ $0x2, 0(SP) ;;參數(shù)值 2  main.go:6   0x4581f5     48c744240803000000      MOVQ $0x3, 0x8(SP) ;;參數(shù)值 3  main.go:6   0x4581fe     e83d000000              CALL main.add(SB);;call add  main.go:6   0x458203     488b442410              MOVQ 0x10(SP), AX  main.go:6   0x458208     4889442418              MOVQ AX, 0x18(SP)  main.go:6   0x45820d     e8fe2dfdff              CALL runtime.printlock(SB)  main.go:6   0x458212     488b442418              MOVQ 0x18(SP), AX  main.go:6   0x458217     48890424                MOVQ AX, 0(SP)  main.go:6   0x45821b     e87035fdff              CALL runtime.printint(SB)  main.go:6   0x458220     e87b30fdff              CALL runtime.printnl(SB)  main.go:6   0x458225     e8662efdff              CALL runtime.printunlock(SB)  main.go:7   0x45822a     488b6c2420              MOVQ 0x20(SP), BP  main.go:7   0x45822f     4883c428                ADDQ $0x28, SP  main.go:7   0x458233     c3                      RET  main.go:5   0x458234     e89797ffff              CALL runtime.morestack_noctxt(SB)  main.go:5   0x458239     eb95                    JMP main.main(SB);; 反編譯得到的匯編與add_amd64.s文件中的匯編大致操作一致TEXT main.add(SB) /root/go/src/demo1/add_amd64.s  add_amd64.s:2   0x458240    488b442408    MOVQ 0x8(SP), AX ;; 獲取第一個參數(shù)  add_amd64.s:3   0x458245    4803442410    ADDQ 0x10(SP), AX ;;參數(shù)a 參數(shù)b  add_amd64.s:5   0x45824a    4889442418    MOVQ AX, 0x18(SP) ;;保存計算結(jié)果  add_amd64.s:7   0x45824f    c3            RET

通過上面操作,可知:

  1. (FP)偽寄存器,只有在編寫 Go 匯編代碼時使用。FP 偽寄存器指向 caller 傳遞給 callee 的第一個參數(shù)
  2. 使用 go tool compile / go tool objdump 得到的匯編中看不到(FP)寄存器的蹤影

6. Go 調(diào)試工具

這里推薦 2 個 Go 代碼調(diào)試工具。

6.1 gdb 調(diào)試 Go 代碼

測試代碼

package maintype Ier interface{        add(a, b int) int        sub(a, b int) int}type data struct{        a, b int}func (*data) add(a, b int) int{        return a b}func (*data) sub(a, b int) int{        return a-b}func main(){        var t Ier = &data{3,4}        println(t.add(1,2))        println(t.sub(3,2))}

編譯 go build -gcflags "-N -l" -o main

使用 GDB 調(diào)試

> gdb mainGNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-80.el7Copyright (C) 2013 Free Software Foundation, Inc.License GPLv3 : GNU GPL version 3 or later http://gnu.org/licenses/gpl.htmlThis is free software: you are free to change and redistribute it.There is NO WARRANTY, to the extent permitted by law. Type "show copying"and "show warranty" for details.This GDB was configured as "x86_64-redhat-linux-gnu".For bug reporting instructions, please see:<http://www.gnu.org/software/gdb/bugs/>...Reading symbols from /root/go/src/interface/main...done.Loading Go Runtime support.(gdb) list // 顯示源碼14 func (*data) add(a, b int) int{15 return a b16 }1718 func (*data) sub(a, b int) int{19 return a-b20 }212223 func main(){(gdb) list24 var t Ier = &data{3,4}2526 println(t.add(1,2))27 println(t.sub(3,2))28 }29(gdb) b 26 // 在源碼26行處設(shè)置斷點Breakpoint 1 at 0x45827c: file /root/go/src/interface/main.go, line 26.(gdb) rStarting program: /root/go/src/interface/mainBreakpoint 1, main.main () at /root/go/src/interface/main.go:2626 println(t.add(1,2))(gdb) info locals // 顯示變量t = {tab = 0x487020 <data,main.Ier>, data = 0xc000096000}(gdb) ptype t // 打印t的結(jié)構(gòu)type = struct runtime.iface { runtime.itab *tab; void *data;}(gdb) p *t.tab.inter // 打印t.tab.inter指針指向的數(shù)據(jù)$2 = {typ = {size = 16, ptrdata = 16, hash = 2491815843, tflag = 7 'a', align = 8 'b', fieldAlign = 8 'b', kind = 20 '024', equal = {void (void *, void *, bool *)} 0x466ec0, gcdata = 0x484351 "002003004005006abtnfr016017020022025026030033034036037"&(,-5<BUXx216231330335377", str = 6568, ptrToThis = 23808}, pkgpath = {bytes = 0x4592b4 ""}, mhdr = []runtime.imethod = {{name = 277, ityp = 48608}, {name = 649, ityp = 48608}}}(gdb) disass // 顯示匯編Dump of assembler code for function main.main: 0x0000000000458210 < 0>: mov %fs:0xfffffffffffffff8,%rcx 0x0000000000458219 < 9>: cmp 0x10(%rcx),%rsp 0x000000000045821d < 13>: jbe 0x458324 <main.main 276> 0x0000000000458223 < 19>: sub $0x50,%rsp 0x0000000000458227 < 23>: mov %rbp,0x48(%rsp) 0x000000000045822c < 28>: lea 0x48(%rsp),%rbp 0x0000000000458231 < 33>: lea 0x10dc8(%rip),%rax # 0x469000 0x0000000000458238 < 40>: mov %rax,(%rsp) 0x000000000045823c < 44>: callq 0x40a5c0 <runtime.newobject>

常用的 gdb 調(diào)試命令

  • run
  • continue
  • break
  • backtrace 與 frame
  • info break、locals
  • list 命令
  • print 和 ptype 命令
  • disass

除了 gdb,另外推薦一款 gdb 的增強版調(diào)試工具 cgdb

https://cgdb.github.io/

效果如下圖所示,分兩個窗口:上面顯示源代碼,下面是具體的命令行調(diào)試界面(跟 gdb 一樣):

萬字詳文:Golang 匯編入門知識總結(jié),看這一篇就夠了(golang 匯編的理解)

6.2 delve 調(diào)試代碼

delve 項目地址

https://github.com/go-delve/delve

帶圖形化界面的 dlv 項目地址

https://github.com/aarzilli/gdlv

dlv 的安裝使用,這里不再做過多講解,感興趣的可以嘗試一下。

  • gdb 作為調(diào)試工具自是不用多說,比較老牌、強大,可以支持多種語言。
  • delve 則是使用 go 語言開發(fā)的,用來調(diào)試 go 的工具,功能也是十分強大,打印結(jié)果可以顯示 gdb 支持不了的東西,這里不再做過多講解,有興趣的可以查閱相關(guān)資料。

7. 總結(jié)

對于 Go 匯編基礎(chǔ)大致需要熟悉下面幾個方面:

萬字詳文:Golang 匯編入門知識總結(jié),看這一篇就夠了(golang 匯編的理解)

通過上面的例子相信已經(jīng)讓你對 Go 的匯編有了一定的理解。當(dāng)然,對于大部分業(yè)務(wù)開發(fā)人員來說,只要看的懂即可。如果想進一步的了解,可以閱讀相關(guān)的資料或者書籍。

最后想說的是:鑒于個人能力有限,在閱讀過程中你可能會發(fā)現(xiàn)存在的一些問題或者缺陷,歡迎各位大佬指正。如果感興趣的話,也可以一起私下交流。

8. 參考資料

在整理的過程中,部分參考、引用下面鏈接地址內(nèi)容。有一些寫的還是不錯的,感興趣的同學(xué)可以閱讀。

[1] https://github.com/cch123/golang-notes/blob/master/assembly.md plan9 assembly

[2] https://segmentfault.com/a/1190000019753885 匯編入門

[3] https://www.davidwong.fr/goasm/ Go Assembly by Example

[4] https://juejin.im/post/6844904005630443533#heading-3

[5] https://github.com/go-internals-cn/go-internals/blob/master/chapter1_assembly_primer/README.md

[6] https://lrita.github.io/2017/12/12/golang-asm/

[7] https://chai2010.cn/advanced-go-programming-book/ch3-asm/ch3-01-basic.html

版權(quán)聲明:本文內(nèi)容由互聯(lián)網(wǎng)用戶自發(fā)貢獻,該文觀點僅代表作者本人。本站僅提供信息存儲空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如發(fā)現(xiàn)本站有涉嫌抄襲侵權(quán)/違法違規(guī)的內(nèi)容, 請發(fā)送郵件至 舉報,一經(jīng)查實,本站將立刻刪除。