做者:ivansli,騰訊 IEG 運營開發工程師html
在深刻學習 Golang 的 runtime 和標準庫實現的時候發現,若是對 Golang 彙編沒有必定了解的話,很難深刻了解其底層實現機制。在這裏整理總結了一份基礎的 Golang 彙編入門知識,經過學習以後可以對其底層實現有必定的認識。
平時業務中一直使用 PHP 編寫代碼,可是一直對 Golang 比較感興趣,閒暇、週末之餘會看一些 Go 底層源碼。linux
近日在分析 go 的某些特性底層功能實現時發現:有些又跟 runtime 運行時有關,而要掌握這一部分的話,有一道坎是繞不過去的,那就是 Go 彙編。索性就查閱了不少大佬們寫的資料,在閱讀之餘整理總結了一下,並在這裏分享給你們。ios
本文使用 Go 版本爲 go1.14.1
衆所周知,在計算機的世界裏,只有 2 種類型。那就是:0 和 1。git
計算機工做是由一系列的機器指令進行驅動的,這些指令又是一組二進制數字,其對應計算機的高低電平。而這些機器指令的集合就是機器語言,這些機器語言在最底層是與硬件一一對應的。程序員
顯而易見,這樣的機器指令有一個致命的缺點:可閱讀性太差
(恐怕也只有天才和瘋子纔有能力把控得了)。github
爲了解決可讀性的問題以及代碼編輯的需求,因而就誕生了最接近機器的語言:彙編語言(在我看來,彙編語言更像一種助記符,這些人們容易記住的每一條助記符都映射着一條不容易記住的由 0、1 組成的機器指令。你以爲像不像域名與 IP 地址的關係呢?)。golang
1.1 程序的編譯過程redis
以 C 語言爲例來講,從 hello.c 的源碼文件到 hello 可執行文件,通過編譯器處理,大體分爲幾個階段:
segmentfault
編譯器在不一樣的階段會作不一樣的事情,可是有一步是能夠肯定的,那就是:源碼會被編譯成彙編,最後纔是二進制。sass
源碼通過編譯以後,獲得一個二進制的可執行文件
。文件
這兩個字也就代表,目前獲得的這個文件跟其餘文件對比,除了是具備必定的格式(Linux 中是 ELF 格式,即:可運行可連接。executable linkable formate)的二進制組成,並沒什麼區別。
在 Linux 中文件類型大體分爲 7 種:
1. b: 塊設備文件 2. c:字符設備文件 3. d:目錄 4. -:普通文件 5. l:連接 6. s:socket 7. p:管道
經過上面能夠看到,可執行文件 main 與源碼文件 main.go,都是同一種類型,屬於普通文件。(固然了,在 Unix 中有一句很經典的話:一切皆文件
)。
那麼,問題來了:
什麼是進程?
維基百科告訴咱們:程序
是指一組指示計算機或其餘具備消息處理能力設備每一步動做的指令,一般用某種程序設計語言編寫,運行於某種目標體系結構上。
從某個層面來看,能夠把程序分爲靜態程序、動態程序:靜態程序:單純的指具備必定格式的可執行二進制文件。動態程序:則是靜態可執行程序文件被加載到內存以後的一種運行時模型(又稱爲進程)。
首先,要知道的是,進程
是分配系統資源的最小單位,線程
(帶有時間片的函數)是系統調度的最小單位。進程包含線程,線程所屬於進程。
建立進程通常使用 fork 方法(一般會有個拉起程序,先 fork 自身生成一個子進程。而後,在該子進程中經過 exec 函數把對應程序加載進來,進而啓動目標進程。固然,實際上要複雜得多),而建立線程則是使用 pthread 線程庫。
以 32 位 Linux 操做系統爲例,進程經典的虛擬內存結構模型以下圖所示:
其中,有兩處結構是靜態程序所不具備的,那就是運行時堆(heap)
與運行時棧(stack)
。
運行時堆
從低地址向高地址增加,申請的內存空間須要程序員本身或者由 GC 釋放。運行時棧
從高地址向低地址增加,內存空間在當前棧楨調用結束以後自動釋放(並非清除其所佔用內存中數據,而是經過棧頂指針 SP 的移動,來標識哪些內存是正在使用的)。
對於 Go 編譯器而言,其輸出的結果是一種抽象可移植的彙編代碼,這種彙編(Go 的彙編是基於 Plan9 的彙編)並不對應某種真實的硬件架構。Go 的彙編器會使用這種僞彙編,再爲目標硬件生成具體的機器指令。
僞彙編
這一個額外層能夠帶來不少好處,最主要的一點是方便將 Go 移植到新的架構上。
相關的信息能夠參考 Rob Pike
的 The Design of the Go Assembler
。
要了解 Go 的彙編器最重要的是要知道 Go 的彙編器不是對底層機器的直接表示,即 Go 的彙編器沒有直接使用目標機器的彙編指令。Go 彙編器所用的指令,一部分與目標機器的指令一一對應,而另一部分則不是。這是由於編譯器套件不須要彙編器直接參與常規的編譯過程。 相反,編譯器使用了一種半抽象的指令集,而且部分指令是在代碼生成後才被選擇的。彙編器基於這種半抽象的形式工做,因此雖然你看到的是一條 MOV 指令,可是工具鏈針對對這條指令實際生成可能徹底不是一個移動指令,也許會是清除或者加載。也有可能精確的對應目標平臺上同名的指令。歸納來講,特定於機器的指令會以他們的本尊出現, 然而對於一些通用的操做,如內存的移動以及子程序的調用以及返回一般都作了抽象。細節因架構不一樣而不同,咱們對這樣的不精確性表示歉意,狀況並不明確。 彙編器程序的工做是對這樣半抽象指令集進行解析並將其轉變爲能夠輸入到連接器的指令。 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
模式,被調用函數的入參參數、返回值都由調用者維護、準備。所以,當須要調用一個函數時,須要先將這些工做準備好,才調用下一個函數,另外這些都須要進行內存對齊,對齊的大小是 sizeof(uintptr)。
在深刻了解 Go 彙編以前,須要知道的幾個概念:
被調者:callee,好比:A 函數調用了 B 函數,那麼 B 就是被調者
go 彙編中有 4 個核心的僞寄存器,這 4 個寄存器是編譯器用來維護上下文、特殊標識等做用的:
寄存器 | 說明 |
---|---|
SB(Static base pointer) | global symbols |
FP(Frame pointer) | arguments and locals |
PC(Program counter) | jumps and branches |
SP(Stack pointer) | top of stack |
symbol+offset(FP)
的方式,引用 callee 函數的入參參數。例如 arg0+0(FP),arg1+8(FP)
,使用 FP 必須加 symbol ,不然沒法經過編譯(從彙編層面來看,symbol 沒有什麼用,加 symbol 主要是爲了提高代碼可讀性)。另外,須要注意的是:每每在編寫 go 彙編代碼時,要站在 callee 的角度來看(FP),在 callee 看來,(FP)指向的是 caller 調用 callee 時傳遞的第一個參數的位置。假如當前的 callee 函數是 add,在 add 的代碼中引用 FP,該 FP 指向的位置不在 callee 的 stack frame 以內。而是在 caller 的 stack frame 上,指向調用 add 函數時傳遞的第一個參數的位置,常常在 callee 中用symbol+offset(FP)
來獲取入參的參數值。務必注意
:對於編譯輸出(go tool compile -S / go tool objdump)的代碼來說,全部的 SP 都是硬件 SP 寄存器,不管是否帶 symbol(這一點很是具備迷惑性,須要慢慢理解。每每在分析編譯輸出的彙編時,看到的就是硬件 SP 寄存器)。下圖描述了棧楨與各個寄存器的內存關係模型,值得注意的是要站在 callee 的角度來看
有一點須要注意的是,return addr 也是在 caller 的棧上的,不過往棧上插 return addr 的過程是由 CALL 指令完成的(在分析彙編時,是看不到關於 addr 相關空間信息的。在分配棧空間時,addr 所佔用空間大小不包含在棧幀大小內)。
在 AMD64 環境,僞 PC 寄存器實際上是 IP 指令計數器寄存器的別名。僞 FP 寄存器對應的是 caller 函數的幀指針,通常用來訪問 callee 函數的入參參數和返回值。僞 SP 棧指針對應的是當前 callee 函數棧幀的底部(不包括參數和返回值部分),通常用於定位局部變量。僞 SP 是一個比較特殊的寄存器,由於還存在一個同名的 SP 真寄存器,真 SP 寄存器對應的是棧的頂部。
在編寫 Go 彙編時,當須要區分僞寄存器和真寄存器的時候只須要記住一點:僞寄存器通常須要一個標識符和偏移量爲前綴,若是沒有標識符前綴則是真寄存器。好比(SP)、+8(SP)沒有標識符前綴爲真 SP 寄存器,而 a(SP)、b+8(SP)有標識符爲前綴表示僞寄存器。
咱們這裏對容易混淆的幾點簡單進行說明:
在 plan9 彙編裏還能夠直接使用的 amd64 的通用寄存器,應用代碼層面會用到的通用寄存器主要是: 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 中的對應關係:
下面列出了經常使用的幾個彙編指令(指令後綴Q
說明是 64 位上的彙編指令)
助記符 | 指令種類 | 用途 | 示例 |
---|---|---|---|
MOVQ |
傳送 | 數據傳送 | MOVQ 48, AX // 把 48 傳送到 AX |
LEAQ |
傳送 | 地址傳送 | LEAQ AX, BX // 把 AX 有效地址傳送到 BX |
PUSHQ |
傳送 | 棧壓入 | PUSHQ AX // 將 AX 內容送入棧頂位置 |
POPQ |
傳送 | 棧彈出 | POPQ AX // 彈出棧頂數據後修改棧頂指針 |
ADDQ |
運算 | 相加並賦值 | ADDQ BX, AX // 等價於 AX+=BX |
SUBQ |
運算 | 相減並賦值 | SUBQ BX, AX // 等價於 AX-=BX |
CMPQ |
運算 | 比較大小 | CMPQ SI CX // 比較 SI 和 CX 的大小 |
CALL |
轉移 | 調用函數 | CALL runtime.printnl(SB) // 發起調用 |
JMP |
轉移 | 無條件轉移指令 | JMP 0x0185 //無條件轉至 0x0185 地址處 |
JLS |
轉移 | 條件轉移指令 | JLS 0x0185 //左邊小於右邊,則跳到 0x0185 |
說了那麼多,it is code show time。
對於寫好的 go 源碼,生成對應的 Go 彙編,大概有下面幾種
go build -gcflags "-N -l" main.go
生成對應的可執行二進制文件 再使用 go tool objdump -s "main." main
反編譯獲取對應的彙編反編譯時"main."
表示只輸出 main 包中相關的彙編"main.main"
則表示只輸出 main 包中 main 方法相關的彙編
go tool compile -S -N -l main.go
這種方式直接輸出彙編方法 3 使用go build -gcflags="-N -l -S" main.go
直接輸出彙編
注意:在使用這些命令時,加上對應的 flag,不然某些邏輯會被編譯器優化掉,而看不到對應完整的彙編代碼
-l 禁止內聯 -N 編譯時,禁止優化 -S 輸出彙編代碼
go 示例代碼
package main func add(a, b int) int{ sum := 0 // 不設置該局部變量sum,add棧空間大小會是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棧空間 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 ;;取參數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) ;;設置返回值 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棧空間 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入參:1 0x0025 00037 (main.go:10) MOVQ $2, 8(SP) ;;add入參:2 0x002e 00046 (main.go:10) CALL "".add(SB) ;;調用add函數 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棧空間 0x0063 00099 (main.go:11) RET ......
這裏列舉了一個簡單的 int 類型加法
示例,實際開發中會遇到各類參數類型,要複雜的多,這裏只是拋磚引玉 :)
針對 4.2 輸出彙編,對重要核心代碼進行分析。
TEXT "".add(SB), NOSPLIT|ABIInternal, $16-24
TEXT "".add
TEXT 指令聲明瞭 "".add
是 .text 代碼段的一部分,並代表跟在這個聲明後的是函數的函數體。在連接期,""
這個空字符會被替換爲當前的包名: 也就是說,"".add
在連接到二進制文件後會變成 main.add(SB)
SB 是一個虛擬的僞寄存器,保存靜態基地址(static-base) 指針,即咱們程序地址空間的開始地址。"".add(SB)
代表咱們的符號位於某個固定的相對地址空間起始處的偏移位置 (最終是由連接器計算獲得的)。換句話來說,它有一個直接的絕對地址: 是一個全局的函數符號。
NOSPLIT:
向編譯器代表不該該插入 stack-split 的用來檢查棧須要擴張的前導指令。在咱們 add 函數的這種狀況下,編譯器本身幫咱們插入了這個標記: 它足夠聰明地意識到,因爲 add 沒有任何局部變量且沒有它本身的棧幀,因此必定不會超出當前的棧。否則,每次調用函數時,在這裏執行棧檢查就是徹底浪費 CPU 時間了。
$0-16
24 指定了調用方傳入的參數+返回值大小(24 字節=入參 a、b 大小8字節*2
+返回值8字節
)
一般來說,幀大小後通常都跟隨着一個參數大小,用減號分隔。(這不是一個減法操做,只是一種特殊的語法) 幀大小 $24-8 意味着這個函數有 24 個字節的幀以及 8 個字節的參數,位於調用者的幀上。若是 NOSPLIT 沒有在 TEXT 中指定,則必須提供參數大小。對於 Go 原型的彙編函數,go vet 會檢查參數大小是否正確。 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, SP
SP 爲棧頂指針,該語句等價於 SP-=16(因爲棧空間是向下增加的,因此開闢棧空間時爲減操做),表示生成 16 字節大小的棧空間。MOVQ $0, "".~r2+40(SP)
此時的 SP 爲 add 函數棧的棧頂指針,40(SP)的位置則是 add 返回值的位置,該位置位於 main 函數棧空間內。該語句設置返回值類型的 0 值,即初始化返回值,防止獲得髒數據(返回值類型爲 int,int 的 0 值爲 0)。MOVQ "".a+24(SP), AX
從 main 函數棧空間獲取入參 a 的值,存到寄存器 AXADDQ "".b+32(SP), AX
從 main 函數棧空間獲取入參 b 的值,與寄存器 AX 中存儲的 a 值相加,結果存到 AX。至關於 AX=a+bMOVQ AX, "".~r2+40(SP)
把 a+b 的結果放到 main 函數棧中, add(a+b)返回值所在的位置ADDQ $16, SP
歸還 add 函數佔用的棧空間
根據 4.2 對應彙編繪製的函數棧楨結構模型
還記得前面提到的,Go 彙編使用的是caller-save
模式,被調用函數的參數、返回值、棧位置都須要由調用者維護、準備嗎?
在函數棧楨結構中能夠看到,add()函數的入參以及返回值都由調用者 main()函數維護。也正是由於如此,GO 有了其餘語言不具備的,支持多個返回值的特性。
這裏重點講一下函數聲明、變量聲明。
來看一個典型的 Go 彙編函數定義
// func add(a, b int) int // 該add函數聲明定義在同一個 package name 下的任意 .go文件中 // 只有函數頭,沒有實現 // add函數的Go彙編實現 // pkgname 默認是 "" TEXT pkgname·add(SB), NOSPLIT, $16-24 MOVQ a+0(FP), AX ADDQ b+8(FP), AX MOVQ AX, ret+16(FP) RET
Go 彙編實現爲何是 TEXT
開頭?仔細觀察上面的進程內存佈局圖就會發現,咱們的代碼在是存儲在.text 段中的,這裏也就是一種約定俗成的起名方式。實際上在 plan9 中 TEXT 是一個指令,用來定義一個函數。
定義中的 pkgname 是能夠省略的,(非想寫也能夠寫上,不過寫上 pkgname 的話,在重命名 package 以後還須要改代碼,默認爲""
) 編譯器會在連接期自動加上所屬的包名稱。
中點 ·
比較特殊,是一個 unicode 的中點,該點在 mac 下的輸入方法是 option+shift+9。在程序被連接以後,全部的中點·
都會被替換爲句號.
,好比你的方法是runtime·main
,在編譯以後的程序裏的符號則是runtime.main
。
簡單總結一下, Go 彙編實現函數聲明,格式爲:
靜態基地址(static-base) 指針 | | add函數入參+返回值總大小 | | TEXT pkgname·add(SB),NOSPLIT,$16-24 | | | 函數所屬包名 函數名 add函數棧幀大小
"".add(SB)
代表咱們的符號位於某個固定的相對地址空間起始處的偏移位置 (最終是由連接器計算獲得的)。換句話來說,它有一個直接的絕對地址: 是一個全局的函數符號。彙編裏的全局變量,通常是存儲在.rodata
或者.data
段中。對應到 Go 代碼,就是已初始化過的全局的 const、var 變量/常量。
使用 DATA 結合 GLOBL 來定義一個變量。
DATA 的用法爲:
DATA symbol+offset(SB)/width, value
大多數參數都是字面意思,不過這個 offset 須要注意:其含義是該值相對於符號 symbol 的偏移,而不是相對於全局某個地址的偏移。
GLOBL 彙編指令用於定義名爲 symbol 的全局變量,變量對應的內存寬度爲 width,內存寬度部分必須用常量初始化。
GLOBL ·symbol(SB), width
下面是定義了多個變量的例子:
DATA ·age+0(SB)/4, $8 ;; 數值8爲 4字節 GLOBL ·age(SB), RODATA, $4 DATA ·pi+0(SB)/8, $3.1415926 ;; 數值3.1415926爲float64, 8字節 GLOBL ·pi(SB), RODATA, $8 DATA ·year+0(SB)/4, $2020 ;; 數值2020爲 4字節 GLOBL ·year(SB), RODATA, $4 ;; 變量hello 使用2個DATA來定義 DATA ·hello+0(SB)/8, $"hello my" ;; `hello my` 共8個字節 DATA ·hello+8(SB)/8, $" world" ;; ` world` 共8個字節(3個空格) GLOBL ·hello(SB), RODATA, $16 ;; `hello my world` 共16個字節 DATA ·hello<>+0(SB)/8, $"hello my" ;; `hello my` 共8個字節 DATA ·hello<>+8(SB)/8, $" world" ;; ` world` 共8個字節(3個空格) GLOBL ·hello<>(SB), RODATA, $16 ;; `hello my world` 共16個字節
大部分都比較好理解,不過這裏引入了新的標記<>
,這個跟在符號名以後,表示該全局變量只在當前文件中生效,相似於 C 語言中的 static。若是在另外文件中引用該變量的話,會報 relocation target not found 的錯誤。
在 Go 源碼中會看到一些彙編寫的代碼,這些代碼跟其餘 go 代碼一塊兒組成了整個 go 的底層功能實現。下面,咱們經過一個簡單的 Go 彙編代碼示例來實現兩數相加功能。
Go 代碼
package main func add(a, b int64) int64 func main(){ println(add(2,3)) }
Go 源碼中 add()函數只有函數簽名,沒有具體的實現(使用 GO 彙編實現)
使用 Go 彙編實現的 add()函數
TEXT ·add(SB), $0-24 ;; add棧空間爲0,入參+返回值大小=24字節 MOVQ x+0(FP), AX ;; 從main中取參數:2 ADDQ y+8(FP), AX ;; 從main中取參數:3 MOVQ AX, ret+16(FP) ;; 保存結果到返回值 RET
把 Go 源碼與 Go 彙編編譯到一塊兒(我這裏,這兩個文件在同一個目錄)
go build -gcflags "-N -l" .
我這裏目錄爲 demo1,因此獲得可執行程序 demo1,運行獲得結果:5
對 5.1 中獲得的可執行程序 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) ;;參數值 2 main.go:6 0x4581f5 48c744240803000000 MOVQ $0x3, 0x8(SP) ;;參數值 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 ;; 獲取第一個參數 add_amd64.s:3 0x458245 4803442410 ADDQ 0x10(SP), AX ;;參數a+參數b add_amd64.s:5 0x45824a 4889442418 MOVQ AX, 0x18(SP) ;;保存計算結果 add_amd64.s:7 0x45824f c3 RET
經過上面操做,可知:
這裏推薦 2 個 Go 代碼調試工具。
測試代碼
package main type 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 調試
gdb main GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-80.el7 Copyright (C) 2013 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later http://gnu.org/licenses/gpl.html This 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+b 16 } 17 18 func (*data) sub(a, b int) int{ 19 return a-b 20 } 21 22 23 func main(){ (gdb) list 24 var t Ier = &data{3,4} 25 26 println(t.add(1,2)) 27 println(t.sub(3,2)) 28 } 29 (gdb) b 26 // 在源碼26行處設置斷點 Breakpoint 1 at 0x45827c: file /root/go/src/interface/main.go, line 26. (gdb) r Starting program: /root/go/src/interface/main Breakpoint 1, main.main () at /root/go/src/interface/main.go:26 26 println(t.add(1,2)) (gdb) info locals // 顯示變量 t = {tab = 0x487020 <data,main.Ier>, data = 0xc000096000} (gdb) ptype t // 打印t的結構 type = struct runtime.iface { runtime.itab *tab; void *data; } (gdb) p *t.tab.inter // 打印t.tab.inter指針指向的數據 $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 "\002\003\004\005\006\a\b\t\n\f\r\016\017\020\022\025\026\030\033\034\036\037\"&(,-5<BUXx\216\231\330\335\377", 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 調試命令
disass
除了 gdb,另外推薦一款 gdb 的加強版調試工具 cgdb
https://cgdb.github.io/
效果以下圖所示,分兩個窗口:上面顯示源代碼,下面是具體的命令行調試界面(跟 gdb 同樣):
delve 項目地址
https://github.com/go-delve/d...
帶圖形化界面的 dlv 項目地址
https://github.com/aarzilli/gdlv
dlv 的安裝使用,這裏再也不作過多講解,感興趣的能夠嘗試一下。
對於 Go 彙編基礎大體須要熟悉下面幾個方面:
經過上面的例子相信已經讓你對 Go 的彙編有了必定的理解。固然,對於大部分業務開發人員來講,只要看的懂便可。若是想進一步的瞭解,能夠閱讀相關的資料或者書籍。
最後想說的是:鑑於我的能力有限,在閱讀過程當中你可能會發現存在的一些問題或者缺陷,歡迎各位大佬指正。若是感興趣的話,也能夠一塊兒私下交流。
8. 參考資料
在整理的過程當中,部分參考、引用下面連接地址內容。有一些寫的仍是不錯的,感興趣的同窗能夠閱讀。
[1] https://github.com/cch123/gol... plan9 assembly
[2] https://segmentfault.com/a/11... 彙編入門
[3] https://www.davidwong.fr/goasm/ Go Assembly by Example
[4] https://juejin.im/post/684490...
[5] https://github.com/go-interna...