01.LLVM

一、 编程语言类型

1.1 解释型语言

pythonJS 就属于常见的 解释型语言 。他们无需生成 可执行文件 就能够执行。

他们的执行过程如下:

1.2 编译型语言

我们常用的 C\C++Objective-CSwift 则属于 编译型语言 需要经过下面的过程生成 可执行文件 后才能执行。

二、 LLVM 概述

LLVM构架编译器(compiler)框架系统 ,以 C++ 编写而成,用于优化以任意程序语言编写的程序的 编译时间(compile-time)链接时间(link-time)运行时间(run-time) 以及 空闲时间(idle-time) ,对开发者保持开放,并兼容已有脚本。

LLVM 计划启动于2000年,最初由美国UIUC大学的 ChrisLattner 博士主持开展。2006年 ChrisLattner 加盟 AppleInc 并致力于 LLVMApple 开发体系中的应用。 Apple 也是 LLVM 计划的主要资助者。

目前 LLVM 已经被 苹果IOS开发工具Xilinx VivadoFacebookGoogle 等各大公司采用。

ChrisLattner 也是 Swift 之父。给大佬🧎‍♂️了

2.1 传统编译器设计

编译器前端

编译器前端的任务是 解析源代码 。它会进行: 词法分析语法分析语义分析检查源代码是否存在错误 ,然后构建 抽象语法树(Abstract Syntax Tree, AST)LLVM 的前端还会生成 中间代码(intermediate representation,IR)

优化器

优化器负责进行 各种优化改善代码的运行时间 ,例如消除冗余计算等。

后端 / 代码生成器

将代码映射到 目标指令集生成机器语言 , 并且 进行机器相关的代码优化

2.2 iOS的编译器架构

ObjectiveC / C / C++ 使用的编译器前端是 ClangSwiftSwift后端 都是 LLVM

2.3 LLVM的设计

当编译器决定支持多种源语言或多种硬件架构时, LLVM 最重要的地方就来了。

其他的编译器如 GCC 是非常成功的一款编译器,但由于它是作为整体应用程序设计的,因此它的用途受到了很大的限制。

LLVM 设计的最重要方面是,使用 通用的代码表示形式(IR) ,它是用来在编译器中表示代码的形式。所以 LLVM 可以为任何编程语言 独立编写前端 ,并且可以为任意硬件架构 独立编写后端

三、 Clang

对于我们的开发人员来说,接触最多的就是我们的 Clang

ClangLLVM 项目中的一个子项目。它是基于 LLVM架构轻量级编译器 ,诞生之初是为了替代 GCC ,提供更快的编译速度。它是负责编译 C、C++、Objecte-C 的编译器,它属于整个 LLVM 架构中的,编译器前端。对于开发者来说,研究 Clang 可以给我们带来很多好处。

四、 Objective-C 的编译流程探索

我们以下面的代码为例进行探索

#import <Foundation/Foundation.h>

int addResult(int a, int b) {
    return a + b;
}

#define ADD_FUNCTION(x, y) addResult(x, y)

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int numA = 1;
        int b1 = 1;
        int b2 = 2;
        int numB = b1 + b2;
        int result = ADD_FUNCTION(numA, numB);
        NSLog(@"结果是:%d", result);
    }
    return 0;
}

4.1 编译阶段概览

通过下面的命令我们可以列出完整的编译阶段:

clang -ccc-print-phases main.m
  • 0:输入文件

    • 找到源文件

  • 1:预处理

    • 这个过程处理包括宏的替换,头文件的导入

  • 2:编译

    • 进行词法分析、语法分析、检测语法是否正确,最终生成IR

  • 3:后端

    • 这里LLVM会通过一个一个的Pass(可以理解为一个节点)去优化,每个Pass做一些事情,最终生成汇编代码

  • 4:汇编

    • 生成目标文件

  • 5:链接

    • 链接需要的动态库和静态库,生成相应的镜像可执行文件

  • 6:绑定结构

    • 根据不同的系统架构,生成对应的可执行文件

4.2 预处理

clang -E main.m >> 预处理.m

通过对比,很明显的发现这里的宏定义被替换掉了。

4.3 编译阶段

词法分析

这里会把代码切成一个个 Token ,比如 大小括号等于号 还有 字符串 等。

clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m

语法分析

语法分析的任务是 验证语法是否正确 。在词法分析的基础上将单词序列组合成各类语法短语,如 “程序”“语句”“表达式” 等,然后将所有节点组成 抽象语法树(AbstractSyntaxTree,AST) 。语法分析其目的就是 对源程序进行分析判断,在结构上是否正确

clang -fmodules -fsyntax-only -Xclang -ast-dump main.m

生产中间代码IR

clang -S -fobjc-arc -emit-llvm main.m

这里会生成 .ll 文件,抽取一段 addResult 相关代码简单进行一下分析:

; Function Attrs: noinline nounwind optnone ssp uwtable
define i32 @addResult(i32 %0, i32 %1) #1 {
  %3 = alloca i32, align 4 // 开辟 32 bit 空间, 4个字节, 为 %3
  %4 = alloca i32, align 4
  store i32 %0, i32* %3, align 4 // 将 %0 参数存到 %3
  store i32 %1, i32* %4, align 4
  %5 = load i32, i32* %3, align 4 // 从 %3 中读取到 %5
  %6 = load i32, i32* %4, align 4
  %7 = add nsw i32 %5, %6 // %5 + %6 的值存到 %7
  ret i32 %7 // 返回 %7 32bit
}

main 函数:

; Function Attrs: noinline optnone ssp uwtable
define i32 @main(i32 %0, i8** %1) #2 {
  %3 = alloca i32, align 4
  %4 = alloca i32, align 4
  %5 = alloca i8**, align 8
  %6 = alloca i32, align 4
  %7 = alloca i32, align 4
  %8 = alloca i32, align 4
  %9 = alloca i32, align 4
  %10 = alloca i32, align 4
  store i32 0, i32* %3, align 4
  store i32 %0, i32* %4, align 4
  store i8** %1, i8*** %5, align 8
  %11 = call i8* @llvm.objc.autoreleasePoolPush() #3
  store i32 1, i32* %6, align 4
  store i32 1, i32* %7, align 4
  store i32 2, i32* %8, align 4
  %12 = load i32, i32* %7, align 4
  %13 = load i32, i32* %8, align 4
  %14 = add nsw i32 %12, %13
  store i32 %14, i32* %9, align 4
  %15 = load i32, i32* %6, align 4
  %16 = load i32, i32* %9, align 4
  %17 = call i32 @addResult(i32 %15, i32 %16)
  store i32 %17, i32* %10, align 4
  %18 = load i32, i32* %10, align 4
  notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to i8*), i32 %18)
  call void @llvm.objc.autoreleasePoolPop(i8* %11)
  ret i32 0
}

编译优化

这里 debug 模式下默认没有进行优化。

我们传入对应的优化等级,再生成一次 .ll 看看(这里用了和 release 一样的 Os ):

clang -Os -S -fobjc-arc -emit-llvm main.m -o main优化过.ll

addResult

; Function Attrs: norecurse nounwind optsize readnone ssp uwtable willreturn
define i32 @addResult(i32 %0, i32 %1) local_unnamed_addr #1 {
  %3 = add nsw i32 %1, %0
  ret i32 %3
}

main

; Function Attrs: optsize ssp uwtable
define i32 @main(i32 %0, i8** nocapture readnone %1) local_unnamed_addr #2 {
  %3 = tail call i8* @llvm.objc.autoreleasePoolPush() #3
  notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to i8*), i32 4) #5, !clang.arc.no_objc_arc_exceptions !9
  tail call void @llvm.objc.autoreleasePoolPop(i8* %3) #3
  ret i32 0
}

这里发现优化后的中间代码简介了不少,并且代码中冗余的计算逻辑直接生成了结果

bitCode

这是 xcode7 以后开启 bitcode 苹果会做进一步的优化,生成 bc 的中间代码。我们可以试试用优化后的 IR 代码生成 bc 代码。

clang -emit-llvm -c main优化过.ll -o main优化过.bc

这里的 bc 文件无法使用文本方式进行查看

生成汇编代码

可以通过的 .bc 或者 .ll 代码生成汇编代码

clang -S -fobjc-arc main优化过.bc -o main优化过.s

clang -S -fobjc-arc main未优化.ll -o main未优化.s

对比一下优化前后的汇编代码,发现优化后的指令少了很多:

生成目标文件

clang -fmodules -c main未优化.s -o main未优化.o

目标文件的生成,是 汇编器 以汇编代码作为 输入 ,将 汇编代码 转换为 机器代码 ,最后输出 目标文件(object-file) ,这个阶段就是属于 编译器后端 的工作了。

查看符号

xcrun nm -nm main未优化.o

输出:

                 (undefined) external _NSLog
                 (undefined) external ___CFConstantStringClassReference
                 (undefined) external _objc_autoreleasePoolPop
                 (undefined) external _objc_autoreleasePoolPush
0000000000000000 (__TEXT,__text) external _addResult
0000000000000020 (__TEXT,__text) external _main
000000000000008e (__TEXT,__ustring) non-external l_.str
  • _NSLog 是一个是 undefined external

    • undefined

      • 表示在当前文件暂时找不到符号 _NSLog

    • external

      • 表示这个符号是外部可以访问的

有兴趣可以移步 通过符号表找到符号 有介绍查找 _NSLog 的过程

生成可执行文件

连接器 把编译产生的 .o文件(dylib .a)文件 ,生成一个 mach-o文件(可执行文件)

clang main.o -o main

报错:

Undefined symbols for architecture x86_64:
  "_NSLog", referenced from:
      _main in main.o
  "___CFConstantStringClassReference", referenced from:
      CFString in main.o
  "_objc_autoreleasePoolPop", referenced from:
      _main in main.o
  "_objc_autoreleasePoolPush", referenced from:
      _main in main.o
ld: symbol(s) not found for architecture x86_64

因为这里用到了 Foundation 框架内的符号 NSLog 不能直接生成,我们加上参数

clang -framework Foundation main.o -o main

生成成功:

查看符号

xcrun nm -nm main

输出:

                 (undefined) external _NSLog (from Foundation)
                 (undefined) external ___CFConstantStringClassReference (from CoreFoundation)
                 (undefined) external _objc_autoreleasePoolPop (from libobjc)
                 (undefined) external _objc_autoreleasePoolPush (from libobjc)
                 (undefined) external dyld_stub_binder (from libSystem)
0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
0000000100003ed0 (__TEXT,__text) external _addResult
0000000100003ef0 (__TEXT,__text) external _main
0000000100008018 (__DATA,__data) non-external __dyld_private

这里的符号就已经和对应的框架对应起来了,如: _NSLog (from Foundation) 。而且 mach-o 文件中的偏移地址也有了。

五、 Swift 编译流程

  • 分析输出AST

    • swiftc main.swift -dump-parse

  • 分析并且检查类型输出AST

    • swiftc main.swift -dump-ast

  • 生成中间体语言(SIL),未优化

    • swiftc main.swift -emit-silgen

  • 生成中间体语言(SIL),优化后的

    • swiftc main.swift -emit-sil

  • 生成LLVM中间体语言 (.ll文件)

    • swiftc main.swift -emit-ir

  • 生成LLVM中间体语言 (.bc文件)

    • swiftc main.swift -emit-bc

  • 生成汇编

    • swiftc main.swift -emit-assembly

  • 编译生成可执行.out文件

    • swiftc -o main.o main.swift

参考

LLVM

Clang 1 Which OSX library to link against (command line) to use NSLog?

Last updated