在 JVM 中,字節碼能夠幫咱們搞清楚不少編譯執行的細節, 爲了搞清楚 go 語言底層的語法糖和原理,須要對底層的彙編知識有深刻的瞭解。彙編其實沒有想象中那麼複雜,其實原理上來講跟 Java 字節碼差很少,只是資料不多,由於更接近系統底層,閱讀的難度相對而言更大一些。linux
首先是要破除迷信,同一個問題網上的答案衆說紛紜,好比究竟是傳值仍是傳引用爭論不休,不如靜下心看一下彙編來的踏實。git
下面寫的這些東西不必定都對,可是但願能與你分享一些方法和思路,授之以漁。學習的目的不是掌握這個知識,而是掌握學習知識的方法,觸類旁通,舉一反三,無論學什麼都有本身的一套方法支撐,快速如何,快速解決問題,長遠來看知識自己是沒什麼太大做用的。github
學習 Go 語言彙編不是爲了之後用匯編來作開發,只是能夠用經過閱讀彙編來深入的理解 Go 語言背後的實現細節,真正的精通這門語言,在使用的過程當中能夠更加安心。編程
這篇文章將會首先介紹在 Linux 平臺上用匯編輸出 "Hello, World!",經過這個例子順帶介紹彙編的一些基本的概念。爲後面咱們介紹 Go 語言 Plan9 彙編打下基礎。緩存
以前看了很多的彙編的書,有一個感受是,咋沒有跟其它編程書籍同樣,介紹如何輸出 "Hello, World!" 呢?看得多之後就慢慢知道了,用匯編在控制檯輸出 "Hello, World!" 沒有那麼簡單,不是三兩行簡單調用一個函數就完了。bash
爲了搞清楚如何在終端中輸出字符串,咱們先來寫一段 C 語言的實現:app
#include <stdio.h>
int main() {
char *str = "Hello, World!\n";
printf("%s", str);
}
複製代碼
更接近系統調用層的寫法是:函數
#include <unistd.h>
int main() {
int stdout_fd = 1;
char* str = "Hello, World!\n";
int length = 14;
write(stdout_fd, str, length);
}
複製代碼
Unix 的設計哲學,一切皆文件,一個程序運行之後都至少包含三個文件描述符(file descriptor,簡稱 fd):學習
在終端執行程序輸出字符串,實際上就是往標準輸出 stdout 文件描述符寫數據,stdout 的 fd 值等於 1。ui
write 是一個系統調用,把數據寫入到文件,它的函數簽名以下:
ssize_t write(int fd, void * buffer, size_t count)
複製代碼
第一個參數 fd 表示要寫入的文件描述符,第二個參數 buffer 表示要寫入文件中數據的內存地址,第三個參數表示從 buffer 寫入文件的數據字節數。所以在標準輸出中輸出"Hello, World!\n"其實是調用 write 系統調用,往 fd 爲 1 的文件描述符寫入 14 個字節的字符串。
編譯並執行上面的 C 代碼,就能夠看到輸出了 "Hello, World!" 字符串
gcc main.c -o main
./main
Hello, World!
複製代碼
彙編主要是跟 CPU 和內存打交道,CPU 自己只負責運算,不負責存儲,數據存儲通常都是放在內存中,咱們知道 CPU 的運算速度遠高於內存的讀寫速度,爲了 CPU 不被內存讀寫拖後腿,CPU 內部引入一級緩存、二級緩存和寄存器的概念,這些資源都很是寶貴,至今都記得有一位老師說過:「二級緩存貴如黃金」。寄存器能夠認爲是在 CPU 內能夠存儲很是少許數據的超高速的存儲單元。由於寄存器個數有限且很是重要,每一個寄存器都有本身的名字,最經常使用的有下面這些,這些先混個眼熟,在後續的文章中再詳細介紹。
%EAX %EBX %ECX %EDX %EDI %ESI %EBP %ESP
複製代碼
下面咱們來介紹系統調用概念,不少人會想,這還不簡單,我一天能夠寫幾百個系統調用。
內核對外暴露的接口被稱爲系統調用,應用程序能夠調用對應的接口請求內核去完成某些動做,咱們常見的建立新進程、IO 讀寫等都屬於系統調用。
須要注意一下這些知識:
%eax
中int 0x80
指令用來觸發處理器從用戶態切換到內核態,int 是 interrupt(中斷)的縮寫,不是整數的那個 int。內核收到 0x80 的中斷請求之後,就會並根據前面準備好的寄存器的內容調用相應的系統調用。執行一個 write 調用的流程以下圖所示:
有了上面的基礎,再來看彙編的代碼,但願不要在這裏就勸退了大部分同窗。文件名是 helloworld.s
,下面是彙編的代碼
.section .data
msg:
.ascii "Hello, World!\n"
.section .text
.globl _start
_start:
# write 的第 3個參數 count: 14
movl $14, %edx
# write 的第 2 個參數 buffer: "Hello, World!\n"
movl $msg, %ecx
# write 的第 1 個參數 fd: 1
movl $1, %ebx
# write 系統調用自己的數字標識:4
movl $4, %eax
# 執行系統調用: write(fd, buffer, count)
int $0x80
# status: 0
movl $0, %ebx
# 函數: exit
movl $1, %eax
# system call: exit(status)
int $0x80
複製代碼
在彙編中,任何以點(.)開頭的都不會被直接翻譯爲機器指令,.section
將彙編代碼劃分爲多個段,.section .data
是數據段的開始,數據段中存儲後面程序須要用到的數據,至關於一個全局變量。在數據段中,咱們定義了一個 msg,ascii 編碼表示的內容是 "Hello, World!\n",
接下來的 .section .text
表示是文本段的開始,文本段是存放程序指令的地方。
接下來的指令是 .globl _start
,這裏並無拼錯,不是 global,_start
是一個標籤。接下來是真正的彙編指令部分了。
前面介紹過,執行 write 系統調用時,%eax
寄存器存儲 write 的系統調用號 4,%ebx
存儲標準輸出的 fd,%ecx
存儲着輸出buffer 的地址。%edx
存儲字節數。因此看到 _start
便籤後有四個 movl 指令,movl 指令的格式是:
movl src dst
複製代碼
好比movl $4, %eax
指令是講常量 4 存儲到寄存器 %eax
中,數字 4 前面的 $ 表示「當即尋址」,彙編的其它尋址方式後面的文章還會詳細介紹,這裏先不展開,只須要知道當即尋址是自己就包含要訪問的數據,好比要把數據初始化爲 4,不用去哪一個地址去讀 4,在指令中直接給出數字 4。
接下來指令是 int $0x80
,前面介紹過,這是一條中斷觸發指令,把執行流程交給內核繼續處理,應用程序不用關心內核是如何處理的,內核處理完會把執行流程還給應用程序,同時根據執行成功與否設置全局變量 errno 的值。通常狀況下,在 linux 上系統調用成功會返回非負值,發送錯誤時會返回負值。
接下來的指令實際上執行 exit(0) 退出程序,指令和邏輯與以前的同樣,再也不贅述。
下面來編譯和執行上面的彙編代碼。在 Linux 上,可使用 as 和 ld 彙編和連接程序
as $helloworld.s -o helloworld.o
ld $helloworld.o -o helloworld
執行:
./helloworld
複製代碼
能夠看到輸出了
Hello, World!
複製代碼
剛開始接觸 Go 語言彙編的時候一臉懵逼,這都是些啥,竟然用的是一個歷來沒據說過的操做系統 plan9 所自帶的彙編器語法,不過沒有辦法,技術選型永遠是 leader 和 CTO 說了算。
注意下面的實驗是在 Mac 平臺上,源代碼見:github.com/arthur-zhan…
文件結構以下:
.
├── helloworld
│ ├── helloworld.go
│ └── helloworld.s
├── main.go
複製代碼
main.go 的內容以下,調用了 helloworld.go 中的 PrintMe 方法:
package main
import (
"./helloworld"
)
func main() {
helloworld.PrintMe()
}
複製代碼
helloworld.go 的內容只是聲明瞭一個 PrintMe() 的空函數:
package helloworld
func PrintMe()
複製代碼
具體的實現是在 helloworld.s 這個彙編文件中,內容以下:
#include "textflag.h"
DATA msg<>+0x00(SB)/8, $"Hello, W"
DATA msg<>+0x08(SB)/8, $"orld!\n"
GLOBL msg<>(SB),NOPTR,$16
TEXT ·PrintMe(SB), NOSPLIT, $0
MOVL $(0x2000000+4), AX // write 系統調用數字編號 4
MOVQ $1, DI // 第 1 個參數 fd
LEAQ msg<>(SB), SI // 第 2 個參數 buffer 指針地址
MOVL $16, DX // 第 3 個參數 count
SYSCALL
RET
複製代碼
雖然指令不太同樣,可是總體的彙編代碼邏輯是同樣的,一樣是分了 Data 段、Text 段,一樣是用 mov 等指令給寄存器賦值。下面簡單介紹一下上面的彙編代碼,後面的文章會有更詳細的介紹。
plan9 中使用寄存器不須要帶 r 或 e 的前綴,例如 rax,只要寫 AX 就能夠了。
eax->AX
ebx->BX
ecx->CX
...
複製代碼
Go 彙編引入了四個僞寄存器,這四個僞寄存器很是重要:
Go 彙編語言中 DATA 命令用於初始化變量,語法以下:
DATA symbol+offset(SB)/width, value
複製代碼
好比聲明 msg 這個變量:
DATA msg<>+0x00(SB)/8, $"Hello, W"
複製代碼
下面來看 GLOBL 指令
GLOBL msg<>(SB),NOPTR,$16
複製代碼
GLOBL 指令將變量聲明爲 global,後面須要跟兩個參數,flag 和變量的大小,這的 NOPTR 不影響後面的閱讀,這裏先不作介紹。
注意到 msg 後面有一個<>
,這表示這個全局變量只在當前文件中能夠被訪問,相似於 C 語言中的 static。
函數定義的語法以下:
TEXT symbol(SB), [flags,] $framesize[-argsize]
複製代碼
分爲 5 個組成部分:TEXT 指令、函數名、可選的 flags 標誌、函數幀大小和可選的函數參數大小
以例子中的彙編代碼爲例:
TEXT ·PrintMe(SB), NOSPLIT, $0
複製代碼
·
」,不知道當初設計這個的人是有一種什麼樣的癖好,😁。這個中點在編譯之後會被替換爲.
,同時也會加上包名,好比這裏的 helloworld.PrintMe
接下來的就是具體的函數體的內容。
MOVL $(0x2000000+4)中的 0x2000000 是什麼鬼?Mac 下的系統調用數字編號須要加 0x2000000,不要問爲何,問就是系統約定。Mac 下的系統調用編號能夠在這裏查:opensource.apple.com/source/xnu/…
與前面介紹的 Linux 下的彙編稍有不一樣,Mac 下的系統調用參數須要存儲在 DI、SI、DX 等寄存器中,系統調用編號存儲在 AX 中。
Go 的 HelloWorld 彙編入門就先介紹到這裏。但願對你有所幫助
這篇文章做爲 Go 語言彙編的入門,由於篇幅有限,沒有很是細緻的展開每個細節,在後面的系列文章中,咱們會繼續結合案例進行介紹。
能夠掃描下面的二維碼關注個人公衆號: