在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
Go 程序是怎样跑起来的
刚开始写这篇文章的时候,目标非常大,想要探索 Go 程序的一生:编码、编译、汇编、链接、运行、退出。它的每一步具体如何进行,力图弄清 Go 程序的这一生。 在这个过程中,我又复习了一遍《程序员的自我修养》。这是一本讲编译、链接的书,非常详细,值得一看!数年前,我第一次看到这本书的书名,就非常喜欢。因为它模仿了周星驰喜剧之王里出现的一本书 ——《演员的自我修养》。心向往之! 在开始本文之前,先推荐一位头条大佬的博客——《面向信仰编程》,他的 Go 编译系列文章,非常有深度,直接深入编译器源代码,我是看了很多遍了。博客链接可以从参考资料里获取。 理想很大,实现的难度也是非常大。为了避免砸了“深度解密”这个牌子,这次起了个更温和的名字,嘿嘿。 下面是文章的目录: 我们从一个
当我用我那价值 1800 元的 cherry 键盘潇洒地敲完上面的 hello world 代码时,保存在硬盘上的 用 vim 打开 hello.go 文件,在命令行模式下,输入命令: :%!xxd 就能在 vim 里以十六进制查看文件内容: ASCII码对照表|ASCII编码_911查询 http://ascii.911cha.com/ 最左边的一列代表地址值,中间一列代表文本对应的 ASCII 字符,最右边的列就是我们的代码。再在终端里执行 和 ASCII 字符表一对比,就能发现,中间的列和最右边的列是一一对应的。也就是说,刚刚写完的 hello.go 文件都是由 ASCII 字符表示的,它被称为 当然,更深入地看,计算机中的所有数据,像磁盘文件、网络中的数据其实都是一串比特位组成,取决于如何看待它。在不同的情景下,一个相同的字节序列可能表示成一个整数、浮点数、字符串或者是机器指令。 而像 hello.go 这个文件,8 个 bit,也就是一个字节看成一个单位(假定源程序的字符都是 ASCII 码),最终解释成人类能读懂的 Go 源码。 Go 程序并不能直接运行,每条 Go 语句必须转化为一系列的低级机器语言指令,将这些指令打包到一起,并以二进制磁盘文件的形式存储起来,也就是可执行目标文件。 从源文件到可执行目标文件的转化过程: 完成以上各个阶段的就是 Go 编译系统。你肯定知道大名鼎鼎的 GCC(GNU Compile Collection),中文名为 GNU 编译器套装,它支持像 C,C++,Java,Python,Objective-C,Ada,Fortran,Pascal,能够为很多不同的机器生成机器码。 可执行目标文件可以直接在机器上执行。一般而言,先执行一些初始化的工作;找到 main 函数的入口,执行用户写的代码;执行完成后,main 函数退出;再执行一些收尾的工作,整个过程完毕。 在接下来的文章里,我们将探索 编译链接概述Go 源码里的编译器源码位于 编译过程我比较喜欢用 IDE(集成开发环境)来写代码, Go 源码用的 Goland,有时候直接点击 IDE 菜单栏里的“运行”按钮,程序就跑起来了。这实际上隐含了编译和链接的过程,我们通常将编译和链接合并到一起的过程称为构建(Build)。 编译过程就是对源文件进行词法分析、语法分析、语义分析、优化,最后生成汇编代码文件,以 之后,汇编器会将汇编代码转变成机器可以执行的指令。由于每一条汇编语句几乎都与一条机器指令相对应,所以只是一个简单的一一对应,比较简单,没有语法、语义分析,也没有优化这些步骤。 编译器是将高级语言翻译成机器语言的一个工具,编译过程一般分为 6 步:扫描、语法分析、语义分析、源代码优化、代码生成、目标代码优化。下图来自《程序员的自我修养》: 词法分析通过前面的例子,我们知道,Go 程序文件在机器看来不过是一堆二进制位。我们能读懂,是因为 Goland 按照 ASCII 码(实际上是 UTF-8)把这堆二进制位进行了编码。例如,把 8个 bit 位分成一组,对应一个字符,通过对照 ASCII 码表就可以查出来。 当把所有的二进制位都对应成了 ASCII 码字符后,我们就能看到有意义的字符串。它可能是关键字,例如:package;可能是字符串,例如:“Hello World”。 词法分析其实干的就是这个。输入是原始的 Go 程序文件,在词法分析器看来,就是一堆二进制位,根本不知道是什么东西,经过它的分析后,变成有意义的记号。简单来说,词法分析是计算机科学中将字符序列转换为标记(token)序列的过程。 我们来看一下维基百科上给出的定义:
记号一般分为这几类:关键字、标识符、字面量(包含数字、字符串)、特殊符号(如加号、等号)。 例如,对于如下的代码: slice[i] = i * (2 + 6) 总共包含 16 个非空字符,经过扫描后,
上面的例子源自《程序员的自我修养》,主要讲解编译、链接相关的内容,很精彩,推荐研读。 Go 语言(本文的 Go 版本是 1.9.2)扫描器支持的 Token 在源码中的路径: src/cmd/compile/internal/syntax/token.go 感受一下:
还是比较熟悉的,包括名称和字面量、操作符、分隔符和关键字。 而扫描器的路径是: src/cmd/compile/internal/syntax/scanner.go 其中最关键的函数就是 next 函数,它不断地读取下一个字符(不是下一个字节,因为 Go 语言支持 Unicode 编码,并不是像我们前面举得 ASCII 码的例子,一个字符只有一个字节),直到这些字符可以构成一个 Token。
代码的主要逻辑就是通过
语法分析上一步生成的 Token 序列,需要经过进一步处理,生成一棵以 比如最开始的那个例子, 整个语句被看作是一个赋值表达式,左子树是一个数组表达式,右子树是一个乘法表达式;数组表达式由 2 个符号表达式组成;乘号表达式则是由一个符号表达式和一个加号表达式组成;加号表达式则是由两个数字组成。符号和数字是最小的表达式,它们不能再被分解,通常作为树的叶子节点。 语法分析的过程可以检测一些形式上的错误,例如:括号是否缺少一半,
语义分析语法分析完成后,我们并不知道语句的具体意义是什么。像上面的 编译期所能检查的是静态语义,可以认为这是在“代码”阶段,包括变量类型的匹配、转换等。例如,将一个浮点值赋给一个指针变量的时候,明显的类型不匹配,就会报编译错误。而对于运行期间才会出现的错误:不小心除了一个 0 ,语义分析是没办法检测的。 语义分析阶段完成之后,会在每个节点上标注上类型:
Go 语言编译器在这一阶段检查常量、类型、函数声明以及变量赋值语句的类型,然后检查哈希中键的类型。实现类型检查的函数通常都是几千行的巨型 switch/case 语句。
例如比较常用的 make 关键字,用它可以创建各种类型,如 slice,map,channel 等等。到这一步的时候,对于 make 关键字,也就是 OMAKE 节点,会先检查它的参数类型,根据类型的不同,进入相应的分支。如果参数类型是 slice,就会进入 TSLICE case 分支,检查 len 和 cap 是否满足要求,如 len <= cap。最后节点类型会从 OMAKE 改成 OMAKESLICE。 中间代码生成我们知道,编译过程一般可以分为前端和后端,前端生成和平台无关的中间代码,后端会针对不同的平台,生成不同的机器码。 前面词法分析、语法分析、语义分析等都属于编译器前端,之后的阶段属于编译器后端。 编译过程有很多优化的环节,在这个环节是指源代码级别的优化。它将语法树转换成中间代码,它是语法树的顺序表示。 中间代码一般和目标机器以及运行时环境无关,它有几种常见的形式:三地址码、P-代码。例如,最基本的
表示变量 y 和 变量 z 进行 op 操作后,赋值给 x。op 可以是数学运算,例如加减乘除。 前面我们举的例子可以写成如下的形式:
这里 2 + 6 是可以直接计算出来的,这样就把 t1 这个临时变量“优化”掉了,而且 t1 变量可以重复利用,因此 t2 也可以“优化”掉。优化之后:
Go 语言的中间代码表示形式为 SSA(Static Single-Assignment,静态单赋值),之所以称之为单赋值,是因为每个名字在 SSA 中仅被赋值一次。。 这一阶段会根据 CPU 的架构设置相应的用于生成中间代码的变量,例如编译器使用的指针和寄存器的大小、可用寄存器列表等。中间代码生成和机器码生成这两部分会共享相同的设置。 在生成中间代码之前,会对抽象语法树中节点的一些元素进行替换。这里引用《面向信仰编程》编译原理相关博客里的一张图: 例如对于 map 的操作 m[i],在这里会被转换成 mapacess 或 mapassign。
目标代码生成与优化不同机器的机器字长、寄存器等等都不一样,意味着在不同机器上跑的机器码是不一样的。最后一步的目的就是要生成能在不同 CPU 架构上运行的代码。 为了榨干机器的每一滴油水,目标代码优化器会对一些指令进行优化,例如使用移位指令代替乘法指令等。 这块实在没能力深入,幸好也不需要深入。对于应用层的软件开发工程师来说,了解一下就可以了。 链接过程编译过程是针对单个文件进行的,文件与文件之间不可避免地要引用定义在其他模块的全局变量或者函数,这些变量或函数的地址只有在此阶段才能确定。 链接过程就是要把编译器生成的一个个目标文件链接成可执行文件。最终得到的文件是分成各种段的,比如数据段、代码段、BSS段等等,运行时会被装载到内存中。各个段具有不同的读写、执行属性,保护了程序的安全运行。 这部分内容,推荐看《程序员的自我修养》和《深入理解计算机系统》。 Go 程序启动仍然使用 hello-world 项目的例子。在项目根目录下执行:
得到了可执行文件 hello,执行:
进入 gdb 调试模式,执行 同时,我们也得到了入口地址:0x450e20。
这就是 Go 程序的入口地址,我是在 linux 上运行的,所以入口文件为
主要是把 argc,argv 从内存拉到了寄存器。这里 LEAQ 是计算内存地址,然后把内存地址本身放进寄存器里,也就是把 argv 的地址放到了 SI 寄存器中。最后跳转到:
继续跳转到
参考文献里的一篇文章【探索 golang 程序启动过程】研究得比较深入,总结下:
最后用一张图来总结 go bootstrap 过程吧: main 函数里执行的一些重要的操作包括:新建一个线程执行 sysmon 函数,定期垃圾回收和调度抢占;启动 gc;执行所有的 init 函数等等。 上面是启动过程,看一下退出过程:
关于程序退出这一段的阐述来自群聊《golang runtime 阅读》,又是一个高阶的读源码的组织,github 主页见参考资料。 当然 Go 程序启动这一部分其实还会涉及到 fork 一个新进程、装载可执行文件,控制权转移等问题。还是推荐看前面的两本书,我觉得我不会写得更好,就不叙述了。 GoRoot 和 GoPathGoRoot 是 Go 的安装路径。mac 或 unix 是在
bin 目录下面:
pkg 目录下面:
Go 工具目录如下,其中比较重要的有编译器
GoPath 的作用在于提供一个可以寻找
src 存放源文件,pkg 存放源文件编译后的库文件,后缀为 Go 命令详解直接在终端执行:
就能得到和 go 相关的命令简介:
和编译相关的命令主要是:
go build
通过执行
至于 build flags 参数,
我们知道,Go 语言的源码文件分为三类:命令源码、库源码、测试源码。
注意, 我们通过一个很简单的例子来演示
最左边可以看到项目的结构,包含三个文件夹:bin,pkg,src。其中 src 目录下有一个 main.go,里面定义了 main 函数,是整个项目的入口,也就是前面提过的所谓的命令源码文件;src 目录下还有一个 util 目录,里面有 util.go 文件,定义了一个可以获取本机 IP 地址的函数,也就是所谓的库源码文件。 中间是 main.go 的源码,引用了两个包,一个是标准库的 fmt;一个是 util 包,util 的导入路径是 最右边是库函数的源码,实现了获取本机 IP 的函数。 在 src 目录下,直接执行
我们也可以指定生成的可执行文件的名称:
这样,在 bin 目录下会生成一个可执行文件,运行结果和上面的 其实,util 包可以单独被编译。我们可以在项目根目录下执行:
编译程序会去 $GoPath/src 路径找 util 包(其实是找文件夹)。还可以在 当然,直接编译库源码文件不会生成 .a 文件,因为:
为了展示整个编译链接的运行过程,我们在项目根目录执行如下的命令:
执行结果:
从结果来看,图中用箭头标注了本次编译过程涉及 2 个包:util,command-line-arguments。第二个包比较诡异,源码里根本就没有这个名字好吗?其实这是 同时,用红框圈出了 compile, link,也就是先编译了 util 包和 另外,第一行显示了编译过程中的工作目录,此目录的文件结构是:
可以看到,和 hello-world 目录的层级基本一致。command-line-arguments 就是虚拟的 main.go 文件所处的包。exe 目录下的可执行文件在最后一步被移动到了 bin 目录下,所以这里是空的。 整体来看, 正常情况下,这些依赖关系会形成一棵倒着生长的树,树根在最上面,就是 main.go 文件,最下面是没有任何其他依赖的包。编译器会从最左的节点所代表的包开始挨个编译,完成之后,再去编译上一层的包。 这里,引用郝林老师几年前在 github 上发表的 go 命令教程,可以从参考资料找到原文地址。
顺便推荐一个浏览器插件 Octotree,在看 github 项目的时候,此插件可以在浏览器里直接展示整个项目的文件结构,非常方便:
到这里,你一定会发现,对于 hello-wrold 文件夹下的 pkg 目录好像一直没有涉及到。 其实,pkg 目录下面应该存放的是涉及到的库文件编译后的包,也就是一些 前面我们提到过,在 go build 命令里加上 在项目根目录执行
生成了 util.a 文件后,再次编译的时候,就不会再重新编译 util.go 文件,加快了编译速度。 同时,在根目录下生成了名称为 main 的可执行文件,这是以 main.go 的文件名命令的。 hello-world 这个项目的代码已经上传到了 github 项目 go install
还是使用之前 hello-world 项目的例子,我们先将 pkg 目录删掉,在项目根目录执行:
两者都会在根目录下新建一个 并且,在执行前者的时候,会在 GOBIN 目录下生成名为 main 的可执行文件。 所以,运行
go run
在 hello-world 项目的根目录,执行 go run 命令:
-x 可以打印整个过程涉及到的命令,-work 可以看到临时的工作目录:
从上图中可以看到,仍然是先编译,再连接,最后直接执行,并打印出了执行结果。 第一行打印的就是工作目录,最终生成的可执行文件就是放置于此:
main 就是最终生成的可执行文件。 总结这次的话题太大了,困难重重。从编译原理到 go 启动时的流程,到 go 命令原理,每个话题单独抽出来都可以写很多。 幸好有一些很不错的书和博客文章可以去参考。这篇文章就作为一个引子,你可以跟随参考资料里推荐的一些内容去发散。 参考资料【《程序员的自我修养》全书】https://book.douban.com/subject/3652388/ 【面向信仰编程 编译过程概述】https://draveness.me/golang-compile-intro 【golang runtime 阅读】https://github.com/zboya/golangruntimereading 【Go-Questions hello-world项目】https://github.com/qcrao/Go-Questions/tree/master/examples/hello-world 【雨痕大佬的 Go 语言学习笔记】https://github.com/qyuhen/book 【vim 以 16 进制文本】https://www.cnblogs.com/meibenjin/archive/2012/12/06/2806396.html 【Go 编译命令执行过程】https://halfrost.com/go_command/ 【Go 命令执行过程】https://github.com/hyper0x/gocommandtutorial 【Go 词法分析】https://ggaaooppeenngg.github.io/zh-CN/2016/04/01/go-lexer-%E8%AF%8D%E6%B3%95%E5%88%86%E6%9E%90/ 【曹大博客 golang 与 ast】http://xargin.com/ast/ 【Golang 词法解析器,scanner 源码分析】https://blog.csdn.net/zhaoruixiang1111/article/details/89892435 【Gopath Explained】https://flaviocopes.com/go-gopath/ 【Understanding the GOPATH】https://www.digitalocean.com/community/tutorials/understanding-the-gopath 【讨论】https://stackoverflow.com/questions/7970390/what-should-be-the-values-of-gopath-and-goroot 【Go 官方 Gopath】https://golang.org/cmd/go/#hdr-GOPATHenvironmentvariable 【Go package 的探索】https://mp.weixin.qq.com/s/OizVLXfZ6EC1jI-NL7HqeA 【Go 官方 关于 Go 项目的组织结构】https://golang.org/doc/code.html 【Go modules】https://www.melvinvivas.com/go-version-1-11-modules/ 【Golang Installation, Setup, GOPATH, and Go Workspace】https://www.callicoder.com/golang-installation-setup-gopath-workspace/ 【编译、链接过程链接】https://mikespook.com/2013/11/%E7%BF%BB%E8%AF%91-go-build-%E5%91%BD%E4%BB%A4%E6%98%AF%E5%A6%82%E4%BD%95%E5%B7%A5%E4%BD%9C%E7%9A%84%EF%BC%9F/ 【1.5 编译器由 go 语言完成】https://www.infoq.cn/article/2015/08/go-1-5 【Go 编译过程系列文章】https://www.ctolib.com/topics-3724.html 【曹大 go bootstrap】https://github.com/cch123/golang-notes/blob/master/bootstrap.md 【golang 启动流程】https://blog.iceinto.com/posts/go/start/ 【探索 golang 程序启动过程】http://cbsheng.github.io/posts/%E6%8E%A2%E7%B4%A2golang%E7%A8%8B%E5%BA%8F%E5%90%AF%E5%8A%A8%E8%BF%87%E7%A8%8B/ 【探索 goroutine 的创建】http://cbsheng.github.io/posts/%E6%8E%A2%E7%B4%A2goroutine%E7%9A%84%E5%88%9B%E5%BB%BA/
原文地址链接: Go 程序是怎样跑起来的-码农桃花源 https://mp.weixin.qq.com/s/Rewl0DKnq6CY53m5D3G2qw Go 程序是怎样跑起来的 - Stefno - 博客园 https://www.cnblogs.com/qcrao-2018/p/11124360.html |
请发表评论