GO实验(一)利用dlv查看go程序执行流程

前文

4.初始runtime
5.GMP模型

导读

源码分析

  • 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
  1. 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 
rt0_amd

调用链

5.进入rt0_go

(dlv) si

rt0_go是一个很长的函数,但是我们可以通过提示直接定位到源码位置
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位置


jmp 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 入口地址
rt0_go

实验小结

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

不断si直到进入

根据上面提示我们可以直到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找到函数位置
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空闲队列 

image.png

至此我们已经把第一个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

mstart调用mstart0,mstart0位于proc.go
mstart0
//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()进入调度,阻塞函数)
schedule实际上是4个函数的循环调用

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
    }
}

总结

image.png

参考

1.GO基础启动流程
2.GO程序的启动和runtime初始化
3.go程序启动过程
4.go夜读schedule分析

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容