编译器将源代码转换为计算机可以理解的可执行机器代码,或将源代码转换为另一种编程语言。本文介绍编译器工具,从LLVM开始。编译器只不过是一个翻译其他程序的程序。传统的编译器将源代码转换为计算机可以理解的可执行机器代码。(一些编译器将源代码转换成另一种编程语言,这些编译器称为源到源转换器或转译器)。LLVM是一个广泛使用的编译器项目,其中包含多个模块化编译器工具。传统的编译器设计包括三个部分:前端将源代码转换为中间表示(IR)。clang(http://clang.llvm.org/)是LLVM项目中类C语言的前端工具。优化器解析IR并将其转换为更有效的形式。opt是LLVM项目的优化器工具。后端通过将IR映射到目标硬件的指令集来生成机器码。llc是LLVM项目的后端工具。LLVMIR是一种类似汇编的低级语言。但是,它没有针对特定的硬件信息进行编程。Hello,Compiler下面是一个简单的C程序,它打印字符串“Hello,Compiler”。程序员虽然能看懂C语言的语法,但计算机看起来一头雾水。接下来我通过编译的三个阶段将下面的程序转换成机器可执行程序。//compile_me.c//Wavetothecompiler.Theworldcanwait.#includeintmain(){printf("Hello,Compiler!\n");return0;}前端如前所述,clang是类LLVMC的前端语言工具。Clang由C预处理器、词法分析器(lexer)、解析器、语义分析器和中间表示生成器组成。C预处理器在将源代码转换为IR之前修改源代码。预处理器将包含外部文件,例如上面的#include。它将#include行替换为C标准库文件stdio.h中的所有代码,其中包含printf函数的声明。通过执行以下命令观察预处理器的输出:clang-Ecompile_me.c-opreprocessed.i词法分析器(Lexer,也称为扫描器或分词器)将一串字符转换为一串单词。每个词或符号,根据其属性,被分配到相应的句法类别:标点符号、关键字、标识符、常量或注释。compile_me.c的词法分析:解析器判断词法分析器生成的词串是否包含源语言中的有效句子。在分析单词的语法后,解析器输出一个抽象语法树(AST)。ClangAST中的节点分别代表声明和类型。compile_me.c的AST:语义分析器遍历AST,判断语句的意义是否有效。此阶段检查类型错误。如果compile_me.c中的主函数返回“零”而不是0,语义分析器将抛出错误,因为“零”不是int类型。IR生成器将AST转换为IR。在compile_me.c上运行clangfrontend生成LLVMIR:clang-S-emit-llvm-ollvm_ir.llcompile_me.cllvm_ir.llmainfunctionin:;llvm_ir.ll@.str=privateunnamed_addrconstant[18xi8]c"Hello,Compiler!\0A\00",align1definei32@main(){%1=allocai32,align4;<-memoryallocatedonthestackstorei320,i32*%1,align4%2=calli32(i8*,...)@printf(i8*getelementptrinbounds([18xi8],[18xi8]*@.str,i320,i320))reti320}declarei32@printf(i8*,...)优化器程序在运行时。优化器的输入是IR,输出是优化后的IR。LLVM的优化器工具opt将使用-O2(大写o,数字2)标志来优化处理器速度,并使用-Os(大写o,s)标志来优化构建对象的大小。看一下优化器优化前后的LLVMIR代码:opt-O2-Sllvm_ir.ll-ooptimized.lloptimized.ll的主要函数:;optimized.ll@str=privateunnamed_addrconstant[17xi8]c"Hello,Compiler!\00"definei32@main(){%puts=tailcalli32@puts(i8*getelementptrinbounds([17xi8],[17xi8]*@str,i640,i640))reti320}declarei32@puts(i8*nocapturereadonly)优化后,main函数不在堆栈上分配内存,因为它不使用任何内存。优化后的代码调用puts函数而不是printf函数,因为它不使用printf函数的任何格式化功能。当然,优化器不仅仅知道何时使用puts而不是printf。优化器还展开循环,内联简单计算的结果。考虑以下代码,它将两个数字相加并打印结果://add.c#includeintmain(){inta=5,b=10,c=a+b;printf("%i+%i=%i\n",a,b,c);}未优化的LLVMIR:@.str=privateunnamed_addrconstant[14xi8]c"%i+%i=%i\0A\00",align1definei32@main(){%1=allocai32,align4;<-allocastackspaceforvara%2=allocai32,align4;<-allocastackspaceforvarb%3=allocai32,align4;<-allocastackspaceforvarcstorei325,i32*%1,align4;<-store5atmemorylocation%1storei3210,i32*%2,align4;<-store10atmemorylocation%2%4=loadi32,i32*%1,align4;<-loadthevalueatmemoryaddress%1intoregister%4%5=loadi32,i32*%2,align4;<-loadthevalueatmemoryaddress%2intoregister%5%6=addnswi32%4,%5;<-addthevaluesinregisters%4and%5.puttheresultinregister%6storei32%6,i32*%3,align4;<-putthevalueofregister%6inmemoryaddress%3%7=loadi32,i32*%1,align4;<-loadthevalueatmemoryaddress%1intoregister%7的值%8=loadi32,i32*%2,align4;<-loadthevalueatmemoryaddress%2intoregister%8%9=loadi32,i32*%3,align4;<-loadthevalueatmemoryaddress%3intoregister%9%10=calli32(i8*,...)@printf(i8*getelementptrinbounds([14xi8],[14xi8]*@.str,i320,i320),i32%7,i32%8,i32%9)reti320}declarei32@printf(i8*,...)优化的LLVMIR:@.str=privateunnamed_addrconstant[14xi8]c"%i+%i=%i\0A\00",align1definei32@main(){%1=tailcalli32(i8*,...)@printf(i8*getelementptrinbounds([14xi8],[14xi8]*@.str,i640,i640),i325,i3210,i3215)reti320}declarei32@printf(i8*nocapturereadonly,...)优化后的main函数实际上是内联了未优化版本第17行和第18行的变量来进行加法运算,因为所有的变量都是常量。很酷吧?后端LLVM的后端工具是llc。它经历了三个阶段,最终将LLVMIR输入转换为机器码:指令选择是IR指令到目标机器指令集的映射。此步骤为虚拟寄存器使用专用命名空间。寄存器分配是从虚拟寄存器到目标架构的真实寄存器的映射。我的CPU是x86架构的,也就是说只能使用16个寄存器。但是,编译器使用尽可能少的寄存器。指令调度是重新安排操作以反映目标机器的性能限制。执行以下命令会生成一些机器码!llc-ocompiled-assembly.soptimized.ll_main:pushq%rbpmovq%rsp,%rbpleaqL_str(%rip),%rdicallq_putsxorl%eax,%eaxpopq%rbpretqL_str:.asciz"你好,编译器!“这是一个x86汇编语言程序,计算机和程序员的通用语言。这可能看起来晦涩难懂,但必须有人理解我。原文:https://nicoleorchard.com/blog/compilers【本文为《机器之心》专栏原文翻译,微信公众号“机器之心(id:almosthuman2014)”】点此查看阅读更多关于作者的好文