go原始碼學習

G1733發表於2024-06-07

1、入口

1.1 準備go程式

package main

func test() int {
    return 1
}

func main() {
    go test()
}

編譯 go build main.go

1.2 使用readelf 查詢入口

root@xxx:/data# readelf -h main 
ELF 頭:
  ...
  入口點地址:               0x453860
  程式頭起點:          64 (bytes into file)
  ...
root@xxx:/data# addr2line -e main -a 0x453860
0x0000000000453860
/usr/lib/go-1.18/src/runtime/rt0_linux_amd64.s:8

2、引導

2.1 檢視引導程式碼

src/runtime/rt0_linux_amd64.s

TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
    JMP    _rt0_amd64(SB)

查詢_rt0_amd64

root@xxx:/data# readelf -s main | grep -P "_rt0_amd64\b"
   912: 0000000000451ba0    14 FUNC    GLOBAL DEFAULT    1 _rt0_amd64
root@xxx:/data# addr2line -e main -a 0451ba0
0x0000000000451ba0
/usr/lib/go-1.18/src/runtime/asm_amd64.s:16

src/runtime/asm_amd64.s

EXT _rt0_amd64(SB),NOSPLIT,$-8
    MOVQ    0(SP), DI    // argc
    LEAQ    8(SP), SI    // argv
    JMP    runtime·rt0_go(SB)
TEXT runtime·rt0_go(SB),NOSPLIT|NOFRAME|TOPFRAME,$0   //標誌 NOSPLIT 表示函式不會引發棧分裂,NOFRAME 表示函式沒有棧幀,TOPFRAME 表示這是棧上的頂層幀
    // copy arguments forward on an even stack
    MOVQ    DI, AX        // argc 將暫存器 DI 中的值(argc,即引數計數)移動到暫存器 AX 中。
    MOVQ    SI, BX        // argv 將暫存器 SI 中的值(argv,即引數陣列)移動到暫存器 BX 中。
    SUBQ    $(5*8), SP        // 3args 2auto 將棧指標 SP 減少 40 位元組(5*8),為 3 個引數和 2 個自動變數留出空間。
    ANDQ    $~15, SP
    MOVQ    AX, 24(SP) // 將引數計數 AX 儲存到棧上的偏移 24 處
    MOVQ    BX, 32(SP) // 同理

    // create istack out of the given (operating system) stack.
    // _cgo_init may update stackguard.
    MOVQ    $runtime·g0(SB), DI // 將全域性變數 runtime.g0 的地址載入到暫存器 DI 中
    LEAQ    (-64*1024)(SP), BX // 將棧指標 SP 減少 64KB,結果儲存在暫存器 BX 中
    MOVQ    BX, g_stackguard0(DI) // 將 BX(新的棧指標值)儲存到 g0 結構的 stackguard0 欄位中
    MOVQ    BX, g_stackguard1(DI)
    MOVQ    BX, (g_stack+stack_lo)(DI) // 將 BX(新的棧指標值)儲存到 g0 結構的 stack.lo 欄位中
    MOVQ    SP, (g_stack+stack_hi)(DI)

    // find out information about the processor we're on
    MOVL    $0, AX
    CPUID   // 獲取cpu 資訊
    CMPL    AX, $0
    JE    nocpuinfo

    CMPL    BX, $0x756E6547  // "Genu"
    JNE    notintel  // 如果不相等,跳轉到 notintel 標籤
    CMPL    DX, $0x49656E69  // "ineI"
    JNE    notintel  
    CMPL    CX, $0x6C65746E  // "ntel"
    JNE    notintel
    MOVB    $1, runtime·isIntel(SB) // 將 1 儲存到 runtime.isIntel 變數中,表示當前 CPU 是 Intel
TEXT runtime·rt0_go(SB),NOSPLIT|NOFRAME|TOPFRAME,$0
    .....省略到 ok 標籤...
ok:
    .....省略一些檢查程式碼...

    CALL    runtime·check(SB)  // 呼叫runtime庫的 check 函式

    MOVL    24(SP), AX        // copy argc
    MOVL    AX, 0(SP)
    MOVQ    32(SP), AX        // copy argv
    MOVQ    AX, 8(SP)
    CALL    runtime·args(SB)    // runtime庫初始化處理命令列引數。
    CALL    runtime·osinit(SB)   // 呼叫 runtime·osinit 函式,進行作業系統相關的初始化工作。
    CALL    runtime·schedinit(SB) // 呼叫 runtime·schedinit 函式,進行排程器的初始化

    // create a new goroutine to start program
    MOVQ    $runtime·mainPC(SB), AX        // entry 將 runtime·mainPC 的地址載入到暫存器 AX 中,是程式的入口點
    PUSHQ    AX     // 將暫存器 AX 中的值推入棧中,即將入口點的地址壓入棧頂
    CALL    runtime·newproc(SB) // 呼叫 runtime·newproc 函式,建立一個新的 goroutine 來執行程式的入口點
    POPQ    AX

    // start this M
    CALL    runtime·mstart(SB)  // 呼叫 runtime·mstart 函式,啟動當前的 M(機器執行緒)

    CALL    runtime·abort(SB)    // mstart should never return 呼叫 runtime·abort 函式,此處是在程式中斷或異常情況下的處理方式
    RET
TEXT runtime·mstart(SB),NOSPLIT|TOPFRAME|NOFRAME,$0
    CALL    runtime·mstart0(SB) // 啟動runtime庫的 mstart0
RET // not reached

2.2 關鍵程式碼

從上述彙編程式碼可以看到,ok標籤下執行到 runtime庫的 osinit -> schedinit -> newproc -> mstart -> mstart0 彙編引導部分就結束了,

其中

  • osinit:src/runtime/os_linux.go:343 func osinit()
  • schedinit:src/runtime/proc.go:750 func schedinit()
  • newproc:src/runtime/proc.go:4874 func newproc(fn *funcval)
  • mstart :src/runtime/asm_amd64.s:393 TEXT runtime·mstart(SB),NOSPLIT|TOPFRAME|NOFRAME,$0
  • mstart0:src/runtime/proc.go:1660 func mstart0()

再到 mstart0 -> mstart1 -> schedule 排程器就正式執行起來了

  • mstart1:src/runtime/proc.go:1702 func mstart1()
  • schedule:src/runtime/proc.go:3839 func schedule()

參考文章

相關文章