Go源码分析1- 引导程序

go编译好的可执行文件的入口并非我们写的 main.main() 函数,因为编译器会根据特定平台的实现有一个引导过程。
环境 ubuntu18.04, go1.11.2 linux/amd64
本文调试工具使用GDB

测试代码:

package main
func main() {
    println("hello, wenTao!")
}

注意: 调试程序时建议使用 -gcflags "-N -l" 参数关闭编译器代码优化和函数内联,避免断点和单步执行无法准确对应源代码,小函数和局部变量被优化掉。

查找入口地址:

wt-001% go build -gcflags "-N -l" -o main main.go    //1.编译main.go
wt-001% gdb main                    //2.执行程序main
...
(gdb) source /usr/local/go/src/runtime/runtime-gdb.py     //3.加载go运行时
Loading Go Runtime support.
(gdb) info files      //4.文件信息
Symbols from "/home/wt/main".
Local exec file:
    `/home/wt/main', file type elf64-x86-64.
    Entry point: 0x44a380
    0x0000000000401000 - 0x000000000044eba4 is .text
    0x000000000044f000 - 0x0000000000478e14 is .rodata
    0x0000000000478fc0 - 0x0000000000479694 is .typelink
    0x0000000000479698 - 0x00000000004796a0 is .itablink
    0x00000000004796a0 - 0x00000000004796a0 is .gosymtab
    0x00000000004796a0 - 0x00000000004b6c35 is .gopclntab
    0x00000000004b7000 - 0x00000000004b7c08 is .noptrdata
    0x00000000004b7c20 - 0x00000000004b9950 is .data
    0x00000000004b9960 - 0x00000000004d5c90 is .bss
    0x00000000004d5ca0 - 0x00000000004d8398 is .noptrbss
    0x0000000000400f9c - 0x0000000000401000 is .note.go.buildid
(gdb) b *0x44a380     //断点
Breakpoint 1 at 0x44a380: file /usr/local/go/src/runtime/rt0_linux_amd64.s, line 8.
(gdb)

然后就跳到 rt0_linux_amd64.s 第8行, 在 runtime 包中找到该文件,代码如下

1. // Copyright 2009 The Go Authors. All rights reserved.
2. // Use of this source code is governed by a BSD-style
3. // license that can be found in the LICENSE file.
4. 
5. #include "textflag.h"
6.
7. TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
8.  JMP _rt0_amd64(SB)    //第8行处

9. TEXT _rt0_amd64_linux_lib(SB),NOSPLIT,$0
10.     JMP _rt0_amd64_lib(SB)

继续

(gdb) b _rt0_amd64
Breakpoint 4 at 0x446a60: file /usr/local/go/src/runtime/asm_amd64.s, line 15.
(gdb) b asm_amd64.s
Function "asm_amd64.s" not defined.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 5 (asm_amd64.s) pending.
(gdb)

然后发现 _rt0_amd64 继续不下去了。于是查看 asm.amd64.s 文件第15行处代码

10.// _rt0_amd64 is common startup code for most amd64 systems when using
11.// internal linking. This is the entry point for the program from the
12.// kernel for an ordinary -buildmode=exe program. The stack holds the
13.// number of arguments and the C-style argv.
14. TEXT _rt0_amd64(SB),NOSPLIT,$-8
15. MOVQ    0(SP), DI   // argc
16. LEAQ    8(SP), SI   // argv
17. JMP runtime·rt0_go(SB)

在第17行处跳转到了 runtime·rt0_go(SB)

asm.amd64.s 文件继续跟踪发现代码如下

87. TEXT runtime·rt0_go(SB),NOSPLIT,$0
88.     // copy arguments forward on an even stack
89.     MOVQ    DI, AX      // argc
90.     MOVQ    SI, BX      // argv
91.     SUBQ    $(4*8+7), SP        // 2args 2auto
92. ANDQ    $~15, SP
93. MOVQ    AX, 16(SP)
94. MOVQ    BX, 24(SP)
...
140. // update stackguard after _cgo_init
141.    MOVQ    $runtime·g0(SB), CX
142.    MOVQ    (g_stack+stack_lo)(CX), AX
143.    ADDQ    $const__StackGuard, AX
144.    MOVQ    AX, g_stackguard0(CX)
145.    MOVQ    AX, g_stackguard1(CX)
146. 
147. #ifndef GOOS_windows
148.    JMP ok
149. #endif

如上汇编程序在执行一系列指令在第148行继续无条件跳转到 ok,往下查看发现有标签ok,代码如下。

174. ok:
175.    // set the per-goroutine and per-mach "registers"
176.    get_tls(BX)
177.    LEAQ    runtime·g0(SB), CX
178.    MOVQ    CX, g(BX)
179.    LEAQ    runtime·m0(SB), AX
180.
181.    // save m->g0 = g0
182.    MOVQ    CX, m_g0(AX)
183.    // save m0 to g0->m
184.    MOVQ    AX, g_m(CX)
185.
186.    CLD             // convention is D is always left cleared
187.    CALL    runtime·check(SB)
188.
189.    MOVL    16(SP), AX      // copy argc
190.    MOVL    AX, 0(SP)
191.    MOVQ    24(SP), AX      // copy argv
192.    MOVQ    AX, 8(SP)
193.    CALL    runtime·args(SB)
194.    CALL    runtime·osinit(SB)
195.    CALL    runtime·schedinit(SB)
196.
197.    // create a new goroutine to start program
198.    MOVQ    $runtime·mainPC(SB), AX     // entry
199.    PUSHQ   AX
200.    PUSHQ   $0          // arg size
201.    CALL    runtime·newproc(SB)
202.    POPQ    AX
203.    POPQ    AX
204.
205.    // start this M
206.    CALL    runtime·mstart(SB)
207.
208.    CALL    runtime·abort(SB)   // mstart should never return
209.    RET
210.
211.    // Prevent dead-code elimination of debugCallV1, which is
212.    // intended to be called by debuggers.
213.    MOVQ    $runtime·debugCallV1(SB), AX
214.    RET
215.

我们可以继续打断点

(gdb) b runtime.check
Breakpoint 3 at 0x430a90: file /usr/local/go/src/runtime/runtime1.go, line 136.

该文件代码如下:

136. func check() {
137.    var (
138.        a     int8
        b     uint8
        c     int16
        d     uint16
        e     int32
        f     uint32
        g     int64
        h     uint64
        i, i1 float32
        j, j1 float64
        k, k1 unsafe.Pointer
        l     *uint16
        m     [4]byte
    )
    type x1t struct {
        x uint8
    }
    type y1t struct {
        x1 x1t
        y  uint8
    }
    var x1 x1t
    var y1 y1t

    if unsafe.Sizeof(a) != 1 {
        throw("bad a")
    }
    if unsafe.Sizeof(b) != 1 {
        throw("bad b")
    }
    if unsafe.Sizeof(c) != 2 {
        throw("bad c")
    }
    if unsafe.Sizeof(d) != 2 {
        throw("bad d")
    }
    if unsafe.Sizeof(e) != 4 {
        throw("bad e")
    }
    if unsafe.Sizeof(f) != 4 {
        throw("bad f")
    }
    if unsafe.Sizeof(g) != 8 {
        throw("bad g")
    }
    if unsafe.Sizeof(h) != 8 {
        throw("bad h")
    }
    if unsafe.Sizeof(i) != 4 {
        throw("bad i")
    }
    if unsafe.Sizeof(j) != 8 {
        throw("bad j")
    }
    if unsafe.Sizeof(k) != sys.PtrSize {
        throw("bad k")
    }
    if unsafe.Sizeof(l) != sys.PtrSize {
        throw("bad l")
    }
    if unsafe.Sizeof(x1) != 1 {
        throw("bad unsafe.Sizeof x1")
    }
    if unsafe.Offsetof(y1.y) != 1 {
        throw("bad offsetof y1.y")
    }
    if unsafe.Sizeof(y1) != 2 {
        throw("bad unsafe.Sizeof y1")
    }

    if timediv(12345*1000000000+54321, 1000000000, &e) != 12345 || e != 54321 {
        throw("bad timediv")
    }

    var z uint32
    z = 1
    if !atomic.Cas(&z, 1, 2) {
        throw("cas1")
    }
    if z != 2 {
        throw("cas2")
    }

    z = 4
    if atomic.Cas(&z, 5, 6) {
        throw("cas3")
    }
    if z != 4 {
        throw("cas4")
    }

    z = 0xffffffff
    if !atomic.Cas(&z, 0xffffffff, 0xfffffffe) {
        throw("cas5")
    }
    if z != 0xfffffffe {
        throw("cas6")
    }

    k = unsafe.Pointer(uintptr(0xfedcb123))
    if sys.PtrSize == 8 {
        k = unsafe.Pointer(uintptr(k) << 10)
    }
    if casp(&k, nil, nil) {
        throw("casp1")
    }
    k1 = add(k, 1)
    if !casp(&k, k, k1) {
        throw("casp2")
    }
    if k != k1 {
        throw("casp3")
    }

    m = [4]byte{1, 1, 1, 1}
    atomic.Or8(&m[1], 0xf0)
    if m[0] != 1 || m[1] != 0xf1 || m[2] != 1 || m[3] != 1 {
        throw("atomicor8")
    }

    m = [4]byte{0xff, 0xff, 0xff, 0xff}
    atomic.And8(&m[1], 0x1)
    if m[0] != 0xff || m[1] != 0x1 || m[2] != 0xff || m[3] != 0xff {
        throw("atomicand8")
    }

    *(*uint64)(unsafe.Pointer(&j)) = ^uint64(0)
    if j == j {
        throw("float64nan")
    }
    if !(j != j) {
        throw("float64nan1")
    }

    *(*uint64)(unsafe.Pointer(&j1)) = ^uint64(1)
    if j == j1 {
        throw("float64nan2")
    }
    if !(j != j1) {
        throw("float64nan3")
    }

    *(*uint32)(unsafe.Pointer(&i)) = ^uint32(0)
    if i == i {
        throw("float32nan")
    }
    if i == i {
        throw("float32nan1")
    }

    *(*uint32)(unsafe.Pointer(&i1)) = ^uint32(1)
    if i == i1 {
        throw("float32nan2")
    }
    if i == i1 {
        throw("float32nan3")
    }

    testAtomic64()

    if _FixedStack != round2(_FixedStack) {
        throw("FixedStack is not power-of-2")
    }

    if !checkASM() {
        throw("assembly checks failed")
    }
}

继续调试

(gdb) b runtime.args
Breakpoint 4 at 0x430540: file /usr/local/go/src/runtime/runtime1.go, line 60.

查看代码如下

60. func args(c int32, v **byte) {
61.     argc = c
62.     argv = v
63.     sysargs(c, v)
64. }

最关键的是schedinit

(gdb) b runtime.schedinit
Breakpoint 6 at 0x424c30: file /usr/local/go/src/runtime/proc.go, line 532.

代码如下

524. // The bootstrap sequence is:
525. //
526. // call osinit
527. // call schedinit
528. // make & queue new G
529. // call runtime·mstart
530. //
531. // The new G calls runtime·main.
532. func schedinit() {
533.    // raceinit must be the first call to race detector.
    // In particular, it must be done before mallocinit below calls racemapshadow.
    _g_ := getg()
    if raceenabled {
        _g_.racectx, raceprocctx0 = raceinit()
    }
      //最大系统线程数量限制, 参考标准库 func SetMaxThreads(threads int) int
    sched.maxmcount = 10000
   
    tracebackinit()
    moduledataverify()
    stackinit()
    mallocinit()
    mcommoninit(_g_.m)
    cpuinit()       // must run before alginit
    alginit()       // maps must not be used before this call
    modulesinit()   // provides activeModules
    typelinksinit() // uses maps, activeModules
    itabsinit()     // uses activeModules

    msigsave(_g_.m)
    initSigmask = _g_.m.sigmask
      //处理命令行参数与环境变量
    goargs()
    goenvs()
//处理 GODEBUG, GOTRACEBACK 调试相关的环境变量
    parsedebugvars()
//垃圾回收器初始化
    gcinit()

    sched.lastpoll = uint64(nanotime())
//通过 CPU Core 和GOMAXPROCS 环境变量确定 P 数量
    procs := ncpu
    if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
        procs = n
    }
    if procresize(procs) != nil {
        throw("unknown runnable goroutine during bootstrap")
    }

    // For cgocheck > 1, we turn on the write barrier at all times
    // and check all pointer writes. We can't do this until after
    // procresize because the write barrier needs a P.
    if debug.cgocheck > 1 {
        writeBarrier.cgo = true
        writeBarrier.enabled = true
        for _, p := range allp {
            p.wbBuf.reset()
        }
    }

    if buildVersion == "" {
        // Condition should never trigger. This code just serves
        // to ensure runtime·buildVersion is kept in the resulting binary.
        buildVersion = "unknown"
    }
}

然后执行该文件中的main()函数

109. // The main goroutine.
110. func main() {
111.    g := getg()
112. 
113.    // Racectx of m0->g0 is used only as the parent of the main goroutine.
    // It must not be used for anything else.
    g.m.g0.racectx = 0

    // Max stack size is 1 GB on 64-bit, 250 MB on 32-bit.
    // Using decimal instead of binary GB and MB because
    // they look nicer in the stack overflow failure message.
    if sys.PtrSize == 8 {
        maxstacksize = 1000000000
    } else {
        maxstacksize = 250000000
    }

    // Allow newproc to start new Ms.
    mainStarted = true

    if GOARCH != "wasm" { // no threads on wasm yet, so no sysmon
        systemstack(func() {
            newm(sysmon, nil)
        })
    }

    // Lock the main goroutine onto this, the main OS thread,
    // during initialization. Most programs won't care, but a few
    // do require certain calls to be made by the main thread.
    // Those can arrange for main.main to run in the main thread
    // by calling runtime.LockOSThread during initialization
    // to preserve the lock.
    lockOSThread()

    if g.m != &m0 {
        throw("runtime.main not on m0")
    }

    runtime_init() // must be before defer
    if nanotime() == 0 {
        throw("nanotime returning zero")
    }

    // Defer unlock so that runtime.Goexit during init does the unlock too.
    needUnlock := true
    defer func() {
        if needUnlock {
            unlockOSThread()
        }
    }()

    // Record when the world started. Must be after runtime_init
    // because nanotime on some platforms depends on startNano.
    runtimeInitTime = nanotime()

    gcenable()

    main_init_done = make(chan bool)
    if iscgo {
        if _cgo_thread_start == nil {
            throw("_cgo_thread_start missing")
        }
        if GOOS != "windows" {
            if _cgo_setenv == nil {
                throw("_cgo_setenv missing")
            }
            if _cgo_unsetenv == nil {
                throw("_cgo_unsetenv missing")
            }
        }
        if _cgo_notify_runtime_init_done == nil {
            throw("_cgo_notify_runtime_init_done missing")
        }
        // Start the template thread in case we enter Go from
        // a C-created thread and need to create a new thread.
        startTemplateThread()
        cgocall(_cgo_notify_runtime_init_done, nil)
    }

    fn := main_init // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
    fn()
    close(main_init_done)

    needUnlock = false
    unlockOSThread()

    if isarchive || islibrary {
        // A program compiled with -buildmode=c-archive or c-shared
        // has a main, but it is not executed.
        return
    }
    fn = main_main // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
    fn()
    if raceenabled {
        racefini()
    }

    // Make racy client program work: if panicking on
    // another goroutine at the same time as main returns,
    // let the other goroutine finish printing the panic trace.
    // Once it does, it will exit. See issues 3934 and 20018.
    if atomic.Load(&runningPanicDefers) != 0 {
        // Running deferred functions should not take long.
        for c := 0; c < 1000; c++ {
            if atomic.Load(&runningPanicDefers) == 0 {
                break
            }
            Gosched()
        }
    }
    if atomic.Load(&panicking) != 0 {
        gopark(nil, nil, waitReasonPanicWait, traceEvGoStop, 1)
    }

    exit(0)
    for {
        var x *int32
        *x = 0
    }
}

以上是 go 引导程序及一个主 goroutine 的启动过程

从上面可知:

  1. 所有 init 函数都在同一个 goroutine 中执行
  2. 所有 init 函数执行结束后才会执行 main.main 函数

参考书籍:

  1. Go语言学习笔记
  2. Go并发编程实战第二版
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,290评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,399评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,021评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,034评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,412评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,651评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,902评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,605评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,339评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,586评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,076评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,400评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,060评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,851评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,685评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,595评论 2 270

推荐阅读更多精彩内容