Golang內部構件,第5部分:運行時引導程序

引導過程是瞭解Go運行時如何工做的關鍵。若是您想繼續使用Go,學習它是必不可少的。所以,咱們的Golang Internals系列的第五部分專門討論Go運行時,尤爲是Go引導過程。此次您將瞭解:html

  • 自舉
  • 可調整大小的堆棧實現
  • 內部TLS實施

請注意,這篇文章包含許多彙編代碼,您至少須要一些基礎知識才能繼續(這裏是Go彙編程序的快速指南)。linux

尋找一個切入點

首先,咱們須要找到啓動Go程序後當即執行的功能。爲此,咱們將編寫一個簡單的Go應用。git

package main

func main() {
    print(123)
}

而後,咱們須要對其進行編譯和連接。github

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

這將6.out在您的當前目錄中建立一個名爲的可執行文件。下一步涉及objdump工具,該工具特定於Linux。Windows和Mac用戶能夠找到相似物或徹底跳過此步驟。如今,運行如下命令。golang

objdump -f 6.out

您應該得到輸出,其中將包含起始地址。算法

6.out:     file format elf64-x86-64
architecture: i386:x86-64, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x000000000042f160

接下來,咱們須要反彙編可執行文件,並找到哪一個函數位於該地址。編程

objdump -d 6.out > disassemble.txt

而後,咱們須要打開disassemble.txt文件並搜索42f160。咱們獲得的輸出以下所示windows

000000000042f160 <_rt0_amd64_linux>:
  42f160:    48 8d 74 24 08               lea    0x8(%rsp),%rsi
  42f165:    48 8b 3c 24                  mov    (%rsp),%rdi
  42f169:    48 8d 05 10 00 00 00     lea    0x10(%rip),%rax        # 42f180 <main>
  42f170:    ff e0                            jmpq   *%rax

很好,咱們找到了!個人操做系統和體系結構的入口點是一個名爲的函數_rt0_amd64_linuxsass

起始順序

如今,咱們須要在Go運行時源中找到此函數。它位於rt0_linux_amd64.s文件中。若是查看Go運行時程序包,則能夠找到許多帶有與操做系統和體系結構名稱相關的後綴的文件名。構建運行時程序包時,僅選擇與當前OS和體系結構相對應的文件。其他的被跳過。讓咱們仔細看一下rt0_linux_amd64.s函數

TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
    LEAQ    8(SP), SI // argv
    MOVQ    0(SP), DI // argc
    MOVQ    $main(SB), AX
    JMP    AX

TEXT main(SB),NOSPLIT,$-8
    MOVQ    $runtime·rt0_go(SB), AX
    JMP    AX

_rt0_amd64_linux功能是很是簡單的。它調用main函數並將參數(argcargv)保存在寄存器(DISI)中。參數位於堆棧中,而且能夠經過SP(堆棧指針)寄存器進行訪問。主要功能也很簡單。它調用的runtime.rt0_go函數更長且更復雜,所以咱們將其分紅小部分並分別描述。第一部分是這樣的。

MOVQ    DI, AX        // argc
MOVQ    SI, BX        // argv
SUBQ    $(4*8+7), SP        // 2args 2auto
ANDQ    $~15, SP
MOVQ    AX, 16(SP)
MOVQ    BX, 24(SP)

在這裏,咱們將一些先前保存的命令行參數值放入AXBX減小堆棧指針。咱們還爲另外兩個四字節變量添加了空間,並將其調整爲16位對齊。最後,咱們將參數移回堆棧。

// create istack out of the given (operating system) stack.
// _cgo_init may update stackguard.
MOVQ    $runtime·g0(SB), DI
LEAQ    (-64*1024+104)(SP), BX
MOVQ    BX, g_stackguard0(DI)
MOVQ    BX, g_stackguard1(DI)
MOVQ    BX, (g_stack+stack_lo)(DI)
MOVQ    SP, (g_stack+stack_hi)(DI)

第二部分比較棘手。首先,咱們將全局runtime.g0變量的地址加載到DI寄存器中。此變量在proc1.go文件中定義,而且屬於該runtime,g類型。將爲goroutine系統中的每一個變量建立此類型的變量。如您所料,runtime.g0 描述了root goroutine。而後,咱們初始化描述root堆棧的字段goroutine。的意義stack.lostack.hi應當明確。這些是指向current的堆棧開始和結束的指針goroutine,可是stackguard0andstackguard1字段是什麼?爲了理解這一點,咱們須要擱置對該runtime.rt0_go函數的研究,並仔細研究Go中的堆棧增加。

Go中可調整大小的堆棧實現

Go語言使用可調整大小的堆棧。每個都goroutine從一個小的堆棧開始,而且每當達到某個閾值時,它的大小就會更改。顯然,有一種方法能夠檢查咱們是否已達到此閾值。實際上,檢查是在每一個功能的開始執行的。爲了瞭解它的工做原理,讓咱們用該-S標誌再編譯一次示例程序(這將顯示生成的彙編代碼)。主要功能的開始看起來像這樣。

"".main t=1 size=48 value=0 args=0x0 locals=0x8
0x0000 00000 (test.go:3)    TEXT    "".main+0(SB),$8-0
0x0000 00000 (test.go:3)    MOVQ    (TLS),CX
0x0009 00009 (test.go:3)    CMPQ    SP,16(CX)
0x000d 00013 (test.go:3)    JHI ,22
0x000f 00015 (test.go:3)    CALL    ,runtime.morestack_noctxt(SB)
0x0014 00020 (test.go:3)    JMP ,0
0x0016 00022 (test.go:3)    SUBQ    $8,SP

首先,咱們將值從線程本地存儲(TLS)加載到CX寄存器(咱們已經在上一篇文章中解釋了TLS是什麼)。此值始終包含一個指向runtime.g與current對應的結構的指針goroutine。而後,咱們將堆棧指針與runtime.g結構中位於16個字節偏移處的值進行比較。咱們能夠輕鬆地計算出這對應於該stackguard0字段。

所以,這就是咱們檢查是否已達到堆棧閾值的方式。若是還沒有達到,則檢查失敗。在這種狀況下,咱們將runtime.morestack_noctxt反覆調用該函數,直到爲堆棧分配了足夠的內存爲止。該stackguard1字段的工做方式與極爲類似stackguard0,可是它在C堆棧增加序言中使用,而不是在Go中使用。的內部運做runtime.morestack_noctxt方式也是一個很是有趣的話題,但咱們將在稍後進行討論。如今,讓咱們回到引導過程。

Go自舉的進一步調查

咱們將經過查看runtime.rt0_go函數中代碼的下一部分來開始啓動序列。

// find out information about the processor we're on
    MOVQ    $0, AX
    CPUID
    CMPQ    AX, $0
    JE    nocpuinfo

    // Figure out how to serialize RDTSC.
    // On Intel processors LFENCE is enough. AMD requires MFENCE.
    // Don't know about the rest, so let's do MFENCE.
    CMPL    BX, $0x756E6547  // "Genu"
    JNE    notintel
    CMPL    DX, $0x49656E69  // "ineI"
    JNE    notintel
    CMPL    CX, $0x6C65746E  // "ntel"
    JNE    notintel
    MOVB    $1, runtime·lfenceBeforeRdtsc(SB)
notintel:

    MOVQ    $1, AX
    CPUID
    MOVL    CX, runtime·cpuid_ecx(SB)
    MOVL    DX, runtime·cpuid_edx(SB)
nocpuinfo:

這部分對於理解Go的主要概念不是相當重要的,所以咱們將對其進行簡要介紹。在這裏,咱們試圖找出正在使用的處理器。若是是Intel,則設置runtime·lfenceBeforeRdtsc變量。該runtime·cputicks方法是惟一使用此變量的地方。此方法利用不一樣的彙編器指令來獲取cpu ticks依賴於的值runtime·lfenceBeforeRdtsc。最後,咱們調用CPUID彙編程序指令,執行該指令,而後將結果保存在runtime·cpuid_ecxruntime·cpuid_edx變量中。這些在alg.go文件中使用,以選擇計算機體系結構自己支持的適當哈希算法。

好的,讓咱們繼續檢查代碼的另外一部分。

// if there is an _cgo_init, call it.
MOVQ    _cgo_init(SB), AX
TESTQ    AX, AX
JZ    needtls
// g0 already in DI
MOVQ    DI, CX    // Win64 uses CX for first parameter
MOVQ    $setg_gcc<>(SB), SI
CALL    AX

// update stackguard after _cgo_init
MOVQ    $runtime·g0(SB), CX
MOVQ    (g_stack+stack_lo)(CX), AX
ADDQ    $const__StackGuard, AX
MOVQ    AX, g_stackguard0(CX)
MOVQ    AX, g_stackguard1(CX)

CMPL    runtime·iswindows(SB), $0
JEQ ok

該片斷僅在cgo啓用時執行。

下一個代碼片斷負責設置TLS。

needtls:
    // skip TLS setup on Plan 9
    CMPL    runtime·isplan9(SB), $1
    JEQ ok
    // skip TLS setup on Solaris
    CMPL    runtime·issolaris(SB), $1
    JEQ ok

    LEAQ    runtime·tls0(SB), DI
    CALL    runtime·settls(SB)

    // store through it, to make sure it works
    get_tls(BX)
    MOVQ    $0x123, g(BX)
    MOVQ    runtime·tls0(SB), AX
    CMPQ    AX, $0x123
    JEQ 2(PC)
    MOVL    AX, 0    // abort

咱們以前已經提到過TLS。如今,是時候瞭解它是如何實現的了

內部TLS實施

若是仔細看一下前面的代碼片斷,您將很容易理解實際工做中僅有的幾行。

LEAQ    runtime·tls0(SB), DI
CALL    runtime·settls(SB)

當您的操做系統不支持TLS設置時,全部其餘全部內容都將用於跳過TLS設置,並檢查TLS是否正常工做。上面的兩行將runtime·tls0變量的地址存儲在DI寄存器中並調用該runtime·settls函數。該功能的代碼以下所示。

// set tls base to DI
TEXT runtime·settls(SB),NOSPLIT,$32
    ADDQ    $8, DI    // ELF wants to use -8(FS)

    MOVQ    DI, SI
    MOVQ    $0x1002, DI    // ARCH_SET_FS
    MOVQ    $158, AX    // arch_prctl
    SYSCALL
    CMPQ    AX, $0xfffffffffffff001
    JLS    2(PC)
    MOVL    $0xf1, 0xf1  // crash
    RET

從註釋中,咱們能夠了解到此函數進行arch_prctl系統調用並ARCH_SET_FS 做爲參數傳遞。咱們還能夠看到,該系統調用爲FS段寄存器設置了基礎。在咱們的例子中,咱們將TLS設置爲指向runtime·tls0變量。

您還記得咱們在主函數的彙編代碼開頭看到的指令嗎?

0x0000 00000 (test.go:3)    MOVQ    (TLS),CX

前面咱們已經解釋過,它將runtime.g結構實例的地址加載到CX寄存器中。此結構描述了當前結構,goroutine並存儲在線程本地存儲中。如今,咱們能夠找到並瞭解如何將此指令轉換爲機器彙編程序。若是打開先前建立的disassembly.txt文件並查找該main.main函數,則其中的第一條指令應以下所示。

400c00:       64 48 8b 0c 25 f0 ff    mov    %fs:0xfffffffffffffff0,%rcx

本指令(%fs:0xfffffffffffffff0)中的冒號表明分段尋址(您能夠在本教程中閱讀更多內容)。

返回開始順序

最後,讓咱們看一下runtime.rt0_go函數的最後兩部分。

// set the per-goroutine and per-mach "registers"
get_tls(BX)
LEAQ    runtime·g0(SB), CX
MOVQ    CX, g(BX)
LEAQ    runtime·m0(SB), AX
// save m->g0 = g0
MOVQ    CX, m_g0(AX)
// save m0 to g0->m
MOVQ    AX, g_m(CX)

在這裏,咱們將TLS地址加載到BX寄存器中,並將runtime·g0變量的地址保存在TLS中。咱們還初始化runtime.m0變量。若是runtime.g0表明root goroutine,則runtime.m0對應於用於運行root的根操做系統線程goroutine。咱們可能須要仔細看看runtime.g0runtime.m0結構在即將到來的博客文章。

開始序列的最後一部分將初始化參數並調用不一樣的函數,但這是單獨討論的主題。所以,咱們瞭解了引導過程的內部機制,並瞭解瞭如何實現堆棧。爲了前進,咱們須要分析開始序列的最後一部分,這將是咱們下一篇博客文章的主題。

相關文章
相關標籤/搜索