以下文章来源于腾讯技术工程,作者ivansli
在深入学习Golang的runtime和标准库实现的时候发现,如果对Golang汇编没有一定了解的话,很难深入了解其底层实现机制。在这里整理总结了一份基础的Golang汇编入门知识,通过学习之后能够对其底层实现有一定的认识。
0.为什么写本文
平时业务中一直使用PHP编写代码,但是一直对Golang比较感兴趣,闲暇、周末之余会看一些Go底层源码。
近日在分析go的某些特性底层功能实现时发现:有些又跟runtime运行时有关,而要掌握这一部分的话,有一道坎是绕不过去的,那就是Go汇编。索性就查阅了很多大佬们写的资料,在阅读之余整理总结了一下,并在这里分享给大家。
本文使用Go版本为go1.14.1
1.为什么需要汇编
众所周知,在计算机的世界里,只有2种类型。那就是:0和1。
计算机工作是由一系列的机器指令进行驱动的,这些指令又是一组二进制数字,其对应计算机的高低电平。而这些机器指令的集合就是机器语言,这些机器语言在最底层是与硬件一一对应的。
显而易见,这样的机器指令有一个致命的缺点:可阅读性太差(恐怕也只有天才和疯子才有能力把控得了)。
为了解决可读性的问题以及代码编辑的需求,于是就诞生了最接近机器的语言:汇编语言(在我看来,汇编语言更像一种助记符,这些人们容易记住的每一条助记符都映射着一条不容易记住的由0、1组成的机器指令。你觉得像不像域名与IP地址的关系呢?)。
1.1程序的编译过程
以C语言为例来说,从hello.c的源码文件到hello可执行文件,经过编译器处理,大致分为几个阶段:
编译器在不同的阶段会做不同的事情,但是有一步是可以确定的,那就是:源码会被编译成汇编,最后才是二进制。
2.程序与进程
源码经过编译之后,得到一个二进制的可执行文件。文件这两个字也就表明,目前得到的这个文件跟其他文件对比,除了是具有一定的格式(Linux中是ELF格式,即:可运行可链接。executablelinkableformate)的二进制组成,并没什么区别。
在Linux中文件类型大致分为7种:
b:块设备文件c:字符设备文件d:目录-:普通文件l:链接s:socketp:管道
通过上面可以看到,可执行文件main与源码文件main.go,都是同一种类型,属于普通文件。(当然了,在Unix中有一句很经典的话:一切皆文件)。
那么,问题来了:
什么是程序?什么是进程?2.1程序
维基百科告诉我们:程序是指一组指示计算机或其他具有消息处理能力设备每一步动作的指令,通常用某种程序设计语言编写,运行于某种目标体系结构上。
从某个层面来看,可以把程序分为静态程序、动态程序:静态程序:单纯的指具有一定格式的可执行二进制文件。动态程序:则是静态可执行程序文件被加载到内存之后的一种运行时模型(又称为进程)。
2.2进程
首先,要知道的是,进程是分配系统资源的最小单位,线程(带有时间片的函数)是系统调度的最小单位。进程包含线程,线程所属于进程。
创建进程一般使用fork方法(通常会有个拉起程序,先fork自身生成一个子进程。然后,在该子进程中通过exec函数把对应程序加载进来,进而启动目标进程。当然,实际上要复杂得多),而创建线程则是使用pthread线程库。
以32位Linux操作系统为例,进程经典的虚拟内存结构模型如下图所示:
其中,有两处结构是静态程序所不具有的,那就是运行时堆(heap)与运行时栈(stack)。
运行时堆从低地址向高地址增长,申请的内存空间需要程序员自己或者由GC释放。运行时栈从高地址向低地址增长,内存空间在当前栈桢调用结束之后自动释放(并不是清除其所占用内存中数据,而是通过栈顶指针SP的移动,来标识哪些内存是正在使用的)。
3.Go汇编
对于Go编译器而言,其输出的结果是一种抽象可移植的汇编代码,这种汇编(Go的汇编是基于Plan9的汇编)并不对应某种真实的硬件架构。Go的汇编器会使用这种伪汇编,再为目标硬件生成具体的机器指令。
伪汇编这一个额外层可以带来很多好处,最主要的一点是方便将Go移植到新的架构上。
相关的信息可以参考RobPike的TheDesignoftheGoAssembler。
要了解Go的汇编器最重要的是要知道Go的汇编器不是对底层机器的直接表示,即Go的汇编器没有直接使用目标机器的汇编指令。Go汇编器所用的指令,一部分与目标机器的指令一一对应,而另外一部分则不是。这是因为编译器套件不需要汇编器直接参与常规的编译过程。相反,编译器使用了一种半抽象的指令集,并且部分指令是在代码生成后才被选择的。汇编器基于这种半抽象的形式工作,所以虽然你看到的是一条MOV指令,但是工具链针对对这条指令实际生成可能完全不是一个移动指令,也许会是清除或者加载。也有可能精确的对应目标平台上同名的指令。概括来说,特定于机器的指令会以他们的本尊出现,然而对于一些通用的操作,如内存的移动以及子程序的调用以及返回通常都做了抽象。细节因架构不同而不一样,我们对这样的不精确性表示歉意,情况并不明确。汇编器程序的工作是对这样半抽象指令集进行解析并将其转变为可以输入到链接器的指令。
ThemostimportantthingtoknowaboutGo’sassembleristhatitisnotadirectrepresentationoftheunderlyingmachine.Someofthedetailsmappreciselytothemachine,butsomedonot.Thisisbecausethe