写在文章开头
Go语言
是一门编译语言,其工作过程即直接通过编译生成各大操作系统的机器码即可直接执行,所以这篇文章笔者就从底层汇编码的角度聊一聊Go语言
是如何运行的。
Hi,我是 sharkChili ,是个不断在硬核技术上作死的 java coder ,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili 。
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。
go语言代码执行详解
最终执行代码
我们首先在goland
上创建一个名为main.go
的文件,代码格式指明未main包下的main方法,当go语言完成编译并启动后,就会执行到这段代码:
package main
import (
"fmt"
)
func main() {
fmt.Println("hello Go")
}
入口跳转
go语言
是跨平台的语言,所以底层对各大平台的启动都做了特定的封装,以笔者的windows
系统为例,其执行入口为rt0_windows_amd64.s
,同理Linux
系统则是rt0_linux_amd64.s
,可以看到在任何平台它们都会通过汇编指令JMP
跳转到_rt0_amd64
方法:
//windows的入口代码_rt0_amd64_windows
TEXT _rt0_amd64_windows(SB),NOSPLIT,$-8
JMP _rt0_amd64(SB)
//Linux入口代码_rt0_amd64_linux
TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
JMP _rt0_amd64(SB)
拷贝参数,启动协程
通过全局搜索看到这个方法的实现,这段代码会通过MOVQ
将参数的数量argc
拷贝到目标寄存器DI
上,然后再通过LEAQ
计算所有参数argv
的偏移量地址并存储到SI
上,然后再次跳转到runtime·rt0_go
方法准备利用上述寄存的参数完成g0
协程初始化。
TEXT _rt0_amd64(SB),NOSPLIT,$-8
MOVQ 0(SP), DI // argc
LEAQ 8(SP), SI // argv
JMP runtime·rt0_go(SB)
启动g0运行main方法
因为runtime·rt0_go
也是汇编方法,所以全局搜索后我们再次定位到该方法的实现,在这里笔者给出几个核心步骤:
- 将参数入栈,用于后续的各种初始化和启动操作所用。
- 调用
check
进行程序启动前必要的检查操作。 - 拷贝上述入栈的参数进行系统参数初始化。
- 初始化全局调度器。
- 创建协程
g0
等待线程绑定并运行main
方法。 - 启动线程即M绑定协程
g0
,执行main
方法。
对应核心代码如下:
TEXT runtime·rt0_go(SB),NOSPLIT|TOPFRAME,$0
// 参数入栈
MOVQ DI, AX // argc
MOVQ SI, BX // argv
SUBQ $(5*8), SP // 3args 2auto
ANDQ $~15, SP
MOVQ AX, 24(SP)
MOVQ BX, 32(SP)
//......
// 初始化协程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)
//......
//调用check进行运行时检查
CALL runtime·check(SB)
//......
//拷贝argc和argv
MOVL 24(SP), AX // copy argc
MOVL AX, 0(SP)
MOVQ 32(SP), AX // copy argv
MOVQ AX, 8(SP)
CALL runtime·args(SB)
//完成系统参数初始化,例如系统字长,CPU核心数等信息初始化
CALL runtime·osinit(SB)
//初始化调度器
CALL runtime·schedinit(SB)
// runtime·mainPC代表我们程序执行的main函数的地址值,下述方法会通过创建一个协程g0来调用这个方法
MOVQ $runtime·mainPC(SB), AX // entry
PUSHQ AX
CALL runtime·newproc(SB)
POPQ AX
// 启动一个线程绑定上述的协程,自此调度器开始工作直接执行main方法
CALL runtime·mstart(SB)
CALL runtime·abort(SB) // mstart should never return
RET
我们先来看看运行时检查的步骤,这段代码在runtime1.go上,从笔者贴出的代码不难看出,这个方法会在程序运行进行类型长度、CAS、指针、原子类的进行正确性的检查操作。
func check() {
//......
if unsafe.Sizeof(a) != 1 {
throw("bad a")
}
//......
if timediv(12345*1000000000+54321, 1000000000, &e) != 12345 || e != 54321 {
throw("bad timediv")
}
//......
if !atomic.Cas(&z, 1, 2) {
throw("cas1")
}
//......
m = [4]byte{
1, 1, 1, 1}
atomic.Or8(&m[1], 0xf0)
if m[0] != 1 || m[1] != 0xf1 || m[2] != 1 || m[3] != 1 {
throw("atomicor8")
}
//......
*(*uint64)(unsafe.Pointer(&j)) = ^uint64(0)
if j == j {
throw("float64nan")
}
if !(j != j) {
throw("float64nan1")
}
//......
if _FixedStack != round2(_FixedStack) {
throw("FixedStack is not power-of-2")
}
if !checkASM() {
throw("assembly checks failed")
}
}
检查之后就是osinit,它会获取当前操作系统核心数、系统字长等基本信息:
func osinit() {
asmstdcallAddr = unsafe.Pointer(abi.FuncPCABI0(asmstdcall))
setBadSignalMsg()
loadOptionalSyscalls()
disableWER()
initExceptionHandler()
initHighResTimer()
timeBeginPeriodRetValue = osRelax(false)
initLongPathSupport()
ncpu = getproccount()
physPageSize = getPageSize()
// Windows dynamic priority boosting assumes that a process has different types
// of dedicated threads -- GUI, IO, computational, etc. Go processes use
// equivalent threads that all do a mix of GUI, IO, computations, etc.
// In such context dynamic priority boosting does nothing but harm, so we turn it off.
stdcall2(_SetProcessPriorityBoost, currentProcess, 1)
}
完成检查后就调用schedinit进行调度器初始化,从各个函数的语义即可知晓它会进行一次STW
然后进行堆栈、cpu、环境变量、垃圾回收器等各个信息的初始化:
func schedinit() {
// The world starts stopped.
worldStopped()
moduledataverify()
stackinit()
mallocinit()
godebug := getGodebugEarly()
initPageTrace(godebug) // must run after mallocinit but before anything allocates
cpuinit(godebug) // must run before alginit
goargs()
goenvs()
secure()
parsedebugvars()
gcinit()
//......
}
最终就是创建一个线程绑定协程g0
,调用$runtime·mainPC(SB)
从而拿到g0
协程的main
方法,最终定位到我们实现的main
包下的main
方法main_main
,这一点我们可以定位main_main的注释知晓(go:linkname main_main main.main)
,这个main_main指向的是被链接的main包下的main方法,也就是我们编写入口代码:
// The main goroutine.
func main() {
//我们实现的main包下的main方法
fn := main_main // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
fn()
}
以终为始印证观点
基于文章开头给出的代码断点,通过堆栈调用的信息可以看到,g0
的main方法确实通过main_main
定位我们编写的main
方法并完成执行:
小结
碍于篇幅等原因,笔者对go程序的运行仅做了简单的介绍,总体来说go程序运行大体分为以下几个步骤:
- 参考拷贝并入栈
- 类型检查
- 系统信息初始化
- 主调度器初始化
- 创建协程g0分配main方法的调用
- 创建线程绑定g0
- 通过协程g0的main方法调用我们的main方法,程序启动并运行
我是 sharkchili ,CSDN Java 领域博客专家,开源项目—JavaGuide contributor,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili 。
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。