go語言調度器源代碼情景分析之六:go匯編語言
go語言runtime(包括調度器)源代碼中有部分代碼是用匯編語言編寫的,不過這些匯編代碼並非針對特定體系結構的匯編代碼,而是go語言引入的一種偽匯編,它同樣也需要經過匯編器轉換成機器指令才能被CPU執行。需要註意的是,用go匯編語言編寫的代碼一旦經過匯編器轉換成機器指令之後,再用調試工具反匯編出來的代碼已經不是go語言匯編代碼了,而是跟平臺相關的匯編代碼。
go匯編格式跟前面討論過的AT&T匯編基本上差不多,但也有些重要區別,本節就這些差異做一個簡單說明。
寄存器
go匯編語言中使用的寄存器的名字與AMD64不太一樣,下表顯示了它們之間的對應關系:
除了這些跟AMD64 CPU硬件寄存器一一對應的寄存器外,go匯編還引入了幾個沒有任何硬件寄存器與之對應的虛擬寄存器
下面重點介紹在go匯編中常見的2個虛擬寄存器的使用方法:
FP虛擬寄存器:主要用來引用函數參數。go語言規定函數調用時參數都必須放在棧上,比如被調用函數使用 first_arg+0(FP) 來引用調用者傳遞進來的第一個參數,用second_arg+8(FP)來引用第二個參數 ,以此類推,這裏的first_arg和second_arg僅僅是一個幫助我們閱讀源代碼的符號,對編譯器來說無實際意義,+0和+8表示相對於FP寄存器的偏移量。我們用一個runtime中的函數片段作為例子來看看FP的使用。
go runtime中有一個叫gogo的函數,它接受一個gobuf類型的指針
// func gogo(buf *gobuf) // restore state from Gobuf; longjmp TEXT runtime·gogo(SB), NOSPLIT, $16-8 MOVQbuf+0(FP), BX// gobuf -->bx ......
MOVQ buf+0(FP), BX 這一條指令把調用者傳遞進來的指針buf放入BX寄存器中,可以看到,在gogo函數是通過buf+0(FP)這種方式獲取到參數的。從被調用函數(此處為gogo函數)的角度來看,FP與函數棧幀之間的關系如下圖,可以看出FP寄存器指向調用者的棧幀,而不是被調用函數的棧幀。
SB虛擬寄存器:保存程序地址空間的起始地址。還記得在函數調用棧一節我們看過的進程在內存中的布局那張圖嗎,這個SB寄存器保存的值就是代碼區的起始地址,它主要用來定位全局符號。go匯編中的函數定義、函數調用、全局變量定義以及對其引用會用到這個SB虛擬寄存器。對於這個虛擬寄存器,我們不用過多的關註,在代碼中看到它時知道它是一個虛擬寄存器就行了。
操作碼
AT&T格式的寄存器操作碼一般使用小寫且寄存器的名字前面有個%符號,而go匯編使用大寫而且寄存器名字前沒有%符號,比如:
#AT&T格式 mov %rbp,%rsp #go匯編格式 MOVQ BP,SP
操作數寬度(即操作數的位數)
AT&T格式的匯編指令中如果有寄存器操作數,則根據寄存器的名字(比如rax, eax, ax, al分別代表64,32,16和8位寄存器)就可以確定操作數到底是多少位(8,16,32還是64位),所以不需要操作碼後綴,如果沒有寄存器操作數又是訪存指令的話,則操作碼需要加上後綴b、w、l或q來指定到底存取內存中的多少個字節。
而go匯編中,寄存器的名字沒有位數之分,比如AX寄存器沒有什麽RAX, EAX之類的名字,指令中一律只能使用AX。所以如果指令中有操作數寄存器或是指令需要訪問內存,則操作碼都需要帶上後綴B(8位)、W(16位)、D(32位)或Q(64位)。
函數定義
還是以go runtime中的gogo函數為例:
// func gogo(buf *gobuf) // restore state from Gobuf; longjmp TEXT runtime·gogo(SB), NOSPLIT, $16-8 ......
下面對這個函數定義的第一行的各部分做個說明:
TEXT runtime·gogo(SB):指明在代碼區定義了一個名字叫gogo的全局函數(符號),該函數屬於runtime包。
NOSPLIT:指示編譯器不要在這個函數中插入檢查棧是否溢出的代碼。
$16-8:數字16說明此函數的棧幀大小為16字節,8說明此函數的參數和返回值一共需要占用8字節內存。因為這裏的gogo函數沒有返回值,只有一個指針參數,對於AMD64平臺來說指針就是8字節。go語言中函數調用的參數和函數返回值都是放在棧上的,而且這部分棧內存是由調用者而非被調用函數負責預留,所以在函數定義時需要說明到底需要在調用者的棧幀中預留多少空間。
go匯編還有一些用法比較特別的地方,現在不討論,等我們分析源代碼遇到它們時再結合上下文做詳細說明。
go語言調度器源代碼情景分析之六:go匯編語言