go初始化源码学习

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)

运行完上面这几行指令后g0与栈之间的关系如下图所示:

主线程与m0绑定
    LEAQ    runtime·m0+m_tls(SB), DI  //DI = &m0.tls,取m0的tls成员的地址到DI寄存器(LEA:Load Effective Address, m0+m_tls m0偏移m_tls的位置)
    CALL    runtime·settls(SB)   //调用settls设置线程本地存储,settls函数的参数在DI寄存器中

    // store through it, to make sure it works ,验证settls是否可以正常工作,如果有问题则abort退出程序
    get_tls(BX)               //上面两个汇编语句,将m0.tls 放到DI寄存器,然后call 过了settls。此处校验tls是否正确,获取settls后的tls 的地址到BX寄存器
    MOVQ    $0x123, g(BX)   //把整型常量0x123拷贝到BX寄存器
    MOVQ    runtime·m0+m_tls(SB), AX  //AX = m0.tls[0]  将m0.tls 的地址放入AX寄存器
    CMPQ    AX, $0x123   //检查m0.tls[0]的值是否是通过线程本地存储存入的0x123来验证tls功能是否正常
    JEQ 2(PC)
    CALL    runtime·abort(SB)

ok:
    // set the per-goroutine and per-mach "registers"
    get_tls(BX)                  // 调用get_tls 将当前m.tls地址放入BX寄存器  BX = &m.tls
    LEAQ runtime·g0(SB), CX      // 获取g0的地址放入 CX = &g0
    MOVQ CX, g(BX)               // 将CX寄存器也就是g0的地址放入BX也就是当前线程的m.tls[0]   m.tls[0] = BX = CX = &g0
    LEAQ runtime·m0(SB), AX      // AX = &m0

    // save m->g0 = g0
    MOVQ CX, m_g0(AX)            // m.g0 = CX = g0
    // save m0 to g0->m
    MOVQ AX, g_m(CX)             // 此处的CX还保存着g0,也就是g0.m = AX = &m0

    CLD // convention is D is always left cleared

这段代码首先调用settls函数初始化主线程的线程本地存储(TLS),目的是把m0与主线程关联在一起

 

下面我们详细来详细看一下settls函数是如何实现线程私有全局变量的。

runtime/sys_linx_amd64.s : 606

// set tls base to DI
TEXT runtime·settls(SB),NOSPLIT,$32
//......
//DI寄存器中存放的是m.tls[0]的地址,m的tls成员是一个数组,读者如果忘记了可以回头看一下m结构体的定义
//下面这一句代码把DI寄存器中的地址加8,为什么要+8呢,主要跟ELF可执行文件格式中的TLS实现的机制有关
//执行下面这句指令之后DI寄存器中的存放的就是m.tls[1]的地址了
ADDQ$8, DI// ELF wants to use -8(FS)

  //下面通过arch_prctl系统调用设置FS段基址
MOVQDI, SI //SI存放arch_prctl系统调用的第二个参数
MOVQ$0x1002, DI// ARCH_SET_FS //arch_prctl的第一个参数
MOVQ$SYS_arch_prctl, AX //系统调用编号
SYSCALL
CMPQAX, $0xfffffffffffff001
JLS2(PC)
MOVL$0xf1, 0xf1 // crash //系统调用失败直接crash
RET
View Code

从代码可以看到,这里通过arch_prctl系统调用把m0.tls[1]的地址设置成了fs段的段基址。CPU中有个叫fs的段寄存器与之对应,而每个线程都有自己的一组CPU寄存器值,操作系统在把线程调离CPU运行时会帮我们把所有寄存器中的值保存在内存中,调度线程起来运行时又会从内存中把这些寄存器的值恢复到CPU,这样,在此之后,工作线程代码就可以通过fs寄存器来找到m.tls

下面继续分析rt0_go,

runtime/asm_amd64.s : 174

ok:
    // set the per-goroutine and per-mach "registers"
    get_tls(BX)                  // 调用get_tls 将当前m.tls地址放入BX寄存器  BX = &m.tls
    LEAQ runtime·g0(SB), CX      // 获取g0的地址放入 CX = &g0
    MOVQ CX, g(BX)               // 将CX寄存器也就是g0的地址放入BX也就是当前线程的m.tls[0]   m.tls[0] = BX = CX = &g0
    LEAQ runtime·m0(SB), AX      // AX = &m0

    // save m->g0 = g0
    MOVQ CX, m_g0(AX)            // m.g0 = CX = g0
    // save m0 to g0->m
    MOVQ AX, g_m(CX)             // 此处的CX还保存着g0,也就是g0.m = AX = &m0

    CLD // convention is D is always left cleared

上面的代码首先把g0的地址放入主线程的线程本地存储中,然后通过

m0.g0 = &g0
g0.m = &m0

把m0和g0绑定在一起,这样,之后在主线程中通过get_tls可以获取到g0,通过g0的m成员又可以找到m0,于是这里就实现了m0和g0与主线程之间的关联。从这里还可以看到,保存在主线程本地存储中的值是g0的地址,也就是说工作线程的私有全局变量其实是一个指向g的指针而不是指向m的指针,目前这个指针指向g0,表示代码正运行在g0栈。此时,主线程,m0,g0以及g0的栈之间的关系如下图所示:

 

初始化m0

下面代码开始处理命令行参数,这部分我们不关心,所以跳过。命令行参数处理完成后调用osinit函数获取CPU核的数量并保存在全局变量ncpu之中,调度器初始化时需要知道当前系统有多少个CPU核。

runtime/asm_amd64.s : 189

//准备调用args函数,前面四条指令把参数放在栈上
MOVL16(SP), AX// AX = argc
MOVLAX, 0(SP)       // argc放在栈顶
MOVQ24(SP), AX// AX = argv
MOVQAX, 8(SP)       // argv放在SP + 8的位置
CALLruntime·args(SB)  //处理操作系统传递过来的参数和env,不需要关心

//对于linx来说,osinit唯一功能就是获取CPU的核数并放在global变量ncpu中,
//调度器初始化时需要知道当前系统有多少CPU核
CALLruntime·osinit(SB)  //执行的结果是全局变量 ncpu = CPU核数
CALLruntime·schedinit(SB) //调度系统初始化

 

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

 

参考文章

posted @ 2024-06-07 01:40  G1733  阅读(28)  评论(0编辑  收藏  举报