万字长文详解 Go 程序是怎样跑起来的?( 四 )
文章插图
例如对于 map 的操作 m[i],在这里会被转换成 mapacess 或 mapassign 。
Go 语言的主程序在执行时会调用 runtime 中的函数,也就是说关键字和内置函数的功能其实是由语言的编译器和运行时共同完成的 。
中间代码的生成过程其实就是从 AST 抽象语法树到 SSA 中间代码的转换过程,在这期间会对语法树中的关键字在进行一次更新,更新后的语法树会经过多轮处理转变最后的 SSA 中间代码 。
目标代码生成与优化
不同机器的机器字长、寄存器等等都不一样,意味着在不同机器上跑的机器码是不一样的 。最后一步的目的就是要生成能在不同 CPU 架构上运行的代码 。
为了榨干机器的每一滴油水,目标代码优化器会对一些指令进行优化,例如使用移位指令代替乘法指令等 。
这块实在没能力深入,幸好也不需要深入 。对于应用层的软件开发工程师来说,了解一下就可以了 。
链接过程
编译过程是针对单个文件进行的,文件与文件之间不可避免地要引用定义在其他模块的全局变量或者函数,这些变量或函数的地址只有在此阶段才能确定 。
链接过程就是要把编译器生成的一个个目标文件链接成可执行文件 。最终得到的文件是分成各种段的,比如数据段、代码段、BSS段等等,运行时会被装载到内存中 。各个段具有不同的读写、执行属性,保护了程序的安全运行 。
这部分内容,推荐看《程序员的自我修养》和《深入理解计算机系统》 。
3.Go 程序启动
仍然使用 hello-world 项目的例子 。在项目根目录下执行:
go build -gcflags "-N -l" -o hello src/main.go
-gcflags"-N -l" 是为了关闭编译器优化和函数内联,防止后面在设置断点的时候找不到相对应的代码位置 。
得到了可执行文件 hello,执行:
[qcrao@qcrao hello-world]$ gdb hello
进入 gdb 调试模式,执行 info files,得到可执行文件的文件头,列出了各种段:

文章插图
同时,我们也得到了入口地址:0x450e20 。
(gdb) b *0x450e20 Breakpoint 1 at 0x450e20: file /usr/local/go/src/runtime/rt0_linux_amd64.s, line 8.
这就是 Go 程序的入口地址,我是在 linux 上运行的,所以入口文件为 src/runtime/rt0_linux_amd64.s,runtime 目录下有各种不同名称的程序入口文件,支持各种操作系统和架构,代码为:TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8 LEAQ 8(SP), SI // argvMOVQ 0(SP), DI // argcMOVQ $main(SB), AXJMP AX
主要是把 argc,argv 从内存拉到了寄存器 。这里 LEAQ 是计算内存地址,然后把内存地址本身放进寄存器里,也就是把 argv 的地址放到了 SI 寄存器中 。最后跳转到:TEXT main(SB),NOSPLIT,$-8 MOVQ $runtime·rt0_go(SB), AXJMP AX
继续跳转到 runtime·rt0_go(SB),位置:/usr/local/go/src/runtime/asm_amd64.s,代码:TEXT runtime·rt0_go(SB),NOSPLIT,$0 // 省略很多 CPU 相关的特性标志位检查的代码// 主要是看不懂,^_^// ………………………………// 下面是最后调用的一些函数,比较重要// 初始化执行文件的绝对路径CALL runtime·args(SB)// 初始化 CPU 个数和内存页大小CALL runtime·osinit(SB)// 初始化命令行参数、环境变量、gc、栈空间、内存管理、所有 P 实例、HASH算法等CALL runtime·schedinit(SB)// 要在 main goroutine 上运行的函数MOVQ $runtime·mainPC(SB), AX // entryPUSHQ AXPUSHQ $0 // arg size// 新建一个 goroutine,该 goroutine 绑定 runtime.main,放在 P 的本地队列,等待调度CALL runtime·newproc(SB)POPQ AXPOPQ AX// 启动M,开始调度goroutineCALL runtime·mstart(SB)MOVL $0xf1, 0xf1 // crashRETDATA runtime·mainPC+0(SB)/8,$runtime·main(SB)GLOBL runtime·mainPC(SB),RODATA,$8
参考文献里的一篇文章【探索 golang 程序启动过程】研究得比较深入,总结下:- 检查运行平台的CPU,设置好程序运行需要相关标志 。
- TLS的初始化 。
- runtime.args、runtime.osinit、runtime.schedinit 三个方法做好程序运行需要的各种变量与调度器 。
- runtime.newproc创建新的goroutine用于绑定用户写的main方法 。
- runtime.mstart开始goroutine的调度 。

文章插图
main 函数里执行的一些重要的操作包括:新建一个线程执行 sysmon 函数,定期垃圾回收和调度抢占;启动 gc;执行所有的 init 函数等等 。
推荐阅读
- 详解3种区别Linux服务器是物理机或者虚拟机的方法
- 详解Docker可视化管理工具shipyard--部署教程及功能展示
- 绝对经典,看了必会 linux中部署mysql主从同步示例详解
- 详解Oracle数据库物理设计--表和索引设计建议
- 详解微服务技术中进程间通信
- ”什么是内网穿透“详解
- 局域网中NAT具体工作过程详解
- MySQL执行计划命令EXPLAIN详解
- 静态路由详解
- 计算机启动过程详解