go 程序如何运行

go的build 分为 编译和链接
编译过程分为:词法分析、语法分析、语义分析、优化。 然后生成汇编代码。 然后汇编器执行汇编代码。

词法分析: 针对 关键字、标识符、 字面量、 特殊符号等做转化处理。将源码的字符分割转化成一系列token。
语法分析:根据生成映射的token。然后将token生成语法树。语法分析可以检测语法是否错误。
语义分析:语法分析是否存在语法错误之后,然后对语义进行分分析,分析语义的意思,语义分析还包括变量类型的匹配、转换等。
中间代码生成: 从AST抽象语法树到SSA中间代码。
链接过程:将目标文件链接成可执行文件

go 程序启动

  1. go build -gcflags "-N -l" -o hello main.og

-gcflags”-N -l” 是为了关闭编译器优化和函数内联,防止后面在设置断点的时候找不到相对应的代码位置。

通过
$ gdb hello
进入到gdb调试模式, 输入info files,然后根据 Entry point 的地址 输入 b *0x454dd0 这个地址是每次都不同的。然后得到程序的入口地址。
image.png

  1. /src/runtime/rt0_linux_amd64.s/ _rt0_amd64_linux(SB)
  2. TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
  3. JMP _rt0_amd64(SB)
  4. /src/runtime/asm_amd64.s/TEXT _rt0_amd64(SB),NOSPLIT,$-
  5. TEXT _rt0_amd64(SB),NOSPLIT,$-8
  6. MOVQ 0(SP), DI // argc
  7. LEAQ 8(SP), SI // argv
  8. JMP runtime·rt0_go(SB)
  1. TEXT runtime·rt0_go(SB),NOSPLIT,$0
  2. .......
  3. // set the per-goroutine and per-mach "registers"
  4. get_tls(BX)
  5. LEAQ runtime·g0(SB), CX
  6. MOVQ CX, g(BX)
  7. LEAQ runtime·m0(SB), AX
  8. // save m->g0 = g0
  9. MOVQ CX, m_g0(AX)
  10. // save m0 to g0->m
  11. MOVQ AX, g_m(CX)
  12. CLD // convention is D is always left cleared
  13. CALL runtime·check(SB)
  14. MOVL 16(SP), AX // copy argc
  15. MOVL AX, 0(SP)
  16. MOVQ 24(SP), AX // copy argv
  17. MOVQ AX, 8(SP)
  18. CALL runtime·args(SB)
  19. /// 初始化执行文件的绝对路径
  20. CALL runtime·osinit(SB)
  21. // 初始化 CPU 个数和内存页大小
  22. CALL runtime·schedinit(SB)
  23. // create a new goroutine to start program
  24. //要在 main goroutine 上运行的函数
  25. MOVQ $runtime·mainPC(SB), AX // entry
  26. PUSHQ AX
  27. PUSHQ $0 // arg size
  28. //新建一个 goroutine,该 goroutine 绑定 runtime.main,放在 P 的本地队列,等待调度
  29. CALL runtime·newproc(SB)
  30. POPQ AX
  31. POPQ AX
  32. // start this M
  33. // 启动M,开始调度goroutine
  34. CALL runtime·mstart(SB)
  35. CALL runtime·abort(SB) // mstart should never return
  36. RET
  37. // Prevent dead-code elimination of debugCallV1, which is
  38. // intended to be called by debuggers.
  39. MOVQ $runtime·debugCallV1(SB), AX
  40. RET
  41. DATA runtime·mainPC+0(SB)/8,$runtime·main(SB)
  42. GLOBL runtime·mainPC(SB),RODATA,$8

汇编函数 runtime·rt0_go(SB)主要工作就几个

  1. 检查运行平台的CPU,设置好程序运行需要相关标志。
  2. TLS的初始化。
  3. runtime.args、runtime.osinit、runtime.schedinit 三个方法做好程序运行需要的各种变量与调度器。
  4. runtime.newproc创建新的goroutine用于绑定用户写的main方法。
  5. runtime.mstart开始goroutine的调度。

image.png

src/runtime/osinit()
获取CPU核数与内存页大小。按照本文的测试工程:

  • runtime.ncpu = 8
  • runtime.physPageSize = 4096

    1. // BSD interface for threading.
    2. func osinit() {
    3. // pthread_create delayed until end of goenvs so that we
    4. // can look at the environment first.
    5. ncpu = getncpu()
    6. physPageSize = getPageSize()
    7. }

    src/runtime/proc.go/schedinit() ```go // (gdb) b runtime.schedinit // Breakpoint 5 at 0x1029b60: file /usr/local/go/src/runtime/proc.go, line 458.

// The bootstrap sequence is: // // call osinit // call schedinit // make & queue new G // call runtime·mstart // // The new G calls runtime·main. func schedinit() { // raceinit must be the first call to race detector. // In particular, it must be done before mallocinit below calls racemapshadow. // 从TLS中获取g实例 g := getg()

  1. if raceenabled {
  2. _g_.racectx, raceprocctx0 = raceinit()
  3. }
  4. // 设置全局线程数上限
  5. sched.maxmcount = 10000
  6. // 初始化一系列函数所在的PC计数器,用于traceback
  7. tracebackinit()
  8. // 貌似是验证链接器符号的正确性
  9. moduledataverify()
  10. // 栈的初始化
  11. stackinit()
  12. // 内存分配器初始化
  13. mallocinit()
  14. mcommoninit(_g_.m)
  15. // 初始化AES HASH算法
  16. alginit() // maps must not be used before this call
  17. modulesinit() // provides activeModules
  18. typelinksinit() // uses maps, activeModules
  19. itabsinit() // uses activeModules
  20. msigsave(_g_.m)
  21. initSigmask = _g_.m.sigmask
  22. // 获取命令行参数
  23. // 例如: ./$GOPATH/test/main test1 test2
  24. // 执行goargs得到runtime.argslice = []string len: 3, cap: 3, ["main","test1","test2"]
  25. goargs()
  26. // 获取所有的环境变量
  27. goenvs()
  28. parsedebugvars()
  29. // gc初始化
  30. gcinit()
  31. sched.lastpoll = uint64(nanotime())
  32. // P个数检查
  33. procs := ncpu
  34. if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
  35. procs = n
  36. }
  37. if procs > _MaxGomaxprocs {
  38. procs = _MaxGomaxprocs
  39. }
  40. // 所有P的初始化
  41. if procresize(procs) != nil {
  42. throw("unknown runnable goroutine during bootstrap")
  43. }
  44. if buildVersion == "" {
  45. // Condition should never trigger. This code just serves
  46. // to ensure runtime·buildVersion is kept in the resulting binary.
  47. buildVersion = "unknown"
  48. }

}

  1. src/runtime/proc.go/newproc->newproc1()<br />newproc1() 就比较长了,这儿概括下它做了的事情:
  2. 1. TLS拿到当前运行的G实例,并且使绑定到当前线程的M实例不可抢占。
  3. 1. M实例上取到P实例,如果P实例本地上有free goroutine就拿过去,没有就到全局调度器那儿偷一些过来。这两个地方都没有,就按照最低栈大小2K new一个G实例(即goroutine)。
  4. 1. 然后设置好G实例上的各种寄存器的信息,SPPC等。
  5. 1. G实例的状态变更为Grunnable,放到P实例的本地可运行队列里等待调度执行,若队列满了,就把一半的G移到全局调度器下。
  6. 1. 释放M实例的不可抢占状态。返回新的G实例。
  7. > 如果是程序刚启动,经由runtime.rt0_go调用newproc1时,实质干的事情就是创建一个G,把runtime.main(也包含main.main)放进去。在执行mstart时,触发调度。所以main实际是在一个新的G里运行的,而不是g0
  8. ```go
  9. // Create a new g running fn with narg bytes of arguments starting
  10. // at argp and returning nret bytes of results. callerpc is the
  11. // address of the go statement that created this. The new g is put
  12. // on the queue of g's waiting to run.
  13. func newproc1(fn *funcval, argp *uint8, narg int32, nret int32, callerpc uintptr) *g {
  14. _g_ := getg()
  15. if fn == nil {
  16. _g_.m.throwing = -1 // do not dump full stacks
  17. throw("go of nil func value")
  18. }
  19. _g_.m.locks++ // disable preemption because it can be holding p in a local var
  20. siz := narg + nret
  21. siz = (siz + 7) &^ 7
  22. // We could allocate a larger initial stack if necessary.
  23. // Not worth it: this is almost always an error.
  24. // 4*sizeof(uintreg): extra space added below
  25. // sizeof(uintreg): caller's LR (arm) or return address (x86, in gostartcall).
  26. // 判断函数参数和返回值的大小是否超出栈大小
  27. if siz >= _StackMin-4*sys.RegSize-sys.RegSize {
  28. throw("newproc: function arguments too large for new goroutine")
  29. }
  30. _p_ := _g_.m.p.ptr()
  31. // 拿到一个free的goroutine,没有就从全局调度器偷
  32. newg := gfget(_p_)
  33. if newg == nil {
  34. // 新建g实例,栈大小2K
  35. newg = malg(_StackMin)
  36. // g实例状态改成dead
  37. casgstatus(newg, _Gidle, _Gdead)
  38. // 将此g实例加入全局的g队列里
  39. allgadd(newg) // publishes with a g->status of Gdead so GC scanner doesn't look at uninitialized stack.
  40. }
  41. if newg.stack.hi == 0 {
  42. throw("newproc1: newg missing stack")
  43. }
  44. if readgstatus(newg) != _Gdead {
  45. throw("newproc1: new g is not Gdead")
  46. }
  47. totalSize := 4*sys.RegSize + uintptr(siz) + sys.MinFrameSize // extra space in case of reads slightly beyond frame
  48. totalSize += -totalSize & (sys.SpAlign - 1) // align to spAlign
  49. sp := newg.stack.hi - totalSize
  50. spArg := sp
  51. if usesLR {
  52. // 使用了LR寄存器存放函数调用完毕后的返回地址
  53. // caller's LR
  54. *(*uintptr)(unsafe.Pointer(sp)) = 0
  55. prepGoExitFrame(sp)
  56. spArg += sys.MinFrameSize
  57. }
  58. if narg > 0 {
  59. memmove(unsafe.Pointer(spArg), unsafe.Pointer(argp), uintptr(narg))
  60. // This is a stack-to-stack copy. If write barriers
  61. // are enabled and the source stack is grey (the
  62. // destination is always black), then perform a
  63. // barrier copy. We do this *after* the memmove
  64. // because the destination stack may have garbage on
  65. // it.
  66. if writeBarrier.needed && !_g_.m.curg.gcscandone {
  67. f := findfunc(fn.fn)
  68. stkmap := (*stackmap)(funcdata(f, _FUNCDATA_ArgsPointerMaps))
  69. // We're in the prologue, so it's always stack map index 0.
  70. bv := stackmapdata(stkmap, 0)
  71. bulkBarrierBitmap(spArg, spArg, uintptr(narg), 0, bv.bytedata)
  72. }
  73. }
  74. // 将newg.sched结构的内存置0
  75. memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched))
  76. // g实例的调度现场保存SP寄存器
  77. newg.sched.sp = sp
  78. // g实例自身也保存SP寄存器
  79. newg.stktopsp = sp
  80. // g实例的调度现场保存goexit函数的PC寄存器,这样goroutine执行完后都能做好回收
  81. newg.sched.pc = funcPC(goexit) + sys.PCQuantum // +PCQuantum so that previous instruction is in same function
  82. // g实例的调度现场关联上对应的g
  83. newg.sched.g = guintptr(unsafe.Pointer(newg))
  84. // g实例的调度现场保存真正待执行函数的PC寄存器
  85. gostartcallfn(&newg.sched, fn)
  86. // g实例保存go语句的PC寄存器位置
  87. newg.gopc = callerpc
  88. // g实例保存待执行函数的PC寄存器位置
  89. newg.startpc = fn.fn
  90. if _g_.m.curg != nil {
  91. // 如果是在goroutine中再new 一个goroutine,就会有labels?
  92. newg.labels = _g_.m.curg.labels
  93. }
  94. // 存在一些go自己创建的goroutine,如果是就在全局调度器里把数量记录下来
  95. if isSystemGoroutine(newg) {
  96. atomic.Xadd(&sched.ngsys, +1)
  97. }
  98. // 设置该goroutine不能被gc扫
  99. newg.gcscanvalid = false
  100. // 设置goroutine状态为可运行
  101. casgstatus(newg, _Gdead, _Grunnable)
  102. // 检查当前p实例里的goroutine id缓存列表是否已经用完,是的话就从全局调度器那儿再获取_GoidCacheBatch个
  103. if _p_.goidcache == _p_.goidcacheend {
  104. // Sched.goidgen is the last allocated id,
  105. // this batch must be [sched.goidgen+1, sched.goidgen+GoidCacheBatch].
  106. // At startup sched.goidgen=0, so main goroutine receives goid=1.
  107. _p_.goidcache = atomic.Xadd64(&sched.goidgen, _GoidCacheBatch)
  108. _p_.goidcache -= _GoidCacheBatch - 1
  109. _p_.goidcacheend = _p_.goidcache + _GoidCacheBatch
  110. }
  111. // 设置goroutine id
  112. newg.goid = int64(_p_.goidcache)
  113. _p_.goidcache++
  114. if raceenabled {
  115. newg.racectx = racegostart(callerpc)
  116. }
  117. if trace.enabled {
  118. traceGoCreate(newg, newg.startpc)
  119. }
  120. // 把新建的G推进当前P的本地队列,并提优设置为下一个可运行的G
  121. runqput(_p_, newg, true)
  122. if atomic.Load(&sched.npidle) != 0 && atomic.Load(&sched.nmspinning) == 0 && mainStarted {
  123. // main方法启动后才进入此if块。唤醒一个空闲的P,如果没有M则创建一个
  124. wakep()
  125. }
  126. _g_.m.locks--
  127. if _g_.m.locks == 0 && _g_.preempt { // restore the preemption request in case we've cleared it in newstack
  128. _g_.stackguard0 = stackPreempt
  129. }
  130. return newg
  131. }

src/runtime/mstart
mstart -> mstart1 -> schedule -> execute

  1. mstart做一些栈相关的检查,然后就调用mstart1。
  2. mstart1先做一些初始化与M相关的工作,例如是信号栈和信号处理函数的初始化。最后调用schedule。
  3. schedule逻辑是这四个方法里最复杂的。简单来说,就是要找出一个可运行的G,不管是从P本地的G队列、全局调度器的G队列、GC worker、因IO阻塞的G、甚至从别的P里偷。然后传给execute运行。
  4. execute对传进来的G设置好相关的状态后,就加载G自身记录着的PC、SP等寄存器信息,恢复现场继续执行。

GOROOT 和 GOPATH

GOROOT

golang的安装路径。

GOPATH

  1. src
  2. pkg
  3. bin
  4. src 存放源文件,
  5. pkg 存放源文件编译后的库文件(归档文件),后缀为 .a
  6. bin 则存放可执行文件

GO 命令详解

go build

-o 只能在编译单个包的时候出现,它指定输出的可执行文件的名字。
-i 会安装编译目标所依赖的包,安装是指生成与代码包相对应的 .a 文件,即静态库文件(后面要参与链接),并且放置到当前工作区的 pkg 目录下,且库文件的目录层级和源码层级一致。
至于 build flags 参数, build,clean,get,install,list,run,test 这些命令会共用一套:

参数 作用
-a 强制重新编译所有涉及到的包,包括标准库中的代码包,这会重写 /usr/local/go 目录下的 .a 文件
-n 打印命令执行过程,不真正执行
-p n 指定编译过程中命令执行的并行数,n 默认为 CPU 核数
-race 检测并报告程序中的数据竞争问题
-v 打印命令执行过程中所涉及到的代码包名称
-x 打印命令执行过程中所涉及到的命令,并执行
-work 打印编译过程中的临时文件夹。通常情况下,编译完成后会被删除
  1. GOOS GOARCH。这两个环境变量不用我们设置,系统默认的。
  2. GOOS Go 所在的操作系统类型,GOARCH Go 所在的计算架构。
  3. Mac 平台上这个目录名就是 darwin_amd64

go install

go install 命令 会在工作目录生成 pkg 目录,GOBIN目录下生成对应的可执行文件

go run

go run 用于编译并运行命令源码文件。 -x 可以打印整个过程涉及到的命令,-work 可以看到临时的工作目录:

reference

  1. 探索golang程序启动过程http://cbsheng.github.io/)
  2. 探索goroutine的创建)
  3. 饶全成-码农桃花源go是如何运行的)