GO 语言的运行时初始化过程解析

ps:这是我19年的写的总结,编辑成了pdf,当时用了很多截图,导致没法复制源码,原来注释的代码也找不到了,只能将就了。
#一、汇编代码分析
本文将剖析,下面这段go语言代码运行的背后逻辑,主要从goroutine的调度层面,深度挖掘其背后的运
行逻辑。以go 1.5.1的源代码为分析:

1.1 go源代码

package main
func main(){
go add(1,2)
}
func add(a,b int)(int,int,int){
return a+b,a,b
}

先来看看上面这段程序的反汇编代码:

1.2 add函数反汇编代码

0x401050     48c744241800000000     MOVQ $0x0, 0x18(SP)
0x401059     48c744242000000000     MOVQ $0x0, 0x20(SP)
0x401062     48c744242800000000     MOVQ $0x0, 0x28(SP)
0x40106b     488b5c2408             MOVQ 0x8(SP), BX
0x401070     488b6c2410             MOVQ 0x10(SP), BP
0x401075     4801eb                 ADDQ BP, BX
0x401078     48895c2418             MOVQ BX, 0x18(SP)
0x40107d     488b5c2408             MOVQ 0x8(SP), BX
0x401082     48895c2420             MOVQ BX, 0x20(SP)
0x401087     488b5c2410             MOVQ 0x10(SP), BX
0x40108c     48895c2428             MOVQ BX, 0x28(SP)
0x401091     c3                     RET

理解这段汇编只需要搞清楚add的栈空间即可:

add 栈空间
因为add的栈帧大小是0,所以SP在CALL之后没有继续扩展,而且没有把BP压栈

1.3 main函数反汇编代码

0x401009     483b6110             CMPQ 0x10(CX), SP
0x40100d     7630                 JBE 0x40103f
0x40100f     4883ec38             SUBQ $0x38, SP
0x401013     48c744241001000000   MOVQ $0x1, 0x10(SP)
0x40101c     48c744241802000000   MOVQ $0x2, 0x18(SP)
0x401025     c7042428000000       MOVL $0x28, 0(SP)
0x40102c     488d05257a0800       LEAQ 0x87a25(IP), AX
0x401033     4889442408           MOVQ AX, 0x8(SP)
0x401038     e8a3920200           CALL runtime.newproc(SB)
0x40103d     ebfe                 JMP 0x40103d
0x40103f     e80c9b0400           CALL runtime.morestack_noctxt(SB)
0x401044     ebba                 JMP main.main(SB)
  • 第一句,是为了获取TLS的地址,可以认为是:MOVQ TLS,CX,在原指令中,我们相对FS基址寄存器向下偏移了8个字节,这是因为我们才将TLS的地址写入FS寄存器的时候是先加了8个字节,原因不知道,反正挺蠢的感觉,这样CX就指向了TLS
  • TLS中存放了g结构体,0x10(CX),指向了g.stack.stackguard0,也就是当前g结构栈的警戒线,当SP指针小 于该值时就需要进行栈的扩展,也就是跳转到0x40103f ,执行 runtime.morestack_noctxt(SB)函数
  • 此外,main函数的主要任务就是调用 runtime.newproc(SB)函数创建一个goroutime,此函数的参数包括 了:协程函数执行地址、参数返回值大小、参数及返回值共7个8字节的变量,因此SP扩展$0x38个字节, 栈空间:
    main 栈空间
    runtime.newproc函数的具体功能暂且不展开,因为main.main本身也是一个goroutine,它显然不是go程序执行的入口,那么main goroutine是如何启动的?G、M、P是如何构建的,需要我们回到回到go世界的起点。

#二、GO runtime的初始化

2.1 极简概述

初始化的流程,我们仅仅考虑goroutime的部分(其实也是主要的核心,GC和内存初始化模块或者占比很小),主要流程是:

  • 创建M0
  • 初始化P数组:allp
  • M0与allp[0]绑定
  • 创建G0,绑定M0,也就是main goroutine
  • 启动M0的调度循环

当然,整个流程没有这么的按部就班,接下来我们逐行分析初始流程

2.2 入口地址 rt0_linux_amd64

通过dlv调试工具,我们很容易的知道,整个程序的入口地址:
_rt0_amd64_linux() /usr/local/go/src/runtime/rt0_linux_amd64.s:8
其核心的汇编代码如下:

所以入口函数的主要工作就是处理一下argc和argv两个参数,然后跳转到rt0_go完成整个初始化过程

2.3 初始化流程控制 rt0_go

其主要代码如下,我们只关注和goroutine相关的主要内容,一些乱七八糟的我们就不看了,主要我也没懂:

2.3.1 保存main参数到栈中


注意此时sp向下扩展了的48个字节,然后把argc和argv保存到了高位的两个28字节,我突然明白了其用意,因为在进行函数调用时,默认都是0(sp)作为第一个参数,这里预留了两个位置就是为了调用其他函数用的,当然前提保证了rt0_go调用的所有的函数的参数都不会超过2*8个字节

2.3.2 初始化g0的栈结构


g0的身份已经明确了,每一个M都自带一个g0结构,用于执行管理类的指令,从而将其和M执行的用户goroutine区分开,这里的g0就是我们的初始化的M0的g0结构。
g0的栈结构初始化结果如下,sp一开始在hi的位置,每次调用函数都sub sp,当达到stackguard0的数值,就会触发函数栈的扩容,这种栈的设计称之为连续栈

2.3.3 存储g0到TLS中


TLS是线程本地存储,在GO语言中,用于存放指向当前G的g结构体指针,也就是每次切换在M上执行的G,或者写换到M的g0结构的时候都要进行TLS的切换。
上面的过程进行了部分的操作:

  • 第一部分通过系统调用arch_prctl,把tls0这个全局变量的地址写到了FS寄存器,TLS正是通过FS基质寄存器进行殉职的,tls0指向的是一个8*8字节大小的指针数组
  • 第二部分是一个测试代码,整明tls0和TLS之间的指向关系
  • 第三部分吧g0的地址写入到TLS中

整个过程的内存操作细节如下:

tls0是一个8*8字节的空间


####通过arch_prctl,让FS基址寄存器指向该空间

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HUST-Kingdo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值