admin 管理员组文章数量: 887021
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的栈帧大小是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个字节, 栈空间:
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基址寄存器指向该空间
版权声明:本文标题:GO 语言的运行时初始化过程解析 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.freenas.com.cn/jishu/1726378107h948353.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论