Go語(yǔ)言 調(diào)用匯編和C

2018-07-25 17:23 更新

只要不使用C的標(biāo)準(zhǔn)庫(kù)函數(shù),Go中是可以直接調(diào)用C和匯編語(yǔ)言的。其實(shí)道理很簡(jiǎn)單,Go的運(yùn)行時(shí)庫(kù)就是用C和匯編實(shí)現(xiàn)的,Go必須是能夠調(diào)用到它們的。當(dāng)然,會(huì)有一些額外的約束,這就是函數(shù)調(diào)用協(xié)議。

Go中調(diào)用匯編

假設(shè)我們做一個(gè)匯編版本的加法函數(shù)。首先GOPATH的src下新建一個(gè)add目錄,然后在該目錄加入add.go的文件,內(nèi)容如下:

package add

func Add(a, b uint64) uint64 {
    return a+b
}

這個(gè)函數(shù)將兩個(gè)uint64的數(shù)字相加,并返回結(jié)果。我們寫(xiě)一個(gè)簡(jiǎn)單的函數(shù)調(diào)用它,內(nèi)容如下:

package main

import (
  "fmt"
  "add"
)

func main() {
     fmt.Println(add.Add(2, 15))
}

可以看到輸出了結(jié)果為17。好的,接下來(lái)讓我們刪除Add函數(shù)的實(shí)現(xiàn),只留下定義部分:

package add

func Add(a, b uint64) uint64

然后在add.go同一目錄中建立一個(gè)add_amd64.s的文件(假設(shè)你使用的是64位系統(tǒng)),內(nèi)容如下:

TEXT    ·Add+0(SB),$0-24
MOVQ    a+0(FP),BX
MOVQ    b+8(FP),BP
ADDQ    BP,BX
MOVQ    BX,res+16(FP)
RET     ,

雖然匯編是相當(dāng)難理解的,但我相信讀懂上面這段不會(huì)有困難。前兩條MOVQ指令分別將第一個(gè)參數(shù)放到寄存器BX,第二個(gè)參數(shù)放到寄存器BP,然后ADDQ指令將兩者相加后,最后的MOVQ和RET指令返回結(jié)果。

現(xiàn)在,再次運(yùn)行前面的main函數(shù),它將使用自定義的匯編版本函數(shù),可以看到成功的輸出了結(jié)果17。從這個(gè)例子中可以看出Go是可以直接調(diào)用匯編實(shí)現(xiàn)的函數(shù)的。大多時(shí)候不必要你去寫(xiě)匯編,即使是研究Go的內(nèi)部實(shí)現(xiàn),能讀懂匯編已經(jīng)很足夠了。

也許你真的覺(jué)得在Go中寫(xiě)匯編很酷,但是不要忽視了這些忠告:

  • 匯編很難編寫(xiě),特別是很難寫(xiě)好。通常編譯器會(huì)比你寫(xiě)出更快的代碼。
  • 匯編僅能運(yùn)行在一個(gè)平臺(tái)上。在這個(gè)例子中,代碼僅能運(yùn)行在 amd64 上。這個(gè)問(wèn)題有一個(gè)解決方案是給 Go 對(duì)于 x86 和 不同版本的代碼分別寫(xiě)一套代碼,文件名相應(yīng)的以_386.s和_arm.s結(jié)尾。
  • 匯編讓你和底層綁定在一起,而標(biāo)準(zhǔn)的 Go 不會(huì)。例如,slice 的長(zhǎng)度當(dāng)前是 32 位整數(shù)。但是也不是不可能為長(zhǎng)整型。當(dāng)發(fā)生這些變化時(shí),這些代碼就被破壞了。

當(dāng)前Go編譯器不能將匯編編譯為函數(shù)的內(nèi)聯(lián),但是對(duì)于小的Go函數(shù)是可以的。因此使用匯編可能意味著讓你的程序更慢。

有時(shí)需要匯編給你帶來(lái)一些力量(不論是性能方面的原因,還是一些相當(dāng)特殊的關(guān)于CPU的操作)。對(duì)于什么時(shí)候應(yīng)該使用它,Go源碼包括了若干相當(dāng)好的例子(可以看看 crypto 和 math)。由于它非常容易實(shí)踐,所以這絕對(duì)是個(gè)學(xué)習(xí)匯編的好途徑。

Go中調(diào)用C

接下來(lái),我們繼續(xù)嘗試在Go中調(diào)用C,跟調(diào)用匯編的過(guò)程很類(lèi)似。首先刪掉前面的add_amd64.s文件,并確保add.go文件中只是給出了Add函數(shù)的聲明部分:

package add

func Add(a, b uint64) uint64

然后在add.go同目錄中,新建一個(gè)add.c文件,內(nèi)容如下:

#include "runtime.h"

void ·Add(uint64 a, uint64 b, uint64 ret) {
    ret = a + b;
    FLUSH(&ret);
}

編譯該包,運(yùn)行前面的測(cè)試函數(shù):

go install add

會(huì)發(fā)現(xiàn)輸出結(jié)果為17,說(shuō)明Go中成功地調(diào)用到了C寫(xiě)的函數(shù)。

要注意的是不管是C或是匯編實(shí)現(xiàn)的函數(shù),其函數(shù)名都是以·開(kāi)頭的。還有,C文件中需要包含runtime.h頭文件。這個(gè)原因在該文件中有說(shuō)明: Go用了特殊寄存器來(lái)存放像全局的struct G和struct M。包含這個(gè)頭文件可以讓所有鏈接到Go的C文件都知道這一點(diǎn),這樣編譯器可以避免使用這些特定的寄存器作其它用途。

讓我們仔細(xì)看一下這個(gè)C實(shí)現(xiàn)的函數(shù)??梢钥吹胶瘮?shù)的返回值為空,而參數(shù)多了一個(gè),第三個(gè)參數(shù)實(shí)際上被作為了返回值使用。其中FLUSH是在pkg/runtime/runtime.h中定義為USED(x),這個(gè)定義是Go的C編譯器自帶的primitive,作用是抑制編譯器優(yōu)化掉對(duì)*x的賦值的。如果你很好奇USED是怎樣定義的,可以去$GOROOT/include/libc.h文件里去找找。

被調(diào)函數(shù)中對(duì)參數(shù)ret的修改居然返回到了調(diào)用函數(shù),這個(gè)看起來(lái)似乎不可理解,不過(guò)早期的C編譯器確實(shí)是可以這么做的。

函數(shù)調(diào)用時(shí)的內(nèi)存布局

Go中使用的C編譯器其實(shí)是plan9的C編譯器,和我們平時(shí)理解的gcc等會(huì)有一些區(qū)別。我們將上面的add.c匯編一下:

go tool 6c -I $GOROOT/src/pkg/runtime -S add.c

生成的匯編代碼大概是這個(gè)樣子的:

"".Add t=1 size=16 value=0 args=0x18 locals=0
000000 00000 (add.c:3)    TEXT    "".Add+0(SB),4,$0-24
000000 00000 (add.c:3)    NOP    ,
000000 00000 (add.c:3)    NOP    ,
000000 00000 (add.c:3)    FUNCDATA    $2,gcargs.0<>+0(SB)
000000 00000 (add.c:3)    FUNCDATA    $3,gclocals.1<>+0(SB)
000000 00000 (add.c:4)    MOVQ    a+8(FP),AX
0x0005 00005 (add.c:4)    ADDQ    b+16(FP),AX
0x000a 00010 (add.c:4)    MOVQ    AX,c+24(FP)
0x000f 00015 (add.c:5)    RET    ,
000000 48 8b 44 24 08 48 03 44 24 10 48 89 44 24 18 c3  H.D$.H.D$.H.D$..

這是Go使用的匯編代碼,是一種類(lèi)似plan9的匯編代碼。類(lèi)似a+8(FP)這種表示的含義是“變量名+偏移(寄存器)”。其中FP是幀寄存器,它是一個(gè)偽寄存器,實(shí)際上是內(nèi)存位置的一個(gè)引用,其實(shí)就是BP(?;芳拇嫫?上移一個(gè)機(jī)器字長(zhǎng)位置的內(nèi)存地址。

函數(shù)調(diào)用之前,a+8(FP),b+16(FP)分別表示參數(shù)a和b,而參數(shù)3的位置被空著,在被調(diào)函數(shù)中,這個(gè)位置將用于存放返回值。此時(shí)的其內(nèi)存布局如下所示:

參數(shù)3
參數(shù)2
參數(shù)1  <-SP 

進(jìn)入被調(diào)函數(shù)之后,內(nèi)存布局如下所示:

參數(shù)3
參數(shù)2
參數(shù)1  <-FP
保存PC <-SP
...
...

CALL指令會(huì)使得SP下移,SP位置的內(nèi)存用于保存返回地址。幀寄存器FP此時(shí)位置在SP上面。在plan9匯編中,進(jìn)入函數(shù)之后的前幾條指令并沒(méi)有出現(xiàn)push ebp; mov esp ebp這種模式。plan9函數(shù)調(diào)用協(xié)議中采用的是caller-save的模式,也就是由調(diào)用者負(fù)責(zé)保存寄存器。注意這和傳統(tǒng)的C是不同的。傳統(tǒng)C中是callee-save的模式,被調(diào)函數(shù)要負(fù)責(zé)保存它想使用的寄存器,在函數(shù)退出時(shí)恢復(fù)這些寄存器。

需要注意的是參數(shù)和返回值都是有對(duì)齊的。這里是按Structrnd對(duì)齊的,Structrnd在源代碼中義為sizeof(uintptr)。

links


以上內(nèi)容是否對(duì)您有幫助:
在線(xiàn)筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)