编译器简介

2020-06-20 17:26:48

编译器只是一个翻译其他程序的程序。传统编译器将源代码转换为您的计算机能够理解的可执行机器码。(一些编译器将源代码翻译成另一种编程语言。这些编译器称为源到源翻译器或转换程序。)。LLVM是一个广泛使用的编译器项目,由许多模块化的编译器工具组成。

前端将源代码转换为中间表示(IR)*。clang是LLVM用于C语言家族的前端。

优化器分析IR并将其转换为更有效的形式。OPT是LLVM优化器工具。

后端通过将IR映射到目标硬件指令集来生成机器码。LLC是LLVM后端工具。

*LLVM IR是一种类似于汇编的低级语言。但是,它抽象了特定于硬件的信息。

下面是一个简单的C程序,它打印“Hello,Compiler!”转到STDOUT。C语法是人类可读的,但是我的计算机不知道如何处理它。我将演练三个编译阶段,使该程序成为机器可执行的。

//Compile_me.c//向编译器波形。这个世界可以等待。#include<;stdio.h>;int main(){printf(";Hello,编译器!\n";);返回0;}。

正如我在上面提到的,clang是LLVM用于C语言家族的前端。Clang由C预处理器、词法分析器、解析器、语义分析器和IR生成器组成。

C预处理器在开始转换到IR之前修改源代码。预处理器处理包括外部文件,如上面的#include<;stdio.h>;。它将用stdio.h C标准库文件的全部内容替换该行,其中将包括printf函数的声明。

词法分析器(或扫描器或标记器)将字符串转换为单词字符串。每个单词或标记被分配给五个语法类别之一:标点符号、关键字、标识符、字面或注释。

解析器确定词流是否由源语言中的有效句子组成。在分析令牌流的语法之后,它输出抽象语法树(AST)。Clang AST中的节点表示声明、语句和类型。

语义分析器遍历AST,确定代码语句是否具有有效含义。此阶段检查类型错误。如果Compile_me.c中的main函数返回";ZERO";而不是0,语义分析器将抛出错误,因为";ZERO";不是INT类型。

[email protected]=私有UNNAME_ADDR常量[18 x i8]c";Hello,编译器!\0A\00";,Align 1定义I32@Main(){%1=alloca I32,Align 4;<;-堆栈存储上分配的内存I32 0,I32*%1,ALIGN 4%2=调用I32(i8*,.)@printf(i8*getelementptr inbound([18 x i8],[18 x i8]*@.str,I32 0,I32 0))ret I32 0}声明I32@printf(i8*,.)。

优化器的工作是基于对程序运行时行为的理解来提高代码效率。优化器将IR作为输入,并生成改进的IR作为输出。LLVM的优化器工具opt将使用标志-O2(大写o,2)优化处理器速度,并使用标志-Os(大写o,s)优化大小。

看看我们的前端在上面生成的LLVM IR代码与运行结果之间的差异:

;Optimized.ll@str=private unnamed_addr常量[17 x i8]c";Hello,编译器!\00";定义I32@main(){%put=尾部调用I32@put(i8*getelementptr inbound([17 x i8],[17 x i8]*@str,i64 0,i64 0))ret I32 0}声明I32@put(i8*nocatch只读)

在优化版本中,main不在堆栈上分配内存,因为它不使用任何内存。优化后的代码还调用put而不是printf,因为没有使用printf的任何格式化功能。

当然,优化器不仅仅知道何时使用put代替printf。优化器还会展开循环并内联简单计算的结果。考虑下面的程序,它将两个整数相加并打印结果。

//add.c#include<;stdio.h>;int main(){int a=5,b=10,c=a+b;printf(";%i+%i=%i\n";,a,b,c);}。

@.str=私有UNNAME_ADDR常量[14 x i8]c";%i+%i=%i\0A\00";,Align 1定义I32@main(){%1=alloca I32,Align 4;<;-为变量a分配堆栈空间%2=Alloca I32,Align 4;<;-为var b%3分配堆栈空间%3=alloca I32,Align 4;<;-为内存位置%1的变量c存储I32 5、I32*%1、Align 4;分配堆栈空间;-在内存位置%1的存储5存储I32 10、I32*%2、Align 4;<;-在内存位置%2的存储10%4=加载I32、I32*%1、Align 4;<;-将内存地址%1处的值加载到寄存器%4%5=加载I32、I32*%2、Align 4;<;-将内存地址%2处的值加载到寄存器%5%6=ADD NSW I32%4,%5;<;-将寄存器%4和%5中的值相加。将结果放入寄存器%6存储I32%6,I32*%3,ALIGN 4;<;-将寄存器%6的值加载到内存地址%3%7=加载I32,I32*%1,ALIGN 4;<;-将内存地址%1处的值加载到寄存器%7%8=加载I32,I32*%2,ALIGN 4;<;-将内存地址%2处的值加载到寄存器%8%9=加载I32,I32*%3,ALIGN 4;<;-将内存地址%3处的值加载到寄存器%9%10=调用I32(i8*,.)@printf(i8*getelementptr inbound([14 x i8],[14 x i8]*@.str,I32 0,I32 0),I32%7,I32%8,I32%9)ret I32 0}声明I32@printf(i8*,.)。

@.str=私有unname_addr常量[14 x i8]c";%i+%i=%i\0A\00";,Align 1定义I32@Main(){%1=Tail调用I32(i8*,.)@printf(i8*getelementptr inbound([14 x i8],[14 x i8]*@.str,i64 0,i64 0),I32 5,I32 10,I32 15)ret I32 0}声明I32@printf(i8*nocatch readonly,.)。

我们优化的主函数实质上是未优化版本的第16和17行,变量值是内联的。OPT计算加法是因为所有变量都是常量。很酷,是吧?

LLVM的后端工具是LLC。它分三个阶段从LLVM IR输入生成机器码:

指令选择是将IR指令映射到目标机器的指令集。此步骤使用虚拟寄存器的无限命名空间。

寄存器分配是将虚拟寄存器映射到目标体系结构上的实际寄存器。我的CPU采用x86架构,最多只能有16个寄存器。但是,编译器将使用尽可能少的寄存器。

指令调度是对操作的重新排序,以反映目标机器的性能约束。

_main:PUSQ%RBP movq%rsp,%RBP leaq L_str(%rip),%RDI callq_put xorl%eax,%eax popq%rbp retqL_str:.asciz";您好,编译器!";

这个程序是x86汇编语言,这是我的计算机所说语言的人类可读语法。终于有人理解我了,🙌