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

相关推荐
摇滚侠18 小时前
Spring Boot 3零基础教程,IOC容器中组件的注册,笔记08
spring boot·笔记·后端
程序员小凯20 小时前
Spring Boot测试框架详解
java·spring boot·后端
你的人类朋友21 小时前
什么是断言?
前端·后端·安全
程序员小凯1 天前
Spring Boot缓存机制详解
spring boot·后端·缓存
i学长的猫1 天前
Ruby on Rails 从0 开始入门到进阶到高级 - 10分钟速通版
后端·ruby on rails·ruby
用户21411832636021 天前
别再为 Claude 付费!Codex + 免费模型 + cc-switch,多场景 AI 编程全搞定
后端
茯苓gao1 天前
Django网站开发记录(一)配置Mniconda,Python虚拟环境,配置Django
后端·python·django
Cherry Zack1 天前
Django视图进阶:快捷函数、装饰器与请求响应
后端·python·django
爱读源码的大都督1 天前
为什么有了HTTP,还需要gPRC?
java·后端·架构
码事漫谈1 天前
致软件新手的第一个项目指南:阶段、文档与破局之道
后端