编译器简介

—— 在 Siri 出现以前如何与计算机对话

译者:penghuster
作者:Nicole Orchard
原文:An Intro to Compilers


简单来说,编译器是一个翻译其他程序的程序。传统的编译器把源码翻译为电脑可识别的机器码。(一些编译器也可将源码翻译为另外一种编程语言。这类编译器被称为源码到源码的翻译器或转译器。)LLVM 是一个使用非常广泛的编译器项目, 由许多模块化的编译工具组成。

传统的编译器设计包括以下三部分:

  • 前端(Frontend)翻译源码 为一种中间表示(IR)*。clang 是 LLVM 提供的 c 系列语言的前端工具。
  • 优化器(Optimizer)分析 IR,并翻译为一种更有效率的形式。opt 是 LLVM 提供的优化工具。
  • 后后端通过 IR 与目标硬件的指令集映射关系生成机器码。 llc 是 LLVM 提供的后端工具。
    *LLVM IR 是一种与汇编语言类似的低级语言。然而,它总是与特定的硬件相关。

你好,编译器👋

下面是一个简单的 c 程序,打印 “Hello, Compiler!”到标准输出。C 语法是容易阅读的,但是电脑却不知道如何处理它。我将通过编译的 3 个阶段,将该程序翻译为可执行的机器码。

// compile_me.c
// Wave to the compiler. The world can wait.

#include <stdio.h>

int main() {
  printf("Hello, Compiler!\n");
  return 0;
}

The Frontend

如上文所述,clang 是 LLVM 提供的 C 系列语言的前端工具。clang 由 c 前处理程序、词法分析程序、语法分析程序、语义分析程序和 IR 生成程序。

  • C 前处理程序 在开始翻译为 IR 之前修改源码。该前处理程序会处理包含的外部文件,如上述代码中的 #include <stdio.h>。前处理程序将该代码行替换为所指文件(stdio.h)的整个内容,该 C 标准库文件包含了 printf 的函数声明。
    查看前处理程序的输出,需执行如下命令:
    clang -E compile_me.c -o preprocessed.i
  • 词法分析程序也叫扫描程序或分词程序,它将一行字符转化为一行单词。每个单词或者记号将被归列于如下 5 种词法范畴:标点符号、关键字、标识符、字面量或注释。
    Tokenization of compile_me.c
  • 语法分析程序决定这一串单词流在源语言中是否能够组成有效的句子。然后通过分析语法标记流,输出一个抽象语法书(abstract syntax tree,AST)。在 clang 语法树中的节点代表声明、语句和类型。
    compile_me.c 的语法树:
  • 词法分析程序 借助语法树判断代码语句是否有有效的意义。该阶段检查类型错误。如果用 “zero” 代替 compile_me.c 文件中的 0,那么词法分析程序将由于 “zero” 不是 int 类型而抛出异常。
  • IR 生成程序 翻译语法树为 IR。
    运行 clang 前端工具利用 compile_me.c 文件生成 LLVM IR:
    clang -S -emit-llvm -o llvm_ir.ll compile_me.c
    在 llvm_ir.ll 中的主函数为:
; llvm_ir.ll

@.str = private unnamed_addr constant [18 x i8] c"Hello, Compiler!\0A\00", align 1

define i32 @main() {
  %1 = alloca i32, align 4 ; <- memory allocated on the stack
  store i32 0, i32* %1, align 4
  %2 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([18 x i8], [18 x i8]* @.str, i32 0, i32 0))
  ret i32 0
}

declare i32 @printf(i8*, ...)

优化器

优化器的工作是根据它对于程序运行时行为的理解,提高代码执行效率。优化器输入 IR 输出优化后的 IR。LLVM 的优化工具—opt—的选项 -O2 将优化处理器执行该程序速度,选项 -Os 将优化改程序的大小。

比较 LLVM 前端工具生成的 LLVM IR 与执行如下指令生成的结果:
opt -O2 -S llvm_ir.ll -o optimized.ll
optimized.ll 中的主函数:

; optimized.ll

@str = private unnamed_addr constant [17 x i8] c"Hello, Compiler!\00"

define i32 @main() {
  %puts = tail call i32 @puts(i8* getelementptr inbounds ([17 x i8], [17 x i8]* @str, i64 0, i64 0))
  ret i32 0
}

declare i32 @puts(i8* nocapture readonly) 

在这个优化版本中,main 函数没有在栈上分配内存空间,因此
main 函数不占用任何内存。由于 printf 中没有用到格式化字符串,故优化代码中调用 puts 代替 printf

当然,优化器不仅仅知道何时用 printf 替换 puts,也知道适时展开循环和内联化简单计算的结果。分析如下程序,该程序两个数相加并打印结果。

// add.c
#include <stdio.h>

int main() {
  int a = 5, b = 10, c = a + b;
  printf("%i + %i = %i\n", a, b, c);
}

这是未经优化的 LLVM IR:

@.str = private unnamed_addr constant [14 x i8] c"%i + %i = %i\0A\00", align 1

define i32 @main() {
  %1 = alloca i32, align 4 ; <- allocate stack space for var a
  %2 = alloca i32, align 4 ; <- allocate stack space for var b
  %3 = alloca i32, align 4 ; <- allocate stack space for var c
  store i32 5, i32* %1, align 4  ; <- store 5 at memory location %1
  store i32 10, i32* %2, align 4 ; <- store 10 at memory location %2
  %4 = load i32, i32* %1, align 4 ; <- load the value at memory address %1 into register %4
  %5 = load i32, i32* %2, align 4 ; <- load the value at memory address %2 into register %5
  %6 = add nsw i32 %4, %5 ; <- add the values in registers %4 and %5. put the result in register %6
  store i32 %6, i32* %3, align 4 ; <- put the value of register %6 into memory address %3
  %7 = load i32, i32* %1, align 4 ; <- load the value at memory address %1 into register %7
  %8 = load i32, i32* %2, align 4 ; <- load the value at memory address %2 into register %8
  %9 = load i32, i32* %3, align 4 ; <- load the value at memory address %3 into register %9
  %10 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str, i32 0, i32 0), i32 %7, i32 %8, i32 %9)
  ret i32 0
}

declare i32 @printf(i8*, ...)

这是优化后的 LLVM IR:

@.str = private unnamed_addr constant [14 x i8] c"%i + %i = %i\0A\00", align 1

define i32 @main() {
  %1 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str, i64 0, i64 0), i32 5, i32 10, i32 15)
  ret i32 0
}

declare i32 @printf(i8* nocapture readonly, ...)

随着变量值的内联化,优化后的 main 函数本质上减少了 17 到 18 行代码。由于变量为常量,opt直接计算了该和。 是否很酷?

后端

LLVM 的后端工具是 llc。它输入 LLVM IR 输出机器码的过程分为三个阶段:

  • 指令选择是映射 IR 指令到目标机器的指令集。该步骤使用虚拟寄存器的无限命名空间。
  • 寄存器分配是在目标机器架构上虚拟寄存器映射到真实寄存器的过程。我的 CPU 是 x86 架构,该架构只有 16 个寄存器。然而,编译器将会尽可能少的使用寄存器。
  • 指令安排阶段是根据目标计算机的性能限制重排指令操作顺序。

运行如下命令将产生机器码。
llc -o compiled-assembly.s optimized.ll

_main:
    pushq   %rbp
    movq    %rsp, %rbp
    leaq    L_str(%rip), %rdi
    callq   _puts
    xorl    %eax, %eax
    popq    %rbp
    retq
L_str:
    .asciz  "Hello, Compiler!"

该程序是 x86 汇编语言,它是计算机说出的人可阅读的语言。某些人最终也许理解了我。 🙌


版权声明:自由转载-非商用-非衍生-保持署名创意共享3.0许可证

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容

  • 前言 2000年,伊利诺伊大学厄巴纳-香槟分校(University of Illinois at Urbana-...
    星光社的戴铭阅读 15,854评论 8 180
  • 本文部分内容引用: 中文维基百科。 结构化编译器前端--clang介绍。 什么是clang编译器? clang是L...
    凌杰_owlman阅读 8,228评论 0 8
  • 编译器做些什么? 本文主要探讨一下编译器主要做些什么,以及如何有效的利用编译器。 简单的说,编译器有两个职责:把 ...
    评评分分阅读 1,113评论 1 5
  • 夜色渐深,城市各个地方开始发出或明亮或暗淡的光,看起来美轮美奂。像是星光落满了人间,一切都是幸福安宁的模样。 ...
    冬雨的陶阅读 199评论 0 3
  • 2017年7月12日,星期三,晴 今天初伏第一天,天气的酷热似乎更升级了。热的不想要做饭了,但生活一定要...
    菊时阅读 221评论 0 2