GO语解惑:从源码分析GO程序的入口
sz_Promi · · 7176 次点击 · · 开始浏览起因
最近几天学完了GO语言,但是教材里给出的信息太少,不能满足需要。于是在网上看了许多博文,这就发现其中有许多冲突之处,使人越发迷惑。为了解惑,我深入分析了一下GO语言而小有心得,想把其中的一些分享给大家,希望能提高大家的学习效率。
GO语言的真正入口
GO语言的runtime.Caller方法会提供当前goroutine的栈上的函数调用信息,主要有当前的PC值和调用的文件和行号。若无法获得信息,第四个返回的值为false。当我们在main.main里使用这个函数(本函数为转载):
func main() {
for skip := 0; ; skip++ {
pc, file, line, ok := runtime.Caller(skip)
if !ok {
break
}
fmt.Printf("skip = %v, pc = %v, file = %v, line = %v\n", skip, pc, file, line)
}
// Output:
// skip = 0, pc = 4198453, file = caller.go, line = 10
// skip = 1, pc = 4280066, file = $(GOROOT)/src/pkg/runtime/proc.c, line = 220
// skip = 2, pc = 4289712, file = $(GOROOT)/src/pkg/runtime/proc.c, line = 1394
}
根据这里的显示入口是:runtime.goexit -> runtime.main -> main.main 。
这不合常理啊!通过分析runtime.goexit的代码,无法确认它调用runtime.main。后来又看到有人讲GO语言的入口在src/lib9/main.c。所以我认为有必要分析一下源码,找出它真正的入口,因为我太疑惑了。
从链接器源码开始
链接器是最终生成可执行文件的工具,我们如果要查看入口,就从它开始。本人使用的是32bit windows 1.21版的GO安装包,所以就从8l的代码开始。首先定位到它的main函数:GO/src/cmd/8l/obj.c/main,具体细节不表,在这里它调用了libinit。libinit的最后几行是关键:
if(INITENTRY == nil) {
INITENTRY = mal(strlen(goarch)+strlen(goos)+20);
if(!flag_shared) {
sprint(INITENTRY, "_rt0_%s_%s", goarch, goos);
} else {
sprint(INITENTRY, "_rt0_%s_%s_lib", goarch, goos);
}
}
lookup(INITENTRY, 0)->type = SXREF;
函数的真正入口就是这个INITENTRY,在我的版本上它被指向GO/src/pkg/runtime/rt0-windows_386.s。在这个libinit之后,INITENTRY被放在HASH表然后又DUMP到可执行文件这些细节跟主题无关,可能有很多人对此有兴趣,但这里我也就不表了。
汇编入口
PLAN9格式的汇编语言,GO/src/pkg/runtime/rt0_windows_386.s内容如下:
TEXT _rt0_386_windows(SB),NOSPLIT,12ドル
MOVL12(SP), AX
LEAL16(SP), BX
MOVLAX, 4(SP)
MOVLBX, 8(SP)
MOVL$-1, 0(SP) // return PC for main
JMPmain(SB) //这个main在哪里呢?就在下面!
TEXT main(SB),NOSPLIT,0ドル
JMP_rt0_go(SB) //这个_rt0_go在哪里?
继续转到GO/src/pkg/runtime/asm_386.s:
TEXT _rt0_go(SB),NOSPLIT,0ドル
// copy arguments forward on an even stack
MOVLargc+0(FP), AX
MOVLargv+4(FP), BX
SUBL128,ドル SP// plenty of scratch
ANDL$~15, SP
MOVLAX, 120(SP)// save argc, argv away
MOVLBX, 124(SP)
// set default stack bounds.
// _cgo_init may update stackguard.
MOVL$runtime·g0(SB), BP
LEAL(-64*1024+104)(SP), BX
MOVLBX, g_stackguard(BP)
MOVLBX, g_stackguard0(BP)
MOVLSP, g_stackbase(BP)
// find out information about the processor we're on
MOVL0,ドル AX
CPUID
CMPLAX, 0ドル
JEnocpuinfo
MOVL1,ドル AX
CPUID
MOVLCX, runtime·cpuid_ecx(SB)
MOVLDX, runtime·cpuid_edx(SB)
nocpuinfo:
// if there is an _cgo_init, call it to let it
// initialize and to set up GS. if not,
// we set up GS ourselves.
MOVL_cgo_init(SB), AX
TESTLAX, AX
JZneedtls
MOVL$setmg_gcc<>(SB), BX
MOVLBX, 4(SP)
MOVLBP, 0(SP)
CALLAX
// update stackguard after _cgo_init
MOVL$runtime·g0(SB), CX
MOVLg_stackguard0(CX), AX
MOVLAX, g_stackguard(CX)
// skip runtime·ldt0setup(SB) and tls test after _cgo_init for non-windows
CMPL runtime·iswindows(SB), 0ドル
JEQ ok
needtls:
// skip runtime·ldt0setup(SB) and tls test on Plan 9 in all cases
CMPLruntime·isplan9(SB), 1ドル
JEQok
// set up %gs
CALLruntime·ldt0setup(SB)
// store through it, to make sure it works
get_tls(BX)
MOVL0ドルx123, g(BX)
MOVLruntime·tls0(SB), AX
CMPLAX, 0ドルx123
JEQok
MOVLAX, 0// abort
ok:
// set up m and g "registers"
get_tls(BX)
LEALruntime·g0(SB), CX
MOVLCX, g(BX)
LEALruntime·m0(SB), AX
MOVLAX, m(BX)
// save m->g0 = g0
MOVLCX, m_g0(AX)
CALLruntime·emptyfunc(SB)// fault if stack check is wrong
// convention is D is always cleared
CLD
CALLruntime·check(SB)
// saved argc, argv
MOVL120(SP), AX
MOVLAX, 0(SP)
MOVL124(SP), AX
MOVLAX, 4(SP)
CALLruntime·args(SB)
CALLruntime·osinit(SB)
CALLruntime·hashinit(SB)
CALLruntime·schedinit(SB)
// create a new goroutine to start program
PUSHL$runtime·main·f(SB)// entry
PUSHL0ドル// arg size
ARGSIZE(8)
CALLruntime·newproc(SB)
ARGSIZE(-1)
POPLAX
POPLAX
// start this M
CALLruntime·mstart(SB)
INT 3ドル
RET
DATAruntime·main·f+0(SB)/4,$runtime·main(SB)
GLOBLruntime·main·f(SB),RODATA,4ドル
反汇编验证
随便编译一个GO程序,用W32ASM验证一下。
200419560 sub esp, 0000000C
:00419563 mov eax, dword pt: [esp+0C]
:O041967ドル lea ebx, dword ptr [esp+10]
:004196ドルB mov dword pct [esp+04], eax
:0041956F mnv dword pt: [esp+08], ebx
:00419S73 mov dword ptr [esp], FFFFFFFF
:004197ドルA jmp test.00419S80
.......
:00417C90 mov eax, dword pt: [esp+04]
:0O417C94 mov ebx, dword pct [esp+08]
200417098 sub esp, 00000080
:00417C9E and esp, FFFFFFFO
:00417CA1 mov dword ptr [esp+78], eax
:0O417CA9 mov ebp, 00447300
:00417CAE lea ebx, dword pt: [esp-0000FF98]
:00417CB5 mov dword pt: [ebp+40], ebx
:00417CB8 mov dword pt: [ebp+00], ebx
:0O417CBB mov dword pct [ebp+O4], esp
上面是rt0_windows_386.s部分,下面是asm_386.s部分。对比可知,这才是GO程序的真正入口。
有疑问加站长微信联系(非本文作者)
入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889
关注微信- 请尽量让自己的回复能够对别人有帮助
- 支持 Markdown 格式, **粗体**、~~删除线~~、
`单行代码` - 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
- 图片支持拖拽、截图粘贴等方式上传
收入到我管理的专栏 新建专栏
起因
最近几天学完了GO语言,但是教材里给出的信息太少,不能满足需要。于是在网上看了许多博文,这就发现其中有许多冲突之处,使人越发迷惑。为了解惑,我深入分析了一下GO语言而小有心得,想把其中的一些分享给大家,希望能提高大家的学习效率。
GO语言的真正入口
GO语言的runtime.Caller方法会提供当前goroutine的栈上的函数调用信息,主要有当前的PC值和调用的文件和行号。若无法获得信息,第四个返回的值为false。当我们在main.main里使用这个函数(本函数为转载):
func main() {
for skip := 0; ; skip++ {
pc, file, line, ok := runtime.Caller(skip)
if !ok {
break
}
fmt.Printf("skip = %v, pc = %v, file = %v, line = %v\n", skip, pc, file, line)
}
// Output:
// skip = 0, pc = 4198453, file = caller.go, line = 10
// skip = 1, pc = 4280066, file = $(GOROOT)/src/pkg/runtime/proc.c, line = 220
// skip = 2, pc = 4289712, file = $(GOROOT)/src/pkg/runtime/proc.c, line = 1394
}
根据这里的显示入口是:runtime.goexit -> runtime.main -> main.main 。
这不合常理啊!通过分析runtime.goexit的代码,无法确认它调用runtime.main。后来又看到有人讲GO语言的入口在src/lib9/main.c。所以我认为有必要分析一下源码,找出它真正的入口,因为我太疑惑了。
从链接器源码开始
链接器是最终生成可执行文件的工具,我们如果要查看入口,就从它开始。本人使用的是32bit windows 1.21版的GO安装包,所以就从8l的代码开始。首先定位到它的main函数:GO/src/cmd/8l/obj.c/main,具体细节不表,在这里它调用了libinit。libinit的最后几行是关键:
if(INITENTRY == nil) {
INITENTRY = mal(strlen(goarch)+strlen(goos)+20);
if(!flag_shared) {
sprint(INITENTRY, "_rt0_%s_%s", goarch, goos);
} else {
sprint(INITENTRY, "_rt0_%s_%s_lib", goarch, goos);
}
}
lookup(INITENTRY, 0)->type = SXREF;
函数的真正入口就是这个INITENTRY,在我的版本上它被指向GO/src/pkg/runtime/rt0-windows_386.s。在这个libinit之后,INITENTRY被放在HASH表然后又DUMP到可执行文件这些细节跟主题无关,可能有很多人对此有兴趣,但这里我也就不表了。
汇编入口
PLAN9格式的汇编语言,GO/src/pkg/runtime/rt0_windows_386.s内容如下:
TEXT _rt0_386_windows(SB),NOSPLIT,12ドル
MOVL12(SP), AX
LEAL16(SP), BX
MOVLAX, 4(SP)
MOVLBX, 8(SP)
MOVL$-1, 0(SP) // return PC for main
JMPmain(SB) //这个main在哪里呢?就在下面!
TEXT main(SB),NOSPLIT,0ドル
JMP_rt0_go(SB) //这个_rt0_go在哪里?
继续转到GO/src/pkg/runtime/asm_386.s:
TEXT _rt0_go(SB),NOSPLIT,0ドル
// copy arguments forward on an even stack
MOVLargc+0(FP), AX
MOVLargv+4(FP), BX
SUBL128,ドル SP// plenty of scratch
ANDL$~15, SP
MOVLAX, 120(SP)// save argc, argv away
MOVLBX, 124(SP)
// set default stack bounds.
// _cgo_init may update stackguard.
MOVL$runtime·g0(SB), BP
LEAL(-64*1024+104)(SP), BX
MOVLBX, g_stackguard(BP)
MOVLBX, g_stackguard0(BP)
MOVLSP, g_stackbase(BP)
// find out information about the processor we're on
MOVL0,ドル AX
CPUID
CMPLAX, 0ドル
JEnocpuinfo
MOVL1,ドル AX
CPUID
MOVLCX, runtime·cpuid_ecx(SB)
MOVLDX, runtime·cpuid_edx(SB)
nocpuinfo:
// if there is an _cgo_init, call it to let it
// initialize and to set up GS. if not,
// we set up GS ourselves.
MOVL_cgo_init(SB), AX
TESTLAX, AX
JZneedtls
MOVL$setmg_gcc<>(SB), BX
MOVLBX, 4(SP)
MOVLBP, 0(SP)
CALLAX
// update stackguard after _cgo_init
MOVL$runtime·g0(SB), CX
MOVLg_stackguard0(CX), AX
MOVLAX, g_stackguard(CX)
// skip runtime·ldt0setup(SB) and tls test after _cgo_init for non-windows
CMPL runtime·iswindows(SB), 0ドル
JEQ ok
needtls:
// skip runtime·ldt0setup(SB) and tls test on Plan 9 in all cases
CMPLruntime·isplan9(SB), 1ドル
JEQok
// set up %gs
CALLruntime·ldt0setup(SB)
// store through it, to make sure it works
get_tls(BX)
MOVL0ドルx123, g(BX)
MOVLruntime·tls0(SB), AX
CMPLAX, 0ドルx123
JEQok
MOVLAX, 0// abort
ok:
// set up m and g "registers"
get_tls(BX)
LEALruntime·g0(SB), CX
MOVLCX, g(BX)
LEALruntime·m0(SB), AX
MOVLAX, m(BX)
// save m->g0 = g0
MOVLCX, m_g0(AX)
CALLruntime·emptyfunc(SB)// fault if stack check is wrong
// convention is D is always cleared
CLD
CALLruntime·check(SB)
// saved argc, argv
MOVL120(SP), AX
MOVLAX, 0(SP)
MOVL124(SP), AX
MOVLAX, 4(SP)
CALLruntime·args(SB)
CALLruntime·osinit(SB)
CALLruntime·hashinit(SB)
CALLruntime·schedinit(SB)
// create a new goroutine to start program
PUSHL$runtime·main·f(SB)// entry
PUSHL0ドル// arg size
ARGSIZE(8)
CALLruntime·newproc(SB)
ARGSIZE(-1)
POPLAX
POPLAX
// start this M
CALLruntime·mstart(SB)
INT 3ドル
RET
DATAruntime·main·f+0(SB)/4,$runtime·main(SB)
GLOBLruntime·main·f(SB),RODATA,4ドル
反汇编验证
随便编译一个GO程序,用W32ASM验证一下。
200419560 sub esp, 0000000C
:00419563 mov eax, dword pt: [esp+0C]
:O041967ドル lea ebx, dword ptr [esp+10]
:004196ドルB mov dword pct [esp+04], eax
:0041956F mnv dword pt: [esp+08], ebx
:00419S73 mov dword ptr [esp], FFFFFFFF
:004197ドルA jmp test.00419S80
.......
:00417C90 mov eax, dword pt: [esp+04]
:0O417C94 mov ebx, dword pct [esp+08]
200417098 sub esp, 00000080
:00417C9E and esp, FFFFFFFO
:00417CA1 mov dword ptr [esp+78], eax
:0O417CA9 mov ebp, 00447300
:00417CAE lea ebx, dword pt: [esp-0000FF98]
:00417CB5 mov dword pt: [ebp+40], ebx
:00417CB8 mov dword pt: [ebp+00], ebx
:0O417CBB mov dword pct [ebp+O4], esp
上面是rt0_windows_386.s部分,下面是asm_386.s部分。对比可知,这才是GO程序的真正入口。