GO實驗(一)利用dlv查看go程序執(zhí)行流程

前文

4.初始runtime
5.GMP模型

導(dǎo)讀

源碼分析

  • rt0_go
  • check
  • args
  • osinit
  • schedinit
  • newproc
  • mstart
  • runtime.main

環(huán)境

阿里云centOS7
go1.18
demo代碼main.go

package main

import "fmt"

func main(){
    var a int = 1
    fmt.Println(a)
}

實驗

1.編譯成可執(zhí)行文件(就算是利用dlv attach main.go命令也是要先編譯生成臨時文件后運(yùn)行的)

go build main.go

2.readelf查看程序入口--查找elf頭的entry point

readelf -h ./main

//Entry point address:               0x45bfc0
  1. dlv進(jìn)入 0x45bfc0地址(按你機(jī)器上的來)
dlv exec ./main
// (dlv)表示在dlv程序中執(zhí)行的指令
(dlv) l 

( dlv) b *入口地址
(dlv) c

我們執(zhí)行" l "發(fā)現(xiàn)debug程序停留的位置實際和 給入口打斷點(diǎn)的地方一樣!
程序入口

4.進(jìn)入runtime/rt0_linxu_amd64.s/_rt0_amd64

//si表示執(zhí)行一行匯編指令
(dlv) si 
rt0_amd

調(diào)用鏈

5.進(jìn)入rt0_go

(dlv) si

rt0_go是一個很長的函數(shù)娜遵,但是我們可以通過提示直接定位到源碼位置
rt0_go

位于runtime/asm_amd64.s文件中,直接閱讀源碼(ctrl+z掛起dlv)

cd $GOROOT/src/runtime
vim xxxxxx.s

細(xì)扒一下里面的邏輯

        // copy arguments forward on an even stack
         //將DI和SI壓入AX和BX寄存器
        MOVQ    DI, AX          // argc
        MOVQ    SI, BX          // argv
        //棧頂向下移動40位(給函數(shù)棧騰出空間)
        SUBQ    $(5*8), SP              // 3args 2auto
       //15二進(jìn)制是1111,取反是0000
      //相當(dāng)于把低地址4位全變?yōu)?,
      //這么做的目的實際是為了內(nèi)存對齊
        ANDQ    $~15, SP
       //argc,argv參數(shù)壓入棧,分別相對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的結(jié)構(gòu)體成員(runtime2.go)

type  g struct {
  stack stack
  stackguarg0
  stackguarg1
  sched  gobuf //gorutine被調(diào)走時部分信息包括sp會被保存于此
   m *m
}

type stack struct {
        lo uintptr
        hi uintptr
}

所以實際是開辟g0函數(shù)棧,分配空間和資源(創(chuàng)建g0)
前一部分根據(jù)備注我們可以知道是確定入口參數(shù)和CPU處理信息,直到后面發(fā)現(xiàn)都是跳到ok代碼段執(zhí)行,那么直接尋找ok位置


jmp ok

這里再復(fù)習(xí)下m結(jié)構(gòu)

type m struct {
g0           *g
tls           [tlsSlots]uintptr //線程本地私有全局變量
}

這段代碼解釋了g0和m0的相互綁定

         //創(chuàng)建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"
   / /設(shè)置per-goroutine和per-mach寄存器
    //綁定m0和g0的關(guān)系
        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通過指針進(jìn)行相互關(guān)聯(lián)
        // save m->g0 = g0
       //之前cx= &g0,ax=&m0,現(xiàn)在 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創(chuàng)建并相互關(guān)聯(lián)惩系,他們是第一個線程和協(xié)程(實際對于g就算分配椕ト螅空間)趴泌,有了主線程我們就可以工作調(diào)度G啦

#接上文
   CLD
   //運(yùn)行時類型檢查
   CALL runtime·check(SB)
    ....
   //系統(tǒng)參數(shù)獲取,調(diào)用GO函數(shù)args
   CALL    runtime·args(SB)
   // 相關(guān)常量初始化
   CALL    runtime·osinit(SB)
   // 程序調(diào)度相關(guān)常量初始化
   CALL    runtime·schedinit(SB)
   #程序馬上開始運(yùn)行
   // create a new goroutine to start program
   //mainPC方法(也就是runtime·main函數(shù)吆玖,是一個全局變量)壓入AX寄存器复局,
  //函數(shù)是在棧上運(yùn)行的要先將地址參數(shù)壓入棧
   MOVQ    $runtime·mainPC(SB), AX         // entry
   PUSHQ   AX
  //調(diào)用 newproc 函數(shù)創(chuàng)建一個新的g
   CALL    runtime·newproc(SB)
   POPQ    AX

   // start this M
  //// 啟動這個 M.mstart 主線程
   CALL    runtime·mstart(SB)
// M.mstart 應(yīng)該永不返回
   CALL    runtime·abort(SB)       // mstart should never return
        RET

關(guān)于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的函數(shù)值(mainPC=fn(){runtime.main})晤柄,要傳遞給newproc,對 runtime.main 的引用是通過 ABIInternal 進(jìn)行的擦剑,因為 newproc 需要實際的函數(shù)(不是 ABI0 包裝器)----即編譯器負(fù)責(zé)生成了 main 函數(shù)的入口地址,runtime.mainPC 在數(shù)據(jù)段中被定義為 runtime.main 保存主 goroutine 入口地址
rt0_go

實驗小結(jié)

go程序不是從main.main(main包中的main函數(shù))開始執(zhí)行芥颈,也不是runtime.main抓于,而是從rt0_adm64啟動,之后調(diào)用rt0_go浇借,進(jìn)行check類型檢查捉撮,args參數(shù)傳遞,osinit系統(tǒng)基本參數(shù)設(shè)置妇垢,schedinit初始化調(diào)度器巾遭,創(chuàng)建一個新的gorutine和主線程M,調(diào)度器開始循環(huán)調(diào)度(第一個G和第一個M)

查看函數(shù)

check

rutime·check是一個GO語言的函數(shù)闯估,我們在上面打斷點(diǎn)(包名.方法名)

退出vim灼舍,喚醒掛起的dlv程序fg
重新運(yùn)行程序
(dlv) r
(dlv) b runtime.check
(dlv) c 
(dlv) si //不斷重復(fù)si直到進(jìn)入check
程序到達(dá)check

不斷si直到進(jìn)入

根據(jù)上面提示我們可以直到check的位置是在runtime1.go文件中具體代碼不貼了,反正是一堆ifelse判斷檢查

args

在同文件下

func args(c int32, v **byte) {
        argc = c
        argv = v
        //看到sys前綴就知道是調(diào)用了系統(tǒng)調(diào)用
        sysargs(c, v)
}

其中argc和argv是在全局中定義的

var (
        argc int32
        argv **byte
)

osinit

同理利用dlv找到函數(shù)位置
dlv

os_linux.go

//runtime/os_dragonfly.go
func osinit() {
    // 獲取CPU核數(shù)
    ncpu = getncpu()
    if physPageSize == 0 {
        physPageSize = getPageSize()
    }
}

schedinit

dlv找到位于proc.go涨薪,太多懶得貼骑素,咱總結(jié)下偽代碼

#加鎖
_g_:=getg() // getg() *g 是獲取g,初始化獲取g0
#設(shè)置最大線程數(shù)量
#初始化棧和內(nèi)存
mcommoninit(_g_.m, -1)  //看下文分析分配id和加入全局鏈表
#繼續(xù)初始化
//初始化p的個數(shù)刚夺,有多少個核就創(chuàng)建多少個P
procs := ncpu
if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
                procs = n
}
//procresize是初始化p
//這也是一個挺重要的函數(shù)献丑,在這里m和P被關(guān)聯(lián)末捣,總結(jié)在下面
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()
        }
        #線程創(chuàng)建數(shù)量檢查
        //把m放到全局鏈表 m.alllink(alllink是m字段) = alm
        //那如果有m1,m2呢 m0.alllink指向m1.alllink再指向,鏈表嘛
       mp.alllink = allm
       #解鎖

procresize偽代碼(太長啦)

#初始化全局變量allp= make([]*p, nprocs)
# 循環(huán)初始化nprocs個p結(jié)構(gòu)體存放在allp中
# m和allp綁定创橄,以m0為例 m0.p =allp[0],allp[0].m = m0
#把除了allp之外的所有p放入全局變量sched的pidle空閑隊列 

image.png

至此我們已經(jīng)把第一個m,g,p都創(chuàng)建且互相綁定啦

newproc

位于proc.go箩做,調(diào)用newproc創(chuàng)建了第二個G(main gorutine),這部分代碼在GMP模型的1.3中分析過

func newproc(fn *funcval) { //funcval是只包含一個地址指針的結(jié)構(gòu)體
        gp := getg() //獲取當(dāng)前G指針
        pc := getcallerpc() //獲取寄存器PC內(nèi)容
        systemstack(func() {//創(chuàng)建新G
                //真正負(fù)責(zé)初始化G
                newg := newproc1(fn, gp, pc)
            
                _p_ := getg().m.p.ptr()
                //runqput之前提到過
                //先嘗試進(jìn)入runnext妥畏,再本地queue邦邦,最后全局queue 
                runqput(_p_, newg, true)

                if mainStarted {
                        wakep()
                }
        })
}

初始化時newproc的參數(shù)fn就是runtime.main函數(shù)之前在rt0.go已經(jīng)被壓入棧“MOVQ $runtime·mainPC(SB), AX ”醉蚁,“ PUSHQ AX”燃辖,newproc創(chuàng)建main gorutine綁定runtime.main,G被runqput函數(shù)放入runnext準(zhǔn)備開始循環(huán)調(diào)度

  • 每個gorutine都有自己的椡鳎空間黔龟,newproc會創(chuàng)建新g來執(zhí)行fn函數(shù),在新的gorutine上執(zhí)行指令要用新的gorutine棧

mstart

其實mstart在第5章談M的創(chuàng)建的時候已經(jīng)見過了确沸,M的創(chuàng)建newm也會調(diào)用mstart,只不過現(xiàn)在這個m是為執(zhí)行runtime的main函數(shù)的


mstart

mstart調(diào)用mstart0,mstart0位于proc.go
mstart0
//mstart 是 new Ms 的入口點(diǎn)俘陷。它是用匯編編寫的罗捎,使用 ABI0,標(biāo)記為 TOPFRAME拉盾,并調(diào)用 mstart0桨菜。
func mstart()
func mstart0() {
    _g_ := getg()

    osStack := _g_.stack.lo == 0
    if osStack {
//從系統(tǒng)堆棧初始化堆棧邊界。 Cgo 可能在 stack.hi 中保留了堆棧大小捉偏。 minit 可能會更新堆棧邊界倒得。注意:這些界限可能不是很準(zhǔn)確。我們將 hi 設(shè)置為 &size夭禽,但它上面還有一些東西霞掺。 1024 應(yīng)該可以彌補(bǔ)這一點(diǎn),但有點(diǎn)武斷讹躯。
        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
    }
    //初始化堆棧保護(hù)菩彬,以便我們可以開始調(diào)用常規(guī)
    // Go code.
    _g_.stackguard0 = _g_.stack.lo + _StackGuard
    // 這是 g0,所以我們也可以調(diào)用 go:systemstack 函數(shù)來檢查 stackguard1潮梯。
    _g_.stackguard1 = _g_.stackguard0
    mstart1()

    // Exit this thread.
    if mStackIsSystemAllocated() {
        // Windows骗灶、Solaris、illumos秉馏、Darwin耙旦、AIX 和Plan 9 總是system-allocate stack,但是在mstart 之前放在_g_.stack 中萝究,所以上面的邏輯還沒有設(shè)置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上運(yùn)行
        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()   // 進(jìn)入調(diào)度。這個函數(shù)會阻塞琴昆,最終會執(zhí)行main函數(shù)
}


func schedule() {
    _g_ := getg()
    
    ...
    execute(gp, inheritTime) // 在這里會執(zhí)行runtime.main
}

mstart0調(diào)用mstart1氓鄙,進(jìn)行minit()(繼續(xù)m的初始化),mstartm0(啟動m0的信號控制),schedule()進(jìn)入調(diào)度业舍,阻塞函數(shù))
schedule實際上是4個函數(shù)的循環(huán)調(diào)用

runtime.main

不用看就能猜到是調(diào)用用戶寫的main函數(shù)

// The main goroutine.
func main() {
    g := getg()

    ...
    // 執(zhí)行棧最大限制:1GB(64位系統(tǒng))或者 250MB(32位系統(tǒng))
    if sys.PtrSize == 8 {
        maxstacksize = 1000000000
    } else {
        maxstacksize = 250000000
    }
    ...

    // 啟動系統(tǒng)后臺監(jiān)控(定期垃圾回收抖拦、搶占調(diào)度等等)
    systemstack(func() {
        newm(sysmon, nil)
    })

    ...
    // 讓goroute獨(dú)占當(dāng)前線程, 
    // runtime.lockOSThread的用法詳見http://xiaorui.cc/archives/5320
    lockOSThread()

    ...
    // runtime包內(nèi)部的init函數(shù)執(zhí)行
    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執(zhí)行
    main_init()
    ...

    needUnlock = false
    unlockOSThread()

    ...
    // 執(zhí)行用戶的main主函數(shù)
    main_main()
    
    ...
    // 退出
    exit(0)
    for {
        var x *int32
        *x = 0
    }
}

總結(jié)

image.png

參考

1.GO基礎(chǔ)啟動流程
2.GO程序的啟動和runtime初始化
3.go程序啟動過程
4.go夜讀schedule分析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末舷暮,一起剝皮案震驚了整個濱河市态罪,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌下面,老刑警劉巖复颈,帶你破解...
    沈念sama閱讀 211,423評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異沥割,居然都是意外死亡耗啦,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,147評論 2 385
  • 文/潘曉璐 我一進(jìn)店門机杜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來帜讲,“玉大人,你說我怎么就攤上這事椒拗∷平” “怎么了?”我有些...
    開封第一講書人閱讀 157,019評論 0 348
  • 文/不壞的土叔 我叫張陵蚀苛,是天一觀的道長在验。 經(jīng)常有香客問我,道長堵未,這世上最難降的妖魔是什么腋舌? 我笑而不...
    開封第一講書人閱讀 56,443評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮渗蟹,結(jié)果婚禮上侦厚,老公的妹妹穿的比我還像新娘。我一直安慰自己拙徽,他們只是感情好刨沦,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,535評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著膘怕,像睡著了一般想诅。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,798評論 1 290
  • 那天来破,我揣著相機(jī)與錄音篮灼,去河邊找鬼。 笑死徘禁,一個胖子當(dāng)著我的面吹牛诅诱,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播送朱,決...
    沈念sama閱讀 38,941評論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼娘荡,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了驶沼?” 一聲冷哼從身側(cè)響起炮沐,我...
    開封第一講書人閱讀 37,704評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎回怜,沒想到半個月后大年,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,152評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡玉雾,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,494評論 2 327
  • 正文 我和宋清朗相戀三年翔试,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片复旬。...
    茶點(diǎn)故事閱讀 38,629評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡垦缅,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出赢底,到底是詐尸還是另有隱情失都,我是刑警寧澤柏蘑,帶...
    沈念sama閱讀 34,295評論 4 329
  • 正文 年R本政府宣布幸冻,位于F島的核電站,受9級特大地震影響咳焚,放射性物質(zhì)發(fā)生泄漏洽损。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,901評論 3 313
  • 文/蒙蒙 一革半、第九天 我趴在偏房一處隱蔽的房頂上張望碑定。 院中可真熱鬧,春花似錦又官、人聲如沸延刘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,742評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽碘赖。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間普泡,已是汗流浹背播掷。 一陣腳步聲響...
    開封第一講書人閱讀 31,978評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留撼班,地道東北人歧匈。 一個月前我還...
    沈念sama閱讀 46,333評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像砰嘁,于是被迫代替她去往敵國和親件炉。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,499評論 2 348

推薦閱讀更多精彩內(nèi)容