图文详解Go程序如何编译并运行起来的
目录
- Go程序是如何编译的
- 从hello RdrB1te开始
- Go 编译过程
- Go程序是如何运行起来的
- Go程序的入口?
- 总结
Go程序是如何编译的
从hello RdrB1te开始
package main import "fmt" func main() { fmt.Println("hello RdrB1te") }
不实际编译它,只输出它的编译过程:
go build -n
简单的编译过程分析:
上面的过程确认了两个事情:
- Runtime会永远随着用户代码一起编译
- 在Windows平台上编译出来了一个exe的可执行文件
Go 编译过程
词法分析
- 将源代码翻译成Token
- Token是代码中的最小语义结构(如变量名、关键字、运算符等不可拆分的最小单元)
句法分析
- Token序列经过处理,变成语法树
语义分析
- 类型检查
- 类型推断
- 查看类型是否匹配
- 函数调用内联
- 逃逸分析
中间码生成:
- 为了处理不同平台的差异,先生成中间代码(SSA)
查看从代码到中间码(SSA)生成的整个过程
$env:GOSSAFUNC="main" # windows PowerShell export GOSSAFUNC=main # linu编程x go build
会看到如下输出:
用浏览器打开ssa.html文件:
sources就是你的源代码,AST就是生成的语法树,genssa就是生成的与平台无关的中间码SSA,当然中间还有很多的其它步骤,这里不再列举,可以点击展开查看
机器码生成:
- 先生成Plan9汇编代码(与平台相关)
- 最后编译为机器码
- 输出的机器码为.a文件
查看Plan9汇编代码
go build -gcflags -S main.go
链接:
- 将各个包进行链接,包括runtime,最终生成可执行文件
Go程序是如何运行起来的
Go程序的入口?
是下面的main方法吗?当然不是
func main() { fmt.Println("hello RdrB1te") }
是runtime包下面的rt0_xxx.s文件,下面以linux x86芯片架构上面运行的rt0_linux-amd64.s举例:
TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8 JMP _rt0_amd64(SB)
只要用了x86芯片架构都要进入到_rt0_amd64
这个方法中去,这个方法调到了哪里呢,选中双击shift
,打开在文件中查找:找到下面这行
在asm_amd64.s
这个文件中的这段代码:
TEXT _rt0_amd64(SB),NOSPLIT,$-8 MOVQ 0(SP), DI // argc LEAQ 8(SP), SI // argv JMP runtimert0_go(SB)
意思是读取命令行参数,复制参数数量argc
和参数值argv
到栈上,然后调用了http://www.devze.comruntime·rt0_go
这个方法,这方法的位置就在这个文件的下面:
TEXT runtimert0_go(SB),NOSPLIT|TOPFRAME,$0 // copy arguments forward on an even stack 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) // create istack out of the given (operating system) stack. js // _cgo_init may update stackguard. MOVQ $runtimeg0(SB), DI
上面这段的意思时初始化g0执行栈,g0是为了调度协程而产生的协程,g0是每个Go程序的第一个协程。继续往下面看,找到下面这段:
CALL runtimecheck(SB)
这行是第一次调用的go语言方法,要找到这个方法可以选中双击shift
,找到下面这行:
进入:
func check(){ }
check方法主要是做运行时检测:
- 检查各种类型的长度
- 检查指针操作
- 检查结构体字段的偏移量
- 检查atomic原子操作
- 检查CAS操作
- 检查栈大小是否是2的幂次
继续往下看,可以通过Ctrl+Alt+左右箭头
进行快速跳转回退或前进,退到这个位置:
CALL runtimecheck(SB) MOVL 24(SP), AX // copy argc MOVL AX, 0(SP) MOVQ 32(SP), AX // copy argv MOVQ AX, 8(SP) CALL runtimeargs(SB) CALL runtimeosinit(SB) CALL runtimeschedinit(SB) // create a new goroutine to start program MOVQ $runtimemainPC(SB), AX // entry PUSHQ AX CALL runtimenewproc(SB) POPQ AX
runtime·args(SB)
:参数初始化runtime.args
,对命令行中的参数进行处理,参数数量赋值给argc int32
,参数值复制给argv **byte
runtime·osinit
:判断操作系统,执行相应的初始化组件,供调度器初始化所用
runtime·schedinit
: 初始化Go调度器。初始化调度器会做哪些事情:
- 全局栈空间内存分配
- 加载命令行参数到 os.Args
- 堆内存空间的初始化
- 加载操作系统环境变量
- 初始化当前系统线程
- 垃圾回收器的参数初始化
- 算法初始化(ma编程客栈p、hash)
- 设置 process 数量
继续往下看:
// create a new goroutine to start program MOVQ $runtimemainPC(SB), AX // entry PUSHQ AX CALL runtimenewproc(SB) POPQ AX // start this M CALL runtimemstart(SB) CALL runtimeabort(SB) // mstart should never return RET // mainPC is a function value for runtime.main, to be passed to newproc. // The reference to runtime.main is made via ABIInternal, since the // actual function (not the ABI0 wrapper) is needed by newproc. DATA runtimemainPC+0(SB)/8,$runtimemain<ABIInternal>(SB)
MOVQ $runtime·mainPC(SB)
:取mainPC
的地址,这个mainPC
的地址就是runtime·main
这个方法的地址
CALL runtime·newproc
:创建一个新的协程(主协程),执行runtime·main
这个方法(主函数),放入调度器等待调度CALL runtime·mstart(SB)
:初始化一个M,用来调度主协程,主协程开始执行主函数。
看下runtime·main
这个方法里面干了什么,选中双击shift
,找到下面这行:
进入:
// The main goroutine. func main() { doInit(&runtime_inittask) // 执行runtime包中的init方法 gcenable() // 启动GC垃圾回收器 doInit(&main_inittask) //执行用户包依赖的init方法 fn := main_main // 执行用户主函数main.mian() fn() }
按住ctrl
进入main_main
:
//go:linkname main_main main.main func mai编程n_main()
主协程执行主函数:
- 执行runtime包中的init方法
- 启动GC垃圾回收器
- 执行用户包依赖的init方法
- 执行用户主函数
main.mian()
总结
- Go启动时经历了检查、各种初始化、初始化协程调度的过程
main.main()
也是在协程中运行的
到此这篇关于Go程序如何编译并运行起来的文章就介绍到这了,更多相关Go编译运行内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!
精彩评论