本文转载自微信公众号《我的大脑是炸鱼》,作者陈建宇。转载本文请联系脑筋急转弯公众号。“HelloWorld”程序几乎是每个程序员入门和测试开发环境的基本标准。代码如下:#inclueintmain(){printf("HelloWolrd\n");return0;}编译程序,然后运行,基本完成了所有新手的第一个程序。从表面上看,它看起来很轻松,没有任何悬念。但实际上,光是这几个操作,就已经包含了很多隐藏操作。为了追根溯源,我们进一步分析其过程。涉及的过程主要包括四个步骤:预处理、编译、组装和链接。由于篇幅问题,本文主要涉及前三部分,链接部分将在下一篇文章进行讲解。预编译程序编译的第一步是“预编译”环境。主要功能是处理源代码文件中以“#”开头的预编译指令,如:#include、#define等。常见的处理规则是:删除所有#defines,展开所有宏定义。处理所有条件预编译指令,如if、ifdef、elif、else、endif。处理#include预编译指令,将包含的文件插入到预编译指令的位置(可以递归处理子导入)。删除所有评论。添加行号和文件名标识符,以便编译器在编译过程中产生编译错误或警告时生成行号信息以供调试和显示行号。保留所有#pragma编译器指令,后续编译器将使用这些指令。预编译后,该文件将不包含宏定义或导入。因为预编译后都会展开,所以相应的代码段已经插入到文件中了。和Go语言一样,gogenerate命令主要涉及相关的预编译处理。第二步编译正式进入“编译”环境。主要功能是对预处理后的文件进行一系列的词法分析、句法分析、语义分析和优化,生成相应的汇编代码文件。这部分通常是整个程序构建的核心部分,也是最复杂的部分之一。执行编译操作的工具通常被称为“编译器”。编译器是将高级语言翻译成机器语言的工具。比如我们平时用Go语言写的程序,编译器可以把它编译成机器可以执行的指令和数据。那么我们就不需要再关心相关的底层细节了,因为用机器指令或者汇编语言编写程序是一件非常耗时繁琐的事情。而且,高级语言可以让程序员更加关注程序逻辑本身,而不必再过多关注计算机本身的局限性。它具有更高的平台可移植性,可以运行在多种计算机体系结构下。编译过程编译过程一般分为6个步骤:扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化。整个过程如下:在编译过程中,我们将上图中从源代码(SourceCode)到最终目标代码(FinalTargetCode)的过程结合起来,用一个代理例子来重现和描述整个过程最简单的Go语言程序,如下:packagemainimport("fmt")funcmain(){fmt.Println("HelloWorld.")}词法分析所有解析程序的第一步,就是阅读源代码。扫描器的任务很简单,就是利用有限状态机对源代码的字符序列进行分割,最后变成一系列的记号(Token)。下面是go/scanner处理的HelloWorld:1:1package"package"1:9IDENT"main"1:13;"\n"3:1import"import"3:8(""4:2STRING"\"fmt\""4:7;"\n"5:1)""5:2;"\n"7:1func"func"7:6IDENT"main"7:10(""7:11)""7:13{""8:2IDENT"fmt"8:5.""8:6IDENT"Println"8:13(""8:14STRING"\"HelloWorld.\""8:28)""8:29;"\n"9:1}""9:2;"\n"经过扫描器扫描后,可以看到输出了很多Token。如果没有先验知识,乍一看可能会很迷惑。在这里可以初步了解Go中主要包括的标识符和基本类型,如下://SpecialtokensILLEGALToken=iotaEOFCOMMENT//Identifiersandbasictypeliterals//(thesetokensstandforclassesofliterals)IDENT//mainINT//12345FLOAT//123.45IMAG//123.45iCHAR//'a'STRING//"abc"literal_end根据输出的Token稍作思考和比较,我们可以知道它只是简单的由scanner翻译输出。实际上,扫描器在识别token时,还会完成其他的工作,比如将标识符放入符号表,将数字和字符串常量存入文本表等。词法分析生成的记号一般可以分为以下几类:关键字。标识符。文字(包含数字、字符串等)。特殊巧合(如加号、等号)语法分析/语义分析语法分析器(GrammarParser)会对扫描器生成的token进行语法分析,生成语法树(SyntaxTree),也称为抽象语法树(抽象语法树,AST)。常见的分析方法有自上而下或自下而上,使用上下文无关文法(Context-freeGrammer)作为分析方法。这块可以参考一些计算机原理方面的资料,涉及面很广。但是语法分析只是完成了表达的语法层面的分析,并不清楚语句是否真正有意义,需要进行语义分析。语义分析器语义分析器(SemanticAnalyzer)会在语法分析器生成的语法树上识别特定类型的表达式。主要分为两类:静态语义:可以在编译器中确定的语义。动态语义:只能在运行时确定的语义。在语义分析阶段之后,整个语法树的表达式将被标记类型。如果某些类型需要隐式转换,语义分析程序会在语法书中插入相应的转换点,成为具有更具体含义的语义。.实践练习解析器生成的语法树本质上是一棵以Expression为节点的树。在Go语言中,可以通过go/token、go/parser、go/ast等相关方法生成语法树。代码如下:funcmain(){src:=[]byte("packagemain\n\nimport(\n\t\"fmt\"\n)\n\nfuncmain(){\n\tfmt.Println(\"HelloWorld.\")\n}")fset:=token.NewFileSet()//positionsarerelativetofsetf,err:=parser.ParseFile(fset,"",src,0)iferr!=nil{panic(err)}ast.Print(fset,f)},经语法分析器(自上而下)分析后,输出结果如下:0*ast.File{1.Package:1:12.Name:*ast.Ident{3..NamePos:1:94..Name:"main"5.}6.Decls:[]ast.Decl(len=2){7..0:*ast.GenDecl{8...TokPos:3:19...Tok:import10...Lparen:3:811...规格:[]ast.Spec(len=1){12....0:*ast.ImportSpec{13.....Path:*ast.BasicLit{14...ValuePos:4:215...Kind:STRING16......值:"\"fmt\""17.......}18.....EndPos:-19....}20...}21...Rparen:5:122。.}23....71.}72.Scope:*ast.Scope{73..Objects:map[string]*ast.Object(len=1){74..."main":*(obj@27)75..}76.}77.Imports:[]*ast.ImportSpec(len=1){78..0:*(obj@12)79.}80.Unresolved:[]*ast.Ident(len=1){81..0:*(obj@46)82.}83}package:解析出package关键字的位置,1:1指的是第一行的第一个Name:解析出包名的名称,类型为*ast.Ident,1:9指的是第一行的第九个。Decls:节点的顶层声明,对应BadDecl(BadDeclaration)、GenDecl(GenericDeclaration)、FuncDecl(FunctionDeclaration)。作用域:该文件中的函数作用域,以及该作用域对应的对象。导入:在此文件中导入的模块。未解析:此文件中未解析的标识符。评论:此文件中的所有评论。可视化语法树如下:上面的可视化语法树主要涉及语法分析和语义分析,属于编译器的前端。最终的结果是语法树,通常称为抽象语法树(AST)。有兴趣的可以自己试试yuroyoro/goast-viewer,对语法树会有更清晰的认识。中间语言生成现代编译器有多个优化级别,通常在源代码级别。例如,可以优化一个简单的1+2表达式。在Go语言中,中间语言涉及静态单一赋值(SSA)的特性。比如有一个很简单的SayHelloWorld方法,如下:通过GOSSAFUNC查看:$GOSSAFUNC=SayHelloWorldgobuildhelloworld.go#command-line-argumentsdumpedSSAto./ssa.html打开ssa.html,可以看到这个文件的源代码对应的语法树,中间代码和最终的几个版本生成的SSA。SSA从左到右依次是:Sources(源代码)、AST(抽象语法树),从最右边第一列开始依次是第一轮中间语言(代码),依次是十几轮。生成中间语言后不能直接使用目标代码生成和优化。因为机器真正能执行的是机器码。这时候,就是编译器后端的工作了。从阶段上来说,当源代码级优化器产生中间代码时,说明后面的过程属于编译器后端。编译器后端主要包括以下两大类,其作用如下:代码生成器(CodeGenerator):代码生成器将中间代码转换为目标机器码。TargetCodeOptimizer:优化代码生成器转换后的目标机器码。在Go语言中,上述的十几轮SSA优化降级中都包含了上述行为。有兴趣的可以自己研究SSA,最后在genssa中可以看到最终的中间代码:此时最终降级的SSA的代码已经降级了,更接近最终的汇编代码,但是还没有被正式转换。组装完成,程序编译完成后,第三步就是“组装”。汇编器会将汇编代码转换成机器可执行的指令,每条汇编语句几乎对应一条机器指令。基本逻辑就是根据汇编指令和机器指令对照表,一个一个翻译。在Go语言中,genssa生成的目标代码已经过优化降级,然后会调用src/cmd/internal/obj包中的汇编器将SSA中间代码生成为机器码。Wecanviewitbygotoolcompile-S:$gotoolcompile-Shelloworld.go"".SayHelloWorldSTEXTnosplitsize=15args=0x10locals=0x00x000000000(helloworld.go:3)TEXT"".SayHelloWorld(SB),NOSPLIT|ABIInternal,$0-160x000000000(helloworld.go:3)FUNCDATA$0,gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)0x000000000(helloworld.go:3)FUNCDATA$1,gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)0x000000000(helloworld.go:4)MOVQ"".a+8(SP),AX0x000500005(helloworld.go:4)ADDQ$2,AX0x000900009(helloworld.go:5)MOVQAX,"".~r1+16(SP)0x000e00014(helloworld.go:5)RET0x0000488b4424084883c934H.481..H.D$..go.cuinfo.packagename.SDWARFINFOdupoksize=00x000068656c6c6f776f726c64helloworldgclocals·33cdeccccebe80329f1fdbee7f5874cbSRODATAdupoksize=80x00000100000000000000........至此就完成了一个高级语言再到计算机所能理解的机器码转换的完整流程了。SummaryInthisarticle,wehaveabasicunderstandingofhowanapplicationiscompiledfromsourcecodetofinalmachinecode,andeachstepofexpansionisaverylargepieceofbasiccomputerknowledge.Ifreadersareinterestedinit,theycanconductin-depthanalysisandunderstandingaccordingtothepracticalstepsinthearticle.在下一篇文章中,我们将进一步分析最后一步环节,了解它的最后一公里。