文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

应用编译,计算机中一定要掌握的知识细节

2024-12-03 18:06

关注

本文转载自微信公众号「脑子进煎鱼了」,作者陈煎鱼。转载本文请联系脑子进煎鱼了公众号。  

 “Hello World” 程序几乎是每个程序员入门和开发环境测试的基本标准。代码如下:

  1. #inclue  
  2.  
  3. int main() 
  4.  printf("Hello Wolrd\n"); 
  5.  return 0; 

编译该程序,再运行,就基本完成了所有新手的第一个程序。表面看起来轻轻松松,毫无悬念。但是实际上单纯这几下操作,就已经包含了不少暗操作。本着追根溯源的目的,我们将进一步对其流程进行分析。

 

 


涉及的流程

 

 

其内部主要包含 4 个步骤,分别是:预处理、编译、汇编以及链接。由于篇幅问题本文主要涉及前三部分,链接部分将会放到下一篇文章来讲解。

预编译

程序编译的第一步是 “预编译” 环境。主要作用是处理源代码文件中以 ”#“ 开始的预编译指令,例如:#include、#define 等。

常见的处理规则是:

在预编译后,文件中将不包含宏定义或引入。因为在预编译后将会全部展开,相应的代码段均已被插入文件中。

像 Go 语言中的话,主要是 go generate 命令会涉及到相关的预编译处理。

编译

第二步正式进入到 "编译" 环境。主要作用是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生成相应的汇编代码文件。该部分通常是整个程序构建的核心部分,也是最复杂的部分之一。

执行编译操作的工具,一般称其为 “编译器”。编译器是将高级语言翻译成机器语言的一个工具。例如我们平时用 Go 语言写的程序,编译器就可以将其编译成机器可以执行的指令及数据。那么我们就不需要再去关心相关的底层细节,因为使用机器指令或汇编语言编写程序是一件十分费时及乏味的事情。

且高级语言能够使得程序员更关注程序逻辑的本身,不再需要过多的关注计算机本身的限制,具有更高的平台可移植性,能够在多种计算机架构下运行。

编译过程

编译过程一般分为 6 步:扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化。整个过程如下:

 

 


编译过程

 

 

我们结合上图的源代码(Source Code)到最终目标代码(Final Target Code)的过程,以一段最简单的 Go 语言程序的代理例子来复现和讲述整个过程,如下:

  1. package main 
  2.  
  3. import ( 
  4.  "fmt" 
  5.  
  6. func main() { 
  7.  fmt.Println("Hello World."

词法分析

首先 Go 程序会被输入到扫描器中,可以理解为所有解析程序的第一步,都是读取源代码。而扫描器的任务很简单,就是利用有限状态机对源代码的字符序列进行分割,最终变成一系列的记号(Token)。

如下 Hello World 利用 go/scanner 进行处理:

  1. 1:1     package "package" 
  2. 1:9     IDENT   "main" 
  3. 1:13    ;       "\n" 
  4. 3:1     import  "import" 
  5. 3:8     (       "" 
  6. 4:2     STRING  "\"fmt\"" 
  7. 4:7     ;       "\n" 
  8. 5:1     )       "" 
  9. 5:2     ;       "\n" 
  10. 7:1     func    "func" 
  11. 7:6     IDENT   "main" 
  12. 7:10    (       "" 
  13. 7:11    )       "" 
  14. 7:13    {       "" 
  15. 8:2     IDENT   "fmt" 
  16. 8:5     .       "" 
  17. 8:6     IDENT   "Println" 
  18. 8:13    (       "" 
  19. 8:14    STRING  "\"Hello World.\"" 
  20. 8:28    )       "" 
  21. 8:29    ;       "\n" 
  22. 9:1     }       "" 
  23. 9:2     ;       "\n" 

在经过扫描器的扫描后,可以看到输出了一大堆的 Token。如果没有前置知识的情况下,第一眼可能会非常懵逼。在此可以初步了解一下 Go 所主要包含的标识符和基本类型,如下:

  1. // Special tokens 
  2. ILLEGAL Token = iota 
  3. EOF 
  4. COMMENT 
  5.  
  6. // Identifiers and basic type literals 
  7. // (these tokens stand for classes of literals) 
  8. IDENT  // main 
  9. INT    // 12345 
  10. FLOAT  // 123.45 
  11. IMAG   // 123.45i 
  12. CHAR   // 'a' 
  13. STRING // "abc" 
  14. literal_end 

再根据所输出的 Token 稍加思考,做对比,就可得知其仅是单纯的利用扫描器翻译和输出。而实质上在识别记号时,扫描器也会完成其他工作,例如把标识符放到符号表,将数字、字符串常量存放到文字表等。

词法分析产生的记号一般可以分为如下几类:

语法分析/语义分析

语法分析器

语法分析器(Grammar Parser)将对扫描器所产生的记号进行语法分析,从而产生语法树(Syntax Tree),也称抽象语法树(Abstract Syntax Tree,AST)。

常见的分析方式是自顶向下或者自底向上,以及采取上下文无关语法(Context-free Grammer)作为分析手段。这块可参考一些计算机理论的资料,涉及的比较广。

但语法分析仅完成了对表达式的语法层面的分析,但并不清楚这个语句是否真正有意义,还需要一步语义分析。

语义分析器

语义分析器(Semantic Analyzer)将会对对语法分析器所生成的语法树上的表达式标识具体的类型。主要分为两类:

在经过语义分析阶段后,整个语法树的表达式都会被标识上类型,如果有些类型需要进行隐式转换,语义分析程序将会在语法书中插入相应的转换点,成为有更具体含义的语义。

实战演练

语法分析器生成的语法树,本质上就是以表达式(Expression)为节点的树。在 Go 语言中可通过 go/token、go/parser、go/ast 等相关方法生成语法树,代码如下:

  1. func main() { 
  2.  src := []byte("package main\n\nimport (\n\t\"fmt\"\n)\n\nfunc main() {\n\tfmt.Println(\"Hello World.\")\n}"
  3.  fset := token.NewFileSet() // positions are relative to fset 
  4.  f, err := parser.ParseFile(fset, "", src, 0) 
  5.  if err != nil { 
  6.   panic(err) 
  7.  } 
  8.  
  9.  ast.Print(fset, f) 

其经过语法分析器(自顶下向)分析后会所输出的结果如下:

  1. 0  *ast.File { 
  2.  1  .  Package: 1:1 
  3.  2  .  Name: *ast.Ident { 
  4.  3  .  .  NamePos: 1:9 
  5.  4  .  .  Name"main" 
  6.  5  .  } 
  7.  6  .  Decls: []ast.Decl (len = 2) { 
  8.  7  .  .  0: *ast.GenDecl { 
  9.  8  .  .  .  TokPos: 3:1 
  10.  9  .  .  .  Tok: import 
  11. 10  .  .  .  Lparen: 3:8 
  12. 11  .  .  .  Specs: []ast.Spec (len = 1) { 
  13. 12  .  .  .  .  0: *ast.ImportSpec { 
  14. 13  .  .  .  .  .  Path: *ast.BasicLit { 
  15. 14  .  .  .  .  .  .  ValuePos: 4:2 
  16. 15  .  .  .  .  .  .  Kind: STRING 
  17. 16  .  .  .  .  .  .  Value: "\"fmt\"" 
  18. 17  .  .  .  .  .  } 
  19. 18  .  .  .  .  .  EndPos: - 
  20. 19  .  .  .  .  } 
  21. 20  .  .  .  } 
  22. 21  .  .  .  Rparen: 5:1 
  23. 22  .  .  } 
  24. 23  .  .  ... 
  25. 71  .  } 
  26. 72  .  Scope: *ast.Scope { 
  27. 73  .  .  Objects: map[string]*ast.Object (len = 1) { 
  28. 74  .  .  .  "main": *(obj @ 27) 
  29. 75  .  .  } 
  30. 76  .  } 
  31. 77  .  Imports: []*ast.ImportSpec (len = 1) { 
  32. 78  .  .  0: *(obj @ 12) 
  33. 79  .  } 
  34. 80  .  Unresolved: []*ast.Ident (len = 1) { 
  35. 81  .  .  0: *(obj @ 46) 
  36. 82  .  } 
  37. 83  } 

可视化后的语法树如下:

 

 


可视化后的语法树

 

 

在上文中,主要涉及语法分析和语义分析部分,其归属于编译器前端,最终结果是得到了语法树,也就是常说是抽象语法树(AST)。

有兴趣可以亲自试试 yuroyoro/goast-viewer,会对语法树的理解更加的清晰。

中间语言生成

现代的编译器有这多个层次的优化,通常源代码级别会有一个优化过程。例如单纯的 1+2 的表达式就可以被优化。

而在 Go 语言中,中间语言则会涉及静态单赋值(Static Single Assignment,SSA)的特性。例如有一个很简单的 SayHelloWorld 方法,如下:

  1. package helloworld 
  2.  
  3. func SayHelloWorld(a intint { 
  4.     c := a + 2 
  5.     return c 

想看到源代码到中间语言,再到 SSA 的话,可通过 GOSSAFUNC 编译源代码并查看:

  1. $ GOSSAFUNC=SayHelloWorld go build helloworld.go 
  2. # command-line-arguments 
  3. dumped SSA to ./ssa.html 

打开 ssa.html,可看到这个文件源代码所对应的语法树,好几个版本的中间代码以及最终所生成的 SSA。

 

 


SSA

 

 

从左往右依次为:Sources(源代码)、AST(抽象语法树),其次最右边第一栏起就是第一轮中间语言(代码),后面还有十几轮。

目标代码生成与优化

在中间语言生成完毕后,还不能直接使用。因为机器真正能执行的是机器码。这时候就到了编译器后端的工作了。

从阶段上来讲,在源代码级优化器产生中间代码时,则标志着接下来的过程都属于编译器后端。

编译器后端主要包括如下两类,作用如下::

在 Go 语言中,以上行为包含在前面所提到的十几轮 SSA 优化降级中,有兴趣可自行研究 SSA,最后在 genssa 中可看见最终的中间代码:

 

 


最终降级完成的 SSA

 

 

此时的代码已经降级的与最终的汇编代码比较接近,但还没经过正式的转换。

汇编

完成程序编译后,第三步将是 ”汇编“,汇编器会将汇编代码转变成机器可执行的指令,每一个汇编语句几乎都对应着一条机器指令。基本逻辑就是根据汇编指令和机器指令的对照表一一翻译。

在 Go 语言中,genssa 所生成的目标代码已经完成了优化降级,接下来会调用 src/cmd/internal/obj 包中的汇编器将 SSA 中间代码生成为机器码。

我们可通过 go tool compile -S 的方式进行查看:

  1. $ go tool compile -S helloworld.go  
  2. "".SayHelloWorld STEXT nosplit size=15 args=0x10 locals=0x0 
  3.     0x0000 00000 (helloworld.go:3)  TEXT    "".SayHelloWorld(SB), NOSPLIT|ABIInternal, $0-16 
  4.     0x0000 00000 (helloworld.go:3)  FUNCDATA    $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 
  5.     0x0000 00000 (helloworld.go:3)  FUNCDATA    $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 
  6.     0x0000 00000 (helloworld.go:4)  MOVQ    "".a+8(SP), AX 
  7.     0x0005 00005 (helloworld.go:4)  ADDQ    $2, AX 
  8.     0x0009 00009 (helloworld.go:5)  MOVQ    AX, "".~r1+16(SP) 
  9.     0x000e 00014 (helloworld.go:5)  RET 
  10.     0x0000 48 8b 44 24 08 48 83 c0 02 48 89 44 24 10 c3     H.D$.H...H.D$.. 
  11. go.cuinfo.packagename. SDWARFINFO dupok size=0 
  12.     0x0000 68 65 6c 6c 6f 77 6f 72 6c 64                    helloworld 
  13. gclocals·33cdeccccebe80329f1fdbee7f5874cb SRODATA dupok size=8 
  14.     0x0000 01 00 00 00 00 00 00 00                          ........ 

至此就完成了一个高级语言再到计算机所能理解的机器码转换的完整流程了。

总结

在本文中,我们基本了解了一个应用程序是怎么从源代码编译到最终的机器码,其中每一步展开都是一块非常大的计算机基础知识。若有读者对其感兴趣,可根据文中的实操步骤进行深入的剖析和了解。

在下一篇文章中,将会进一步针对最后的一个步骤链接来进行分析和了解其最后一公里。

来源:脑子进煎鱼了内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯