前文
导读
源码分析
- rt0_go
- check
- args
- osinit
- schedinit
- newproc
- mstart
- runtime.main
环境
阿里云centOS7
go1.18
demo代码main.go
package main
import "fmt"
func main(){
var a int = 1
fmt.Println(a)
}
实验
1.编译成可执行文件(就算是利用dlv attach main.go命令也是要先编译生成临时文件后运行的)
go build main.go
2.readelf查看程序入口--查找elf头的entry point
readelf -h ./main
//Entry point address: 0x45bfc0
- dlv进入 0x45bfc0地址(按你机器上的来)
dlv exec ./main
// (dlv)表示在dlv程序中执行的指令
(dlv) l
( dlv) b *入口地址
(dlv) c
我们执行" l "发现debug程序停留的位置实际和 给入口打断点的地方一样!4.进入runtime/rt0_linxu_amd64.s/_rt0_amd64
//si表示执行一行汇编指令
(dlv) si
5.进入rt0_go
(dlv) si
rt0_go是一个很长的函数,但是我们可以通过提示直接定位到源码位置位于runtime/asm_amd64.s文件中,直接阅读源码(ctrl+z挂起dlv)
cd $GOROOT/src/runtime
vim xxxxxx.s
细扒一下里面的逻辑
// copy arguments forward on an even stack
//将DI和SI压入AX和BX寄存器
MOVQ DI, AX // argc
MOVQ SI, BX // argv
//栈顶向下移动40位(给函数栈腾出空间)
SUBQ $(5*8), SP // 3args 2auto
//15二进制是1111,取反是0000
//相当于把低地址4位全变为0,
//这么做的目的实际是为了内存对齐
ANDQ $~15, SP
//argc,argv参数压入栈,分别相对SP偏移24和32
MOVQ AX, 24(SP)
MOVQ BX, 32(SP)
// create istack out of the given (operating system) stack.
// _cgo_init may update stackguard.
//给g0分配栈空间
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)
上面的这些字段都是g的结构体成员(runtime2.go)
type g struct {
stack stack
stackguarg0
stackguarg1
sched gobuf //gorutine被调走时部分信息包括sp会被保存于此
m *m
}
type stack struct {
lo uintptr
hi uintptr
}
所以实际是开辟g0函数栈,分配空间和资源(创建g0)
前一部分根据备注我们可以知道是确定入口参数和CPU处理信息,直到后面发现都是跳到ok代码段执行,那么直接寻找ok位置
这里再复习下m结构
type m struct {
g0 *g
tls [tlsSlots]uintptr //线程本地私有全局变量
}
这段代码解释了g0和m0的相互绑定
//创建m0
//初始化m的tls字段用于之后绑定g0
//DI = &m0.tls
LEAQ runtime·m0+m_tls(SB), DI
CALL runtime·settls(SB)
// store through it, to make sure it works
get_tls(BX)
MOVQ $0x123, g(BX)
MOVQ runtime·m0+m_tls(SB), AX
CMPQ AX, $0x123
JEQ 2(PC)
//abort用于退出,本地线程存储不能工作则退出
CALL runtime·abort(SB)
ok:
// set the per-goroutine and per-mach "registers"
/ /设置per-goroutine和per-mach寄存器
//绑定m0和g0的关系
get_tls(BX) //获取fs段的基地址到bx,bx=m0.tls[0]
//lea是取址指令,把g0地址放到cx,cx=&g0
LEAQ runtime·g0(SB), CX
MOVQ CX, g(BX) //m0.tls[0] = &g0
LEAQ runtime·m0(SB), AX //m0地址给ax,ax=&m0
//将m0和g0通过指针进行相互关联
// save m->g0 = g0
//之前cx= &g0,ax=&m0,现在 m0.g0 = &g0
MOVQ CX, m_g0(AX) //给m0的g0字段绑定g
// save m0 to g0->m
// g0.m = &m0
MOVQ AX, g_m(CX)
至此,m0和g0创建并相互关联,他们是第一个线程和协程(实际对于g就算分配栈空间),有了主线程我们就可以工作调度G啦
#接上文
CLD
//运行时类型检查
CALL runtime·check(SB)
....
//系统参数获取,调用GO函数args
CALL runtime·args(SB)
// 相关常量初始化
CALL runtime·osinit(SB)
// 程序调度相关常量初始化
CALL runtime·schedinit(SB)
#程序马上开始运行
// create a new goroutine to start program
//mainPC方法(也就是runtime·main函数,是一个全局变量)压入AX寄存器,
//函数是在栈上运行的要先将地址参数压入栈
MOVQ $runtime·mainPC(SB), AX // entry
PUSHQ AX
//调用 newproc 函数创建一个新的g
CALL runtime·newproc(SB)
POPQ AX
// start this M
//// 启动这个 M.mstart 主线程
CALL runtime·mstart(SB)
// M.mstart 应该永不返回
CALL runtime·abort(SB) // mstart should never return
RET
关于mainPC,编译文件里是这样描述的
// mainPC is a function value for runtime.main, to be passed to newproc.
// The reference to runtime.main is made via ABIInternal, since the
// actual function (not the ABI0 wrapper) is needed by newproc.
DATA runtime·mainPC+0(SB)/8,$runtime·main<ABIInternal>(SB)
GLOBL runtime·mainPC(SB),RODATA,$8
mainPC是runtime.main的函数值(mainPC=fn(){runtime.main}),要传递给newproc,对 runtime.main 的引用是通过 ABIInternal 进行的,因为 newproc 需要实际的函数(不是 ABI0 包装器)----即编译器负责生成了 main 函数的入口地址,runtime.mainPC 在数据段中被定义为 runtime.main 保存主 goroutine 入口地址实验小结
go程序不是从main.main(main包中的main函数)开始执行,也不是runtime.main,而是从rt0_adm64启动,之后调用rt0_go,进行check类型检查,args参数传递,osinit系统基本参数设置,schedinit初始化调度器,创建一个新的gorutine和主线程M,调度器开始循环调度(第一个G和第一个M)
查看函数
check
rutime·check是一个GO语言的函数,我们在上面打断点(包名.方法名)
退出vim,唤醒挂起的dlv程序fg
重新运行程序
(dlv) r
(dlv) b runtime.check
(dlv) c
(dlv) si //不断重复si直到进入check
根据上面提示我们可以直到check的位置是在runtime1.go文件中具体代码不贴了,反正是一堆ifelse判断检查
args
在同文件下
func args(c int32, v **byte) {
argc = c
argv = v
//看到sys前缀就知道是调用了系统调用
sysargs(c, v)
}
其中argc和argv是在全局中定义的
var (
argc int32
argv **byte
)
osinit
同理利用dlv找到函数位置os_linux.go
//runtime/os_dragonfly.go
func osinit() {
// 获取CPU核数
ncpu = getncpu()
if physPageSize == 0 {
physPageSize = getPageSize()
}
}
schedinit
dlv找到位于proc.go,太多懒得贴,咱总结下伪代码
#加锁
_g_:=getg() // getg() *g 是获取g,初始化获取g0
#设置最大线程数量
#初始化栈和内存
mcommoninit(_g_.m, -1) //看下文分析分配id和加入全局链表
#继续初始化
//初始化p的个数,有多少个核就创建多少个P
procs := ncpu
if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
procs = n
}
//procresize是初始化p
//这也是一个挺重要的函数,在这里m和P被关联,总结在下面
if procresize(procs) != nil {
throw("unknown runnable goroutine during bootstrap")
}
mcomoninit也只贴个大概
func mcommoninit(mp *m, id int64) {
_g_ := getg() //初始化g0
lock(&sched.lock)
//给m分配id
if id >= 0 {
mp.id = id
} else {
mp.id = mReserveID()
}
#线程创建数量检查
//把m放到全局链表 m.alllink(alllink是m字段) = alm
//那如果有m1,m2呢 m0.alllink指向m1.alllink再指向,链表嘛
mp.alllink = allm
#解锁
procresize伪代码(太长啦)
#初始化全局变量allp= make([]*p, nprocs)
# 循环初始化nprocs个p结构体存放在allp中
# m和allp绑定,以m0为例 m0.p =allp[0],allp[0].m = m0
#把除了allp之外的所有p放入全局变量sched的pidle空闲队列
至此我们已经把第一个m,g,p都创建且互相绑定啦
newproc
位于proc.go,调用newproc创建了第二个G(main gorutine),这部分代码在GMP模型的1.3中分析过
func newproc(fn *funcval) { //funcval是只包含一个地址指针的结构体
gp := getg() //获取当前G指针
pc := getcallerpc() //获取寄存器PC内容
systemstack(func() {//创建新G
//真正负责初始化G
newg := newproc1(fn, gp, pc)
_p_ := getg().m.p.ptr()
//runqput之前提到过
//先尝试进入runnext,再本地queue,最后全局queue
runqput(_p_, newg, true)
if mainStarted {
wakep()
}
})
}
初始化时newproc的参数fn就是runtime.main函数之前在rt0.go已经被压入栈“MOVQ $runtime·mainPC(SB), AX ”,“ PUSHQ AX”,newproc创建main gorutine绑定runtime.main,G被runqput函数放入runnext准备开始循环调度
- 每个gorutine都有自己的栈空间,newproc会创建新g来执行fn函数,在新的gorutine上执行指令要用新的gorutine栈
mstart
其实mstart在第5章谈M的创建的时候已经见过了,M的创建newm也会调用mstart,只不过现在这个m是为执行runtime的main函数的
//mstart 是 new Ms 的入口点。它是用汇编编写的,使用 ABI0,标记为 TOPFRAME,并调用 mstart0。
func mstart()
func mstart0() {
_g_ := getg()
osStack := _g_.stack.lo == 0
if osStack {
//从系统堆栈初始化堆栈边界。 Cgo 可能在 stack.hi 中保留了堆栈大小。 minit 可能会更新堆栈边界。注意:这些界限可能不是很准确。我们将 hi 设置为 &size,但它上面还有一些东西。 1024 应该可以弥补这一点,但有点武断。
size := _g_.stack.hi
if size == 0 {
size = 8192 * sys.StackGuardMultiplier
}
_g_.stack.hi = uintptr(noescape(unsafe.Pointer(&size)))
_g_.stack.lo = _g_.stack.hi - size + 1024
}
//初始化堆栈保护,以便我们可以开始调用常规
// Go code.
_g_.stackguard0 = _g_.stack.lo + _StackGuard
// 这是 g0,所以我们也可以调用 go:systemstack 函数来检查 stackguard1。
_g_.stackguard1 = _g_.stackguard0
mstart1()
// Exit this thread.
if mStackIsSystemAllocated() {
// Windows、Solaris、illumos、Darwin、AIX 和Plan 9 总是system-allocate stack,但是在mstart 之前放在_g_.stack 中,所以上面的逻辑还没有设置osStack。
osStack = true
}
mexit(osStack)//帮助退出线程的
}
func mstart1() {
_g_ := getg()
if _g_ != _g_.m.g0 { // 判断是不是g0
throw("bad runtime·mstart")
}
_g_.sched.g = guintptr(unsafe.Pointer(_g_))
_g_.sched.pc = getcallerpc() // 保存pc、sp信息到g0
_g_.sched.sp = getcallersp()
asminit() // asm初始化
minit() // m初始化
// Install signal handlers; after minit so that minit can
// prepare the thread to be able to handle the signals.
if _g_.m == &m0 {
//该部分仅在m0上运行
mstartm0() // 启动m0的signal handler
}
if fn := _g_.m.mstartfn; fn != nil {
fn()
}
if _g_.m != &m0 { // 如果不是m0
acquirep(_g_.m.nextp.ptr())
_g_.m.nextp = 0
}
schedule() // 进入调度。这个函数会阻塞,最终会执行main函数
}
func schedule() {
_g_ := getg()
...
execute(gp, inheritTime) // 在这里会执行runtime.main
}
mstart0调用mstart1,进行minit()(继续m的初始化),mstartm0(启动m0的信号控制),schedule()进入调度,阻塞函数)runtime.main
不用看就能猜到是调用用户写的main函数
// The main goroutine.
func main() {
g := getg()
...
// 执行栈最大限制:1GB(64位系统)或者 250MB(32位系统)
if sys.PtrSize == 8 {
maxstacksize = 1000000000
} else {
maxstacksize = 250000000
}
...
// 启动系统后台监控(定期垃圾回收、抢占调度等等)
systemstack(func() {
newm(sysmon, nil)
})
...
// 让goroute独占当前线程,
// runtime.lockOSThread的用法详见http://xiaorui.cc/archives/5320
lockOSThread()
...
// runtime包内部的init函数执行
runtime_init() // must be before defer
// Defer unlock so that runtime.Goexit during init does the unlock too.
needUnlock := true
defer func() {
if needUnlock {
unlockOSThread()
}
}()
// 启动GC
gcenable()
...
// 用户包的init执行
main_init()
...
needUnlock = false
unlockOSThread()
...
// 执行用户的main主函数
main_main()
...
// 退出
exit(0)
for {
var x *int32
*x = 0
}
}