您的位置:首页 > 编程语言 > Go语言

Go内核源码剖析 一 程序执行启动过程

2016-12-01 16:32 555 查看

go内核源码剖析 一

这篇是看雨痕大佬的书所做练习的笔记,(其实后面部分基本都是抄的,但是都实践了)

由于电脑抽风,使用的是win10的Linux子系统,功能不完善,很多跟踪支持性不好(可以算是抄的原因)。

要想看内核源码剖析的可以到雨痕大神的github。

1.新建hello.go编译得到hello

2.gdb调试,输入 gdb hello 命令行得到:((gdb) 后都表示输入的))

先是Linux系统下的:

(gdb) info files

Symbols from "C:\Users\WnagoiYy\hello".

Local exec file:

'C:\Users\WnagoiYy\hello', file type elf64-x86-64.

Entry point: 0x41bc50

0x0000000000400c00 - 0x000000000041c0ba is .text

0x000000000041d000 - 0x0000000000435a00 is .rodata

0x0000000000435a00 - 0x0000000000435af8 is .typelink

0x0000000000435b00 - 0x0000000000440c91 is .gosymtab

0x0000000000440ca0 - 0x0000000000454c46 is .gopclntab

0x0000000000455000 - 0x0000000000455030 is .noptrdata

0x0000000000455040 - 0x0000000000459688 is .data

0x00000000004596a0 - 0x0000000000461698 is .bss

0x00000000004616a0 - 0x0000000000476bf8 is .noptrbss


接下来是windows下的编译后文件gdb:

(gdb) info files

Symbols from "C:\Users\WnagoiYy\go\hello".

Local exec file:

'C:\Users\WnagoiYy\go\hello', file type pei-x86-64.

Entry point: 0x44d730

0x0000000000401000 - 0x00000000004953ad is .text

0x0000000000496000 - 0x0000000000496c00 is .data

0x0000000000506000 - 0x00000000005064fc is .idata


Entry point: 0x41bc50 入口地址在.text段

file type elf64-x86-64. 文件类型为elf

.text

代码段(codesegment/textsegment)通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。

.rodata

存放字符串和#define定义的常量

.data

数据段(datasegment)通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。

.bss

BSS段(bsssegment)通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS是英文BlockStarted by Symbol的简称。BSS段属于静态内存分配。

(gdb) b *0x41bc50

Breakpoint 1 at 0x41bc50: file /usr/lib/go/src/pkg/runtime/rt0_linux_amd64.s, line 8.


入口打上一个断点,因为是win10下的linux子系统,所以文件路径不对

hejing@DESKTOP-EP9L18K:/mnt/c/Go/src/runtime$ ls rt0_*

...

rt0_darwin_arm64.s   rt0_linux_amd64.s      rt0_nacl_386.s

rt0_openbsd_amd64.s  rt0_windows_amd64.s


先cd到/mnt/c/Go/src/runtime,然后输入ls rt0_*,会找到许多文件,我们需要上面断点的rt0_linux_amd64.s

打开rt0_linux_amd64.s,跳到具体行查看代码:

TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8

LEAQ    8(SP), SI // argv

MOVQ    0(SP), DI // argc

MOVQ    $main(SB), AX

JMP AX

...

TEXT main(SB),NOSPLIT,$-8

MOVQ    $runtime·rt0_go(SB), AX

JMP AX


查看MOVQ $runtime·rt0_go(SB), AX里具体的runtime.rt0_go

(gdb) b runtime.rt0_go

Breakpoint 2 at 0x44a780: file /usr/local/go/src/runtime/asm_amd64.s, line 12.


正是asm_amd64.s完成了初始化和运行时启动:

TEXT runtime·rt0_go(SB),NOSPLIT,$0

...

//调用初始化函数

CALL runtime·args(SB)

CALL runtime·osinit(SB)

CALL runtime·schedinit(SB)

//创建 main goroutine 用于执行 runtime.main

MOVQ $runtime·mainPC(SB), AX

PUSHQ AX

PUSHQ $0

CALL runtime·newproc(SB)

POPQ AX

POPQ AX

//让当前线程开始执行 main goroutine

CALL runtime·mstart(SB)

RET

DATA runtime·mainPC+0(SB)/8,$runtime·main(SB)

GLOBL runtime·mainPC(SB),RODATA,$8


到此,汇编引导全部完成,剩下的由golang实现

(gdb) b runtime.main

Breakpoint 3 at 0x423250: file /usr/local/go/src/runtime/proc.go, line 28.


接下来查看初始化的相关内容:

由上得知调用了以下初始化函数:

CALL runtime·args(SB)

CALL runtime·osinit(SB)

CALL runtime·schedinit(SB)

现在查看args函数的位置和内容:

(gdb) b runtime.args

Breakpoint 7 at 0x42ebf0: file /usr/local/go/src/runtime/runtime1.go, line 48.


runtime1.go可以看见初始化参数函数:

func args(c int32, v **byte) {//整理命令行参数
argc = c
argv = v
sysargs(c, v)
}

现在查看osinit函数的位置和内容:

Breakpoint 8 at 0x41e9d0: file /usr/local/go/src/runtime/os1_linux.go, line 172.


在**os1_linux.go**中可以看见:
func osinit() { //确定CPU Core的数量
ncpu = getproccount()
}


最为关键的就是**schedinit**,所有运行环境的初始化都在这里:

Breakpoint 9 at 0x424590: file /usr/local/go/src/runtime/proc1.go, line 40.


跟进**proc1.go**:
// The bootstrap sequence is:
//
//  call osinit
//  call schedinit
//  make & queue new G
//  call runtime·mstart
//
// The new G calls runtime·main.
func schedinit() {
// raceinit must be the first call to race detector.
// In particular, it must be done before mallocinit below calls racemapshadow.
_g_ := getg()
if raceenabled {
_g_.racectx, raceprocctx0 = raceinit()
}

//最大系统线程数量限制,具体查看 runtime/debug.SetMaxThreads
// maximum number of m's allowed (or die)
sched.maxmcount = 10000            //sched结构体位于rutime2.go中

//栈,内存分配器,调度器相关初始化
tracebackinit()
moduledataverify()
stackinit()
mallocinit()
mcommoninit(_g_.m)
alginit()       // maps must not be used before this call
typelinksinit() // uses maps
itabsinit()

msigsave(_g_.m)
initSigmask = _g_.m.sigmask

//处理命令行参数和环境变量
goargs()
goenvs()

//处理 GODEBUG?GOTRACEBACK 调试相关的环境变量设置
parsedebugvars()

//垃圾回收初始化
gcinit()

//通过CPU Core 和 GOMAXPROCS 环境变量确定 P 数量
sched.lastpoll = uint64(nanotime())
procs := int(ncpu)
if procs > _MaxGomaxprocs {
procs = _MaxGomaxprocs
}
if n := atoi(gogetenv("GOMAXPROCS")); n > 0 {
if n > _MaxGomaxprocs {
n = _MaxGomaxprocs
}
procs = n
}

//调整p的数量
if procresize(int32(procs)) != nil {
throw("unknown runnable goroutine during bootstrap")
}

if buildVersion == "" {
// Condition should never trigger. This code just serves
// to ensure runtime·buildVersion is kept in the resulting binary.
buildVersion = "unknown"
}
}


初始化操作到此并未结束,因为接下来要执⾏的是 runtime.main,⽽不是⽤户逻
辑⼊⼜函数 main.main。

(gdb) b runtime.main
Breakpoint 10 at 0x423250: file /usr/local/go/src/runtime/proc.go, line 28.


查看**proc.go**里的main函数:
func main() {
g := getg()
g.m.g0.racectx = 0
//执行栈的最大限制:1GB-on 64bit, 250MB-on 32bit
if sys.PtrSize == 8 {
maxstacksize = 1000000000
} else {
maxstacksize = 250000000
}

runtimeInitTime = nanotime()

//启动系统后台监控(定期垃圾回收,以及并发任务相关)
systemstack(func() {
newm(sysmon, nil)
})

lockOSThread()

if g.m != &m0 {
throw("runtime.main not on m0")
}

//执行runtime里面的所有init函数
runtime_init() // must be before defer

needUnlock := true
defer func() {
if needUnlock {
unlockOSThread()
}
}()

//启动垃圾回收器后台操作
gcenable()

main_init_done = make(chan bool)
if iscgo {
if _cgo_thread_start == nil {
throw("_cgo_thread_start missing")
}
if GOOS != "windows" {
if _cgo_setenv == nil {
throw("_cgo_setenv missing")
}
if _cgo_unsetenv == nil {
throw("_cgo_unsetenv missing")
}
}
if _cgo_notify_runtime_init_done == nil {
throw("_cgo_notify_runtime_init_done missing")
}
cgocall(_cgo_notify_runtime_init_done, nil)
}

//用户main包的init函数初始化调用
main_init()
close(main_init_done)

needUnlock = false
unlockOSThread()

if isarchive || islibrary {
return
}

//执行用户的main函数
main_main()
if raceenabled {
racefini()
}

if panicking != 0 {
gopark(nil, nil, "panicwait", traceEvGoStop, 1)
}

//执行结束,返回函数状态码
exit(0)
for {
var x *int32
*x = 0
}
}


与之相关的就是 runtime_init 和 main_init 这两个函数,它们都是由编译器动态⽣成

//go:linkname runtime_init runtime.init

func runtime_init()

//go:linkname main_init main.init

func main_init()

//go:linkname main_main main.main

func main_main()


注意链接后符号名的变化:runtime_init > runtime.init。

我们准备⼀个稍微复杂点的⽰例,看看编译器究竟⼲了什么。

src

|+- main.go, test.go

|+- lib

|+- sum.go

lib/sum.go

package lib
func init(){
println("sum.init")
}
func Sum(x ...int)int{
n:=0
for _,i:=range x{
n+=i
}
return n
}

test.go
package main
import (    "lib")
func init() {
println("test.init")
}
func test() {
println(lib.Sum(1, 2, 3))
}

main.go
package main
import (
_ "net/http"
)
func init() {
println("main.init.2")
}
func main() {
test()
}
func init() {
println("main.init.1")
}

编译,执⾏输出。
$ go build -gcflags "-N -l" -o test
$ ./test
sum.init
main.init.2
main.init.1
test.init
6


接下来我们⽤反汇编⼯具,看看最终动态⽣成代码的真实⾯⽬:
TEXT runtime.init.1(SB) c:/go/src/runtime/mstats.go
...
mstats.go:175   0x41fc00  CALL runtime.printlock(SB)
...
mstats.go:175   0x41fc10  CALL runtime.printint(SB)
mstats.go:175   0x41fc15  CALL runtime.printsp(SB)
mstats.go:175   0x41fc1a  MOVQ $0x1690, 0(SP)
mstats.go:175   0x41fc22  CALL runtime.printint(SB)
mstats.go:175   0x41fc27  CALL runtime.printnl(SB)
mstats.go:175   0x41fc2c  CALL runtime.printunlock(SB)
mstats.go:176   0x41fc31  LEAQ 0x49ffd(IP), AX
mstats.go:176   0x41fc38  MOVQ AX, 0(SP)
mstats.go:176   0x41fc3c  MOVQ $0x24, 0x8(SP)
mstats.go:176   0x41fc45  CALL runtime.throw(SB)
mstats.go:176   0x41fc4a  UD2
mstats.go:172   0x41fc4c  CALL runtime.morestack_noctxt(SB)
mstats.go:172   0x41fc51  JMP runtime.init.1(SB)
:-1             0x41fc56  INT $0x3
...

TEXT runtime.init.2(SB) c:/go/src/runtime/panic.go
...
panic.go:177    0x423966  CALL runtime.writebarrierptr(SB)
panic.go:178    0x42396b  JMP 0x42394c
panic.go:174    0x42396d  CALL runtime.morestack_noctxt(SB)
panic.go:174    0x423972  JMP runtime.init.2(SB)
:-1             0x423977  INT $0x3
....

TEXT runtime.init.3(SB) c:/go/src/runtime/proc.go
...
proc.go:213     0x426857  CALL runtime.newproc(SB)
proc.go:214     0x42685c  MOVQ 0x10(SP), BP
proc.go:214     0x426861  ADDQ $0x18, SP
proc.go:214     0x426865  RET
proc.go:212     0x426866  CALL runtime.morestack_noctxt(SB)
proc.go:212     0x42686b  JMP runtime.init.3(SB)
:-1             0x42686d  INT $0x3
...

TEXT runtime.init(SB) c:/go/src/runtime/zcallback_windows.go
...
zcallback_windows.go:6  0x445d53  CALL runtime.throwinit(SB)
...
panic.go:23             0x445d95  CALL runtime.convT2I(SB)
...
select.go:48            0x445fa9  CALL runtime.funcPC(SB)
...
select.go:49            0x445fd1  CALL runtime.funcPC(SB)
...
zcallback_windows.go:6  0x445fe2  CALL runtime.init.1(SB)
zcallback_windows.go:6  0x445fe7  CALL runtime.init.2(SB)
zcallback_windows.go:6  0x445fec  CALL runtime.init.3(SB)
...
panic.go:23             0x44609a  CALL runtime.writebarrierptr(SB)
panic.go:30             0x44609f  JMP 0x445dc0
zcallback_windows.go:6  0x4460a4  CALL runtime.morestack_noctxt(SB)
zcallback_windows.go:6  0x4460a9  JMP runtime.init(SB)
:-1                     0x4460ae  INT $0x3


runtime里面的多个init被赋予唯一的函数名,再由 runtime.init(SB)统一调用,⾄于 main.init,情况基本⼀致。区别在于它负责调⽤⾮ runtime 包的初始化函数:

$ go tool objdump -s "main\.init\b" test
TEXT main.init.1(SB) src/main.go
main.go:7 ...
TEXT main.init.2(SB) src/main.go
main.go:15 ...
TEXT main.init.3(SB) src/test.go
test.go:7 ...
TEXT main.init(SB) src/test.go
test.go:13 ...
test.go:13 CALL net/http.init(SB)
test.go:13 CALL test/lib.init(SB)
test.go:13 CALL main.init.1(SB)
test.go:13 CALL main.init.2(SB)
test.go:13 CALL main.init.3(SB)
test.go:13 MOVL $0x2, 0x48d543(IP)
test.go:13 RET


被引⽤的包,包括 lib 和标准库 net/http ⾥的 init 函数都被 main.init 调⽤。

*虽然从当前版本的编译器⾓度来说,init 的执⾏顺序和依赖关系、⽂件名以及定义顺序有关。但这种次序⾮常不便于维护和理解,极易造成潜在错误,所以强烈要求让 init 只做该做的事情:局部初始化。

最后需要记住:

-所有 init 函数都在同⼀个 goroutine 内执⾏。

-所有 init 函数结束后才会执⾏ main.main 函数。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  源码 Go GoLang