這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
起因
最近幾天學完了GO語言,但是教材裡給出的資訊太少,不能滿足需要。於是在網上看了許多博文,這就發現其中有許多衝突之處,使人越發迷惑。為瞭解惑,我深入分析了一下GO語言而小有心得,想把其中的一些分享給大家,希望能提高大家的學習效率。
GO語言的真正入口
GO語言的runtime.Caller方法會提供當前goroutine的棧上的函數調用資訊,主要有當前的PC值和調用的檔案和行號。若無法獲得資訊,第四個返回的值為false。當我們在main.main裡使用這個函數(本函數為轉載):
func main() { for skip := 0; ; skip++ { pc, file, line, ok := runtime.Caller(skip) if !ok { break } fmt.Printf("skip = %v, pc = %v, file = %v, line = %v\n", skip, pc, file, line) } // Output: // skip = 0, pc = 4198453, file = caller.go, line = 10 // skip = 1, pc = 4280066, file = $(GOROOT)/src/pkg/runtime/proc.c, line = 220 // skip = 2, pc = 4289712, file = $(GOROOT)/src/pkg/runtime/proc.c, line = 1394}
根據這裡的顯示入口是:runtime.goexit -> runtime.main -> main.main 。
這不合常理啊!通過分析runtime.goexit的代碼,無法確認它調用runtime.main。後來又看到有人講GO語言的入口在src/lib9/main.c。所以我認為有必要分析一下源碼,找出它真正的入口,因為我太疑惑了。
從連結器源碼開始
連結器是最終產生可執行檔的工具,我們如果要查看入口,就從它開始。本人使用的是32bit windows 1.21版的GO安裝包,所以就從8l的代碼開始。首先定位到它的main函數:GO/src/cmd/8l/obj.c/main,具體細節不表,在這裡它調用了libinit。libinit的最後幾行是關鍵:
if(INITENTRY == nil) {INITENTRY = mal(strlen(goarch)+strlen(goos)+20);if(!flag_shared) {sprint(INITENTRY, "_rt0_%s_%s", goarch, goos);} else {sprint(INITENTRY, "_rt0_%s_%s_lib", goarch, goos);}}lookup(INITENTRY, 0)->type = SXREF;
函數的真正入口就是這個INITENTRY,在我的版本上它被指向GO/src/pkg/runtime/rt0-windows_386.s。在這個libinit之後,INITENTRY被放在HASH表然後又DUMP到可執行檔這些細節跟主題無關,可能有很多人對此有興趣,但這裡我也就不表了。
彙編入口
PLAN9格式的組合語言,GO/src/pkg/runtime/rt0_windows_386.s內容如下:
TEXT _rt0_386_windows(SB),NOSPLIT,$12
MOVL 12(SP), AX
LEAL 16(SP), BX
MOVL AX, 4(SP)
MOVL BX, 8(SP)
MOVL $-1, 0(SP) // return PC for main
JMP main(SB) //這個main在哪裡呢?就在下面!
TEXT main(SB),NOSPLIT,$0
JMP _rt0_go(SB) //這個_rt0_go在哪裡?
繼續轉到GO/src/pkg/runtime/asm_386.s:
TEXT _rt0_go(SB),NOSPLIT,$0
// copy arguments forward on an even stack
MOVL argc+0(FP), AX
MOVL argv+4(FP), BX
SUBL $128, SP // plenty of scratch
ANDL $~15, SP
MOVL AX, 120(SP) // save argc, argv away
MOVL BX, 124(SP)
// set default stack bounds.
// _cgo_init may update stackguard.
MOVL $runtime·g0(SB), BP
LEAL (-64*1024+104)(SP), BX
MOVL BX, g_stackguard(BP)
MOVL BX, g_stackguard0(BP)
MOVL SP, g_stackbase(BP)
// find out information about the processor we're on
MOVL $0, AX
CPUID
CMPL AX, $0
JE nocpuinfo
MOVL $1, AX
CPUID
MOVL CX, runtime·cpuid_ecx(SB)
MOVL DX, runtime·cpuid_edx(SB)
nocpuinfo:
// if there is an _cgo_init, call it to let it
// initialize and to set up GS. if not,
// we set up GS ourselves.
MOVL _cgo_init(SB), AX
TESTL AX, AX
JZ needtls
MOVL $setmg_gcc<>(SB), BX
MOVL BX, 4(SP)
MOVL BP, 0(SP)
CALL AX
// update stackguard after _cgo_init
MOVL $runtime·g0(SB), CX
MOVL g_stackguard0(CX), AX
MOVL AX, g_stackguard(CX)
// skip runtime·ldt0setup(SB) and tls test after _cgo_init for non-windows
CMPL runtime·iswindows(SB), $0
JEQ ok
needtls:
// skip runtime·ldt0setup(SB) and tls test on Plan 9 in all cases
CMPL runtime·isplan9(SB), $1
JEQ ok
// set up %gs
CALL runtime·ldt0setup(SB)
// store through it, to make sure it works
get_tls(BX)
MOVL $0x123, g(BX)
MOVL runtime·tls0(SB), AX
CMPL AX, $0x123
JEQ ok
MOVL AX, 0 // abort
ok:
// set up m and g "registers"
get_tls(BX)
LEAL runtime·g0(SB), CX
MOVL CX, g(BX)
LEAL runtime·m0(SB), AX
MOVL AX, m(BX)
// save m->g0 = g0
MOVL CX, m_g0(AX)
CALL runtime·emptyfunc(SB) // fault if stack check is wrong
// convention is D is always cleared
CLD
CALL runtime·check(SB)
// saved argc, argv
MOVL 120(SP), AX
MOVL AX, 0(SP)
MOVL 124(SP), AX
MOVL AX, 4(SP)
CALL runtime·args(SB)
CALL runtime·osinit(SB)
CALL runtime·hashinit(SB)
CALL runtime·schedinit(SB)
// create a new goroutine to start program
PUSHL $runtime·main·f(SB) // entry
PUSHL $0 // arg size
ARGSIZE(8)
CALL runtime·newproc(SB)
ARGSIZE(-1)
POPL AX
POPL AX
// start this M
CALL runtime·mstart(SB)
INT $3
RET
DATA runtime·main·f+0(SB)/4,$runtime·main(SB)
GLOBL runtime·main·f(SB),RODATA,$4
反組譯碼驗證
隨便編譯一個GO程式,用W32ASM驗證一下。
200419560 sub esp, 0000000C
:00419563 mov eax, dword pt: [esp+0C]
:O0419$67 lea ebx, dword ptr [esp+10]
:00419$6B mov dword pct [esp+04], eax
:0041956F mnv dword pt: [esp+08], ebx
:00419S73 mov dword ptr [esp], FFFFFFFF
:00419$7A jmp test.00419S80
.......
:00417C90 mov eax, dword pt: [esp+04]
:0O417C94 mov ebx, dword pct [esp+08]
200417098 sub esp, 00000080
:00417C9E and esp, FFFFFFFO
:00417CA1 mov dword ptr [esp+78], eax
:0O417CA9 mov ebp, 00447300
:00417CAE lea ebx, dword pt: [esp-0000FF98]
:00417CB5 mov dword pt: [ebp+40], ebx
:00417CB8 mov dword pt: [ebp+00], ebx
:0O417CBB mov dword pct [ebp+O4], esp
上面是rt0_windows_386.s部分,下面是asm_386.s部分。對比可知,這才是GO程式的真正入口。