Golang 源碼分析: Golang 的啓動原理

導讀

    隨着技術的發展,大多的 PHPer 都開始轉型 golang。這個也是 golang 因爲 go 的一些特別深受大家喜愛。我們團隊也在試水,在內部的一些小項目上做了試探。在此同時我作爲試探的一員身先士卒,沙場練兵。從中也琢磨到一些技巧希望能和大家一起學習共勉。

一、前期準備:

      go 版本:go1.13.4

        由於是做源碼分析調試,我們必須找到適合自己的工具。咱們調試的話可以考慮幾種工具

        1)gdb    linux 使用比較合適

        2)lldb   mac 自帶,不過建議 dlv

        3)dlv    go 開發的調試工具

        本文中主要以 dlv 爲主,接下來我們一起來安裝一下 dlv。   

    第一步下載

#go get -u github.com/derekparker/delve/cmd/dlv

     

  第二步編譯:

#git clone https://github.com/go-delve/delve.git $GOPATH/src/github.com/go-delve/delve 
#cd $GOPATH/src/github.com/go-delve/ 
#make install

$GOPATH 是 golang 的環境變量,我使用的是 Linux,建議大家在 / etc/profile 設置,mac 相同。

windows 設置 gopath

https://jingyan.baidu.com/article/5d368d1eb616133f60c057bf.html

    第三步設置環境變量:

#vim /etc/profile 
export PATH=$PATH:/data/gopath/bin

/data/gopath/bin 爲你自己的 go 的 bin 目錄

初始化環境變量

#source /etc/profile

dlv 參數介紹(常用)

rWuT1Y

  

二. 調試:

     1. 編譯調試文件  

    代碼內容

 package main 
 import ( 
    "fmt" 
 ) 
 
 func main() { 
     var p **int     
     var i int = 10    
     var p1 *int = &i    
     fmt.Println("p1=", p1)    
     
     p = &p1    
     fmt.Println("p=", p) 
}

    編譯

#go build -gcflags=all="-N -l" pointer.go

必須這樣編譯,待能用 dlv 打印導出變量信息;

    2. 載入文件

#dlv exec ./pointer

  

設置 rt0_go 斷點,程序入口

(dlv) break runtime.rt0_go

是不是很好奇爲什麼入口在 runtime.rt0_go

  

可以打開程序後輸入 r,在輸入 list,然後在輸入 si;si 是單步 cpu 指令; 

其實 go 在運行時會根據系統和 CPU 的不同,找到底層代碼下的,rt0_**.s 的彙編代碼,彙編代碼中再去調轉到 runtime.rt0_go 。

當然由於系統的不同可執行程序的形式不同。

    常見的可執行程序可以分爲三大類:

    1)PE 文件     

         PE 文件主要是 Windows 系列系統,可執行文件索引;

    2)ELF 文件     

        ELF 文件是 linux 系列系統,可執行文件索引;

    3)mach-o 文件    

        mach-o 是 mac 系列系統的可執行文件格式,蘋果系統是基於 FreeBSD 的,屬於 unix-like 操作系統;

    有一篇比較不錯的文章可以推薦給大家:

    https://blog.csdn.net/abc_12366/article/details/88205670

好的我們繼續往下說, 我們進入

  

我們按住 n 執行停留到 212 行

  

我們可以看到 runtime.args 、runtime.osinit 和 runtime.schedinit 三個函數調用。我們可以依次輸入 si 進入函數體內查看

(div)si

  

args 函數主要用途整理命令行參數;

  

  

osint 函數是確定 CPU Core 數量

schedinit 源碼如下:

  

schedinit 主要作用是所有運行時環境初始化; 我們繼續下一個斷點,然後往下執行:

(dlv)b runtime.main  
 (dlv)n

  

runtime/proc.go 部分源碼

func main() {  
  g := getg()        
  g.m.g0.racectx = 0   
  
  // 執行棧最大限制 64位系統1GB,32位系統250MB     
  if sys.PtrSize == 8 {       
      maxstacksize = 1000000000    
  } else {       
      maxstacksize = 250000000
  }    
  
  // 允許newproc啓動新Ms。    
  mainStarted = true    
  
  if GOARCH != "wasm" { // wasm上還沒有線程,所以沒有sysmon       
      // 啓動系統後監控(併發任務調度相關)       
      systemstack(func() {          
          newm(sysmon, nil)       
      })    
  }    
  ...      
  //啓動垃圾回收器後臺操作        
  gcenable()    
  ...        
  //進行間接調用,因爲鏈接器在放置運行時不知道主包的地址。這個就是我們程序文件入口    
  //其實就是用戶main.main函數    
  fn := main_main    
  fn()      
  
  //執行結束    
  exit(0)    
}

我們執行到 fn() 出可以按 s

(dlv)s

  

這樣就來到了我們程序的代碼;這時我們可以看一下我們的調度棧

  

執行完成程序完成之後又回到 runtime/proc.go 中 if atomic.Load(&runningPanicDefers) 處

  

繼續往下執行,一直到 exit(0) 處,按 si 執行。

  

程序回到 sys_linux_amd64.s 彙編代碼中的 runtime.exit

  

最終程序結束;

三. 調用流程:

非 goroutine 退出情況

 

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/c6txkGyafTlLayRURd7KeA