編譯以下代碼
package main
func main() {
println("Hello Go")
}
查找 Go 可執行程序的入口#
使用 readelf
查看可執行二進制的入口地址,使用 go 工具鏈的 nm 工具找到符號表中對應地址的函數名
$ readelf -h ./hello
ELF Header:
...
Entry point address: 0x454020
...
$ go tool nm ./hello| grep 454020
454020 T _rt0_amd64_linux
啟動階段#
在 Go 源碼中可以查找到 _rt0_amd64_linux
所在的文件,程序啟動之初的工作寫在對應平台下的匯編代碼裡,主要有兩個文件 runtime/rt0_linux_amd64.s
和 runtime/asm_amd64.s
TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
JMP _rt0_amd64(SB)
TEXT _rt0_amd64(SB),NOSPLIT,$-8
MOVQ 0(SP), DI // argc
JMP runtime·rt0_go(SB)
TEXT runtime·rt0_go(SB),NOSPLIT|NOFRAME|TOPFRAME,$0
// ...
// 設置 g0 的堆棧空間
MOVQ $runtime·g0(SB), DI
LEAQ (-64*1024)(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)
// 使用 arch_prctl(ARCH_SET_FS) 設置 m0 線程的 TLS 基地址為 m0.tls
LEAQ runtime·m0+m_tls(SB), DI
CALL runtime·settls(SB)
// 進行基礎的正確性和安全性檢查
// 包括: 基礎類型的大小, 平台指針類型的大小 CAS 正確性檢查
CALL runtime·check(SB)
MOVL 24(SP), AX // copy argc
MOVL AX, 0(SP)
MOVQ 32(SP), AX // copy argv
MOVQ AX, 8(SP)
CALL runtime·args(SB)
CALL runtime·osinit(SB)
CALL runtime·schedinit(SB)
// rnutime.main 函數的地址傳入 newproc
// 即將 rnutime.main 加入到一個 p 的運行隊列
MOVQ $runtime·mainPC(SB), AX // entry
PUSHQ AX
CALL runtime·newproc(SB)
POPQ AX
// 主線程執行 schedule 調度循環
// runtime.main 會被調度執行
// runtime.main 內部會調用 main.main
CALL runtime·mstart(SB)
// mstart 不應該返回所以走到這裡直接報錯
CALL runtime·abort(SB)
RET
TEXT runtime·mstart(SB),NOSPLIT|TOPFRAME|NOFRAME,$0
CALL runtime·mstart0(SB)
RET // not reached
runtime.args#
// runtime/runtime1.go
func args(c int32, v **byte) {
argc = c
argv = v
sysargs(c, v)
}
此函數主要設置了 argc 和 argv 兩個全局變量,在 sysargs
中通過讀取 argv 獲取 / 設置:
startupRandomData
一個由內核 (ld-linux.so) 設置的 16 字節隨機隨機數據緩衝區physPageSize
系統物理頁大小- 讀取 ELF 文件頭獲取字符串表,符號表,動態鏈接和 vdso 信息
- 在 Linux 下會讀取符號表設置了兩個特殊的 vdso 調用的指針
var vdsoSymbolKeys = []vdsoSymbolKey{
{"__vdso_gettimeofday", 0x315ca59, 0xb01bca00, &vdsoGettimeofdaySym},
{"__vdso_clock_gettime", 0xd35ec75, 0x6e43a318, &vdsoClockgettimeSym},
}
runtime.osinit#
// runtime/os_linux.go
func osinit() {
ncpu = getproccount()
physHugePageSize = getHugePageSize()
if iscgo {
// ... 處理 cgo 信號相關
}
osArchInit()
}
Linux 下的 osinit 比較簡單:
- 通過
sched_getaffinity
獲取處理器信息 - 通過
/sys/kernel/mm/transparent_hugepage/hpage_pmd_size
獲取透明大頁的物理大小
runtime.schedinit#
Go 協程調度器的初始化工作,這裡只取關鍵部分的代碼,對一些暫時沒有實現的空函數調用也忽略掉了
// runtime/proc.go
func schedinit() {
gp := getg()
sched.maxmcount = 10000
moduledataverify() // module 數據校驗
stackinit() // 協程堆棧內存池初始化
mallocinit() // 內存分配器的初始化
alginit() // AES 算法硬件支持初始化
fastrandinit() // 初始化隨機種子, 用了之前的 startupRandomData
mcommoninit(gp.m, -1) // 初始化線程遞增 ID 信號處理協程和快速隨機種子
modulesinit() // 讀取各個 module 初始化 GC 掃描全局變量的大小
typelinksinit() // 讀取各個 module 收集類型鏈表信息
itabsinit() // 根據 typelink 初始化 itab 表
stkobjinit() // 堆棧初始化用於 GC 相關
sigsave(&gp.m.sigmask) // 保存線程信號掩碼
goargs() // 設置全局變量 argslice
goenvs() // 設置全局變量 envs
parsedebugvars() // 讀取 GODEBUG 環境變量設置 trace
gcinit() // GC 初始化
// 根據物理處理器數量或者 GOMAXPROCS 初始化 allp
procs := ncpu
if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
procs = n
}
if procresize(procs) != nil {
throw("unknown runnable goroutine during bootstrap")
}
}
runtime.getg#
runtime.getg 函數會在編譯過程中編譯為一條單獨的匯編,即取出 TLS 基地址,這個基址在協程啟動的時候通過 runtime.gogo
設置為當前協程數據指針即 *g
// ir.OGETG -> ssa.OpGetG -> ssa.OpAMD64LoweredGetG -> MOVQ (TLS), r
TEXT runtime·gogo(SB), NOSPLIT, $0-8
// ...
JMP gogo<>(SB)
TEXT gogo<>(SB), NOSPLIT, $0
get_tls(CX)
MOVQ DX, g(CX)
runtime.procresize#
// runtime/proc.go
func procresize(nprocs int32) *p {
// 前半部分主要是修改:
// 1. allp []*p
// 2. idlepMask 空閒 P 掩碼每個 bit 表示對應索引 allp 上的 P 是否空閒
// 3. timerpMask 可能有定時器的 P 的掩碼每個 bit 表示對應索引 allp 上的 P 是否有定時器
// proc 數量擴張時初始化新創建的 P
// 程序剛開始時 allp 是空的這裡會創建和初始化所有 P
for i := old; i < nprocs; i++ {
pp := allp[i]
if pp == nil {
pp = new(p)
}
pp.init(i)
atomicstorep(unsafe.Pointer(&allp[i]), unsafe.Pointer(pp))
}
// 後續 P 收縮的時候會銷毀多餘的 P
// 所以這裡會先確保當前 P 不是要被銷毀的那個
// 如果是就換成 allp[0]
gp := getg()
if gp.m.p != 0 && gp.m.p.ptr().id < nprocs {
// 繼續用當前的 P
gp.m.p.ptr().status = _Prunning
gp.m.p.ptr().mcache.prepareForSweep()
} else {
if gp.m.p != 0 {
gp.m.p.ptr().m = 0
}
gp.m.p = 0
pp := allp[0]
pp.m = 0
pp.status = _Pidle
acquirep(pp)
}
// proc 數量收縮時清理舊的 P
for i := nprocs; i < old; i++ {
pp := allp[i]
pp.destroy()
}
// 返回可運行的 P 鏈表
var runnablePs *p
for i := nprocs - 1; i >= 0; i-- {
pp := allp[i]
if gp.m.p.ptr() == pp {
continue
}
pp.status = _Pidle
if runqempty(pp) {
// 如果 p 上的本地隊列沒有可運行的 g 放置到空閒 p 掩碼上
pidleput(pp, now)
} else {
// 給 p 找到一個空閒的 m 這裡 m 可能是空的
pp.m.set(mget())
pp.link.set(runnablePs)
runnablePs = pp
}
}
if old != nprocs {
// 如果 proc 數量改變需要修改 GC 占用的 proc 容量(默認會取 25% 的 proc 數量)
gcCPULimiter.resetCapacity(now, nprocs)
}
return runnablePs
}
runtime.newproc#
newproc 傳入一個函數 (地址) 創建一個新的 g 放到當前 p 的本地隊列上然後喚醒當前 p
在程序啟動過程中這裡傳入的就是 runtime.main 的地址
func newproc(fn *funcval) {
gp := getg()
pc := getcallerpc()
systemstack(func() {
newg := newproc1(fn, gp, pc)
pp := getg().m.p.ptr()
runqput(pp, newg, true)
if mainStarted {
// mainStarted 在 runtime.main 中設置
wakep()
}
})
}
func newproc1(fn *funcval, callergp *g, callerpc uintptr) *g {
mp := acquirem()
pp := mp.p.ptr()
newg := gfget(pp) // 這裡先嘗試從 p 的 freeg 鏈表裡取一個出來
if newg == nil {
// 如果找不到再用 malg 創建一個新的 g
// 新的 g 的堆棧還沒有初始化為了避免被 GC 掃描到先將狀態設置為 dead
// 添加到全局的 allg 裡面
newg = malg(_StackMin)
casgstatus(newg, _Gidle, _Gdead)
allgadd(newg)
}
// 計算堆棧指針位置
totalSize := uintptr(4*goarch.PtrSize + sys.MinFrameSize) // extra space in case of reads slightly beyond frame
totalSize = alignUp(totalSize, sys.StackAlign)
sp := newg.stack.hi - totalSize
// 設置 newg 的調度數據, 堆棧指針, 函數地址, 程序計數器, 上級 caller 信息等等數據
memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched))
newg.sched.sp = sp
newg.stktopsp = sp
newg.sched.pc = abi.FuncPCABI0(goexit) + sys.PCQuantum // +PCQuantum so that previous instruction is in same function
newg.sched.g = guintptr(unsafe.Pointer(newg))
gostartcallfn(&newg.sched, fn)
newg.parentGoid = callergp.goid
newg.gopc = callerpc
newg.ancestors = saveAncestors(callergp)
newg.startpc = fn.fn
// 修改狀態為 runnable 把堆棧添加到 GC 堆棧掃描
casgstatus(newg, _Gdead, _Grunnable)
gcController.addScannableStack(pp, int64(newg.stack.hi-newg.stack.lo))
newg.goid = pp.goidcache
pp.goidcache++
releasem(mp)
return newg
}
runtime.mstart0#
func mstart0() {
gp := getg()
// 初始化 g0 的 stackguard 用於堆棧溢出和堆棧擴容檢查
gp.stackguard0 = gp.stack.lo + _StackGuard
gp.stackguard1 = gp.stackguard0
mstart1()
mexit(osStack)
}
func mstart1() {
gp := getg()
// Set up m.g0.sched as a label returning to just
// after the mstart1 call in mstart0 above, for use by goexit0 and mcall.
// We're never coming back to mstart1 after we call schedule,
// so other calls can reuse the current frame.
// And goexit0 does a gogo that needs to return from mstart1
// and let mstart0 exit the thread.
gp.sched.g = guintptr(unsafe.Pointer(gp))
gp.sched.pc = getcallerpc()
gp.sched.sp = getcallersp()
// 初始化線程的信號處理協程堆棧和信後掩碼
minit()
if gp.m == &m0 {
// 設置線程的信號處理函數 sighandler
mstartm0()
}
// 一些內部線程比如 sysmon 直接在這裡啟動
if fn := gp.m.mstartfn; fn != nil {
fn()
}
if gp.m != &m0 {
acquirep(gp.m.nextp.ptr())
gp.m.nextp = 0
}
// 執行調度循環永不返回
// 這裡當前只有一個 p 和一個 g
// 所有會跳轉到 runtime.main
schedule()
}
func main() {
mp := getg().m
// 設置堆棧最大為 1G
if goarch.PtrSize == 8 {
maxstacksize = 1000000000
} else {
maxstacksize = 250000000
}
// newproc 創建的協程可以通過 wakep 找到或創建一個線程執行
mainStarted = true
// 執行 runtime 下的 init 函數和全局變量的初始化
doInit(&runtime_inittask)
// 啟用 GC
gcenable()
// 執行用戶級的 init 函數和全局變量的初始化
doInit(&main_inittask)
// -buildmode=c-archive or c-shared 不執行 main
if isarchive || islibrary {
return
}
// 執行用戶級的 main.main 函數
fn := main_main
fn()
// 主協程退出前如果當前有其他協程的 panic-defer 在處理
// 需要等其他協程先處理完(比如打印 panic 信息等)
if runningPanicDefers.Load() != 0 {
for c := 0; c < 1000; c++ {
if runningPanicDefers.Load() == 0 {
break
}
Gosched()
}
}
// 進程退出前執行註冊過的 hook
// 比如 compile -cover 模式下的代碼覆蓋率數據輸出
runExitHooks(0)
exit(0)
}