聊聊Go程序是如何运行的

写在文章开头

Go语言是一门编译语言,其工作过程即直接通过编译生成各大操作系统的机器码即可直接执行,所以这篇文章笔者就从底层汇编码的角度聊一聊Go语言是如何运行的。

Hi,我是 sharkChili ,是个不断在硬核技术上作死的 java coder ,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili

因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 "加群" 即可和笔者和笔者的朋友们进行深入交流。

go语言代码执行详解

最终执行代码

我们首先在goland上创建一个名为main.go的文件,代码格式指明未main包下的main方法,当go语言完成编译并启动后,就会执行到这段代码:

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方法:

scss 复制代码
//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协程初始化。

scss 复制代码
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也是汇编方法,所以全局搜索后我们再次定位到该方法的实现,在这里笔者给出几个核心步骤:

  1. 将参数入栈,用于后续的各种初始化和启动操作所用。
  2. 调用check进行程序启动前必要的检查操作。
  3. 拷贝上述入栈的参数进行系统参数初始化。
  4. 初始化全局调度器。
  5. 创建协程g0等待线程绑定并运行main方法。
  6. 启动线程即M绑定协程g0,执行main方法。

对应核心代码如下:

scss 复制代码
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、指针、原子类的进行正确性的检查操作。

scss 复制代码
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,它会获取当前操作系统核心数、系统字长等基本信息:

scss 复制代码
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、环境变量、垃圾回收器等各个信息的初始化:

scss 复制代码
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方法,也就是我们编写入口代码:

go 复制代码
// 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程序运行大体分为以下几个步骤:

  1. 参考拷贝并入栈
  2. 类型检查
  3. 系统信息初始化
  4. 主调度器初始化
  5. 创建协程g0分配main方法的调用
  6. 创建线程绑定g0
  7. 通过协程g0的main方法调用我们的main方法,程序启动并运行

我是 sharkchiliCSDN Java 领域博客专家开源项目---JavaGuide contributor ,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili 。 因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 "加群" 即可和笔者和笔者的朋友们进行深入交流。

相关推荐
cjy00011121 分钟前
springboot的 nacos 配置获取不到导致启动失败及日志不输出问题
java·spring boot·后端
小江的记录本1 小时前
【事务】Spring Framework核心——事务管理:ACID特性、隔离级别、传播行为、@Transactional底层原理、失效场景
java·数据库·分布式·后端·sql·spring·面试
sheji34161 小时前
【开题答辩全过程】以 基于springboot的校园失物招领系统为例,包含答辩的问题和答案
java·spring boot·后端
程序员cxuan1 小时前
人麻了,谁把我 ssh 干没了
人工智能·后端·程序员
wuyikeer3 小时前
Spring Framework 中文官方文档
java·后端·spring
Victor3563 小时前
MongoDB(61)如何避免大文档带来的性能问题?
后端
Victor3563 小时前
MongoDB(62)如何避免锁定问题?
后端
wuyikeer4 小时前
Spring BOOT 启动参数
java·spring boot·后端
子木HAPPY阳VIP4 小时前
Ubuntu 22.04 VMware 设置固定IP配置
人工智能·后端·目标检测·机器学习·目标跟踪
人间打气筒(Ada)5 小时前
如何基于 Go-kit 开发 Web 应用:从接口层到业务层再到数据层
开发语言·后端·golang