最近想学习golang的源码。但是不知道从何入手,查了很多资料,都是直接介绍从runtime/proc.go开始看。但是我想知道为什么从这开始,如何找到这里的代码,翻了golang的源码感觉压根不知道从何入手去找。黄天不负苦心人,突然找到了go 1.5源码分析这本书,里面介绍了用gdb单步连调,二话不说就自己开始摸索与尝试,感谢作者提供的思路。

  ps:此次寻找路口函数的go版本为master分之上的最新代码,主要分享思路,不考虑版本号。(commit:68e28998d7f094e70cef7ec0bef9fabfa9e17d07)

  直接用go编译出带调试符号(没有调试符号是找不到相应的代码)的可执行文件,命令如下:

ps,当你使用的编译环境为mac时,需要先 export GOFLAGS="-ldflags=-compressdwarf=false" 之后才能build,原理如下:

Go 1.11版本中,由于编译器会产生更多更准确的调试信息,为了减少二进制的大小,DWARF调试信息编译时候会默认被压缩。这对于大多数ELF工具来说这是透明的,也得到Delve支持。但是macOS和Windows上一些工具不支持。如果要禁用DWARF压缩,可以在编译的时候传入参数-ldflags "-compressdwarf=false"

Go 1.11添加了一个实验性的功能,允许在调试器中调用函数。目前这个特性仅得到Delve(version 1.1.0及以上)的支持。

go build -gcflags "-N -l" -o test test.go

  test.go为helloword的代码。或者别的你觉得需要看的代码。由于这里只需要找入口函数,所以不介意test.go的具体内容

  接下来用gdb来调试编译好的test可执行文件

gdb test
info file

//会出现如下结果
(gdb) info files
......
Entry point: 0x104a520
0x0000000001001000 - 0x000000000104eb2f is .text
0x000000000104eb40 - 0x000000000107cdd4 is __TEXT.__rodata
0x000000000107cde0 - 0x000000000107cea6 is __TEXT.__symbol_stub1
0x000000000107cec0 - 0x000000000107d5dc is __TEXT.__typelink
0x000000000107d5e0 - 0x000000000107d5e8 is __TEXT.__itablink
0x000000000107d5e8 - 0x000000000107d5e8 is __TEXT.__gosymtab
0x000000000107d600 - 0x00000000010bcf4c is __TEXT.__gopclntab
0x00000000010bd000 - 0x00000000010bd108 is __DATA.__nl_symbol_ptr
0x00000000010bd120 - 0x00000000010bdc48 is __DATA.__noptrdata
0x00000000010bdc60 - 0x00000000010bf650 is .data
0x00000000010bf660 - 0x00000000010daa70 is .bss
0x00000000010daa80 - 0x00000000010dcf98 is __DATA.__noptrbss 

其中Entry point为程序的入口函数,如果没记错的话,下面部分为ELF 节头表的信息,具体就不深究了

此时找到了入口处,对其下断电,可以得到如下:

(gdb) b *0x104a520
Breakpoint 2 at 0x104a520: file /usr/local/go/src/runtime/rt0_darwin_amd64.s, line 8.

在源码中,找到rt0_darwin_amd64.s文件(不同平台对应的文件名应该也不一样,命名感觉和编译平台强相关)

#include "textflag.h"

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

从以上代码中可以很明显的看到调用了_rt0_amd64这个函数,对此函数下断点,找到对应的函数声明的地方。

(gdb) b _rt0_amd64
Breakpoint
3 at 0x1046c20: file /usr/local/go/src/runtime/asm_amd64.s, line 15.
TEXT _rt0_amd64(SB),NOSPLIT,$-8
   MOVQ   0(SP), DI  // argc
   LEAQ   8(SP), SI  // argv
   JMP    runtime·rt0_go(SB)
(gdb) b runtime.rt0_go    源码中的·实际上为.
Breakpoint 4 at 0x1046c30: file /usr/local/go/src/runtime/asm_amd64.s, line 89.
TEXT runtime·rt0_go(SB),NOSPLIT,$0
    // copy arguments forward on an even stack
    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)

    // 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)

......
    // 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)

    CLD                // convention is D is always left cleared
    CALL    runtime·check(SB)

//初始化,参数,调度等 MOVL 16(SP), AX // copy argc MOVL AX, 0(SP) MOVQ 24(SP), AX // copy argv MOVQ AX, 8(SP) CALL runtime·args(SB) CALL runtime·osinit(SB) CALL runtime·schedinit(SB) // create a new goroutine to start program MOVQ $runtime·mainPC(SB), AX // entry 真正的入口函数,在此函数中初始化并调用了我们所定义的main函数 PUSHQ AX PUSHQ $0 // arg size CALL runtime·newproc(SB)//将runtime.mainPC加入gorounting中 POPQ AX POPQ AX // start this M CALL runtime·mstart(SB)//开始运行 CALL runtime·abort(SB) // mstart should never return RET // Prevent dead-code elimination of debugCallV1, which is // intended to be called by debuggers. MOVQ $runtime·debugCallV1(SB), AX RET

从上述代码可见 runtime.mainPC是整个程序入口函数的代码,将其加入gorouting中。由runtime.mstart开始真正的跑起来。

ps:runtime·mainPC 应该是 runtime.main,通过下断点发现该函数在runtime/proc.go函数中,至此结束

下一章分析一下runtime.mian函数做了什么以及gorouting究竟如何运行