iOS编译器LLVM + Clang架构分析

西门桃桃 2022-02-09 PM 1666℃ 0条

LLVM简介:

LLVM的项目是一个模块化和可重复使用的编译器和工具技术的集合。Clang 是 LLVM 的子项目,是 C,C++ 和 Objective-C 编译器。

iOS 开发中 Objective-C 是 Clang / LLVM 来编译的(Swift 是 Swift / LLVM)。

传统编译器分三个阶段:

前端(Frontend)-- 优化器(Optimizer)-- 后端(Backend)

1.png

前端(Frontend)负责解析源代码,检查语法错误,并将其翻译为抽象的语法树(Abstract Syntax Tree).也就是词法分析,语法分析,语义分析和生成中间代码

优化器(Optimizer)负责进行各种转换尝试改进代码的运行时间.release 包比 debug 包体积小运行快,其中的一个原因就是优化器起作用。比如重复计算的消除

后端(Backend)用来生成实际的机器码

这种设计有很多优点,但实际这一结构却从来没有被完美实现过。GCC 做的比较好,实现了很多前端和后端,支持了很多语言。但是有个缺陷,那就是他们是一个完整的可执行文件,没有把前端和后端分的太开,所以GCC 为了支持一门新的语言,或者支持一种新的平台,就变得比较困难。

LLVM 就解决了上面的问题,因为它被设计为一组库,而不是一个编译器,如下图

2.png

从上图中我们发现LLVM与GCC在三段式架构上并没有本质区别,也是分为前端、优化器和后端。但是,其设计最重要的方面是不同的前端、后端使用同一的中间代码 LLVM Intermediate Representation (LLVM IR) . 也就是图中的 LLVM Optimizer

`这种设计的好处就是,如果需要支持一种新的编程语言,那么只需要实现一个新的前端。
如果需要支持一种新的硬件设备,那么只需要实现一个新的后端。优化阶段是一个通用的阶段,它针对的是同一的LLVM IR,不论是支持新的编程语言,还是支持新的硬件设备,都不需要对优化阶段做修改。`

Clang 就是基于LLVM架构的C/C++/Objective-C编译器前端,如下图

LLVM现在被用作实现各种静态和运行时编译语言的通用基础结构(GCC家族(C,C++,Objecttive-C),Java,.Net,Python,Ruby,D等)

3.PNG

Clang 主要处理一些和具体机器无关的针对语言的分析操作.编译器的优化器部分和后端部分是LLVM后端,也可以直接叫 LLVM(狭义的LLVM),广义上LLVM 就是整个LLVM架构。

Clang和GCC比较

1.编译速度快,Debug模式下,编译OC的速度是GCC的三倍

2.占用内存小,Clang的生成的AST语法树占用内存是GCC的五分之一

3.模块化设计,Clang基于库的模块化设计,易于IDE集成和重用

4.诊断性可读性强,在编译过程中,Clang创建并保留了大量的详细元素,有利于调试

5.设计清晰简单,容易理解,易于扩展增强

4.png

看上面的图,左边是编程语言,最终是机器码,在我们Xcode 中编写的代码首先会经过 clang 这个编译器前端,他会生成中间代码(IR),这个中间代码又经过一系列的优化,这些优化就是 Pass,如果咱要编写中间代码优化代码的话,那就是编写Pass,最后就是生成机器码.
通过图也可以看出,Pass 是 LLVM 系统转化和优化的工作的一个节点,每个节点做一些工作,这些工作加起来就构成了 LLVM 整个系统的优化和转化。

在编译一个源文件时,编译器的处理过程分为几个阶段。
用命令行查看一下OC源文件的编译过程

$ clang -ccc-print-phases main.m
0: input, "main.m", objective-c
1: preprocessor, {0}, objective-c-cpp-output
2: compiler, {1}, ir
3: backend, {2}, assembler
4: assembler, {3}, object
5: linker, {4}, image
6: bind-arch, "x86_64", {5}, image

`一共是7个阶段,
第0个阶段找到源代码,读入文件。
第1个阶段 preprocessor,预处理器,就是把头导入,宏定义给展开,包括 #define、#include、 #import、 #indef、 #pragma。
第2阶段就是 compiler,编译器编译成 ir 中间代码。
第3阶段就是交给后端,来生成汇编代码(assembler)。
第4阶段是将汇编代码转换为目标对象文件
第5阶段是链接器,将多个目标对象文件合并为一个可执行文件 (或者一个动态库) 。
最后一阶段 生成可执行文件 :Mach-O`

Demo介绍这6个阶段

新建一个project,选择最简单的Command Line Tool

代码如下

#include <stdio.h>

#define kPeer 3

int main(int argc, const char * argv[]) {
    int a = 1;
    int b = 2;
    int c = a + b + kPeer;
    printf("%d",c);
    return 0;
}

1.查看预处理preprocess结果

Clang -E main.m
.....省略很多
extern int __vsnprintf_chk (char * restrict, size_t, int, size_t,
       const char * restrict, va_list);
# 412 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/usr/include/stdio.h" 2 3 4
# 10 "main.m" 2



int main(int argc, const char * argv[]) {
    int a = 1;
    int b = 2;
    int c = a + b + 3;
    printf("%d",c);
    return 0;
}

可以看到,这里的宏定义进行了替换,而且把include的资源给导进来了。

2.编译器前端 Clang的作用
前端(Frontend)负责解析源代码,检查语法错误,并将其翻译为抽象的语法树(Abstract Syntax Tree).也就是词法分析,语法分析,语义分析和生成中间代码
词法分析(输出Token流)

clang -fmodules -E -Xclang -dump-tokens main.m
int main(int argc, const char * argv[]) {
 int a = 1;
 int b = 2;
 int c = a + b + kPeer;
 printf("%' Loc=<main.m:9:1>
int 'int' [StartOfLine] Loc=<main.m:13:1>
identifier 'main' [LeadingSpace] Loc=<main.m:13:5>
l_paren '(' Loc=<main.m:13:9>
int 'int' Loc=<main.m:13:10>
identifier 'argc' [LeadingSpace] Loc=<main.m:13:14>
comma ',' Loc=<main.m:13:18>
const 'const' [LeadingSpace] Loc=<main.m:13:20>
char 'char' [LeadingSpace] Loc=<main.m:13:26>
star '*' [LeadingSpace] Loc=<main.m:13:31>
identifier 'argv' [LeadingSpace] Loc=<main.m:13:33>
l_square '[' Loc=<main.m:13:37>
r_square ']' Loc=<main.m:13:38>
r_paren ')' Loc=<main.m:13:39>
......

语法分析(生成语法树AST Abstract Syntax Tree)

clang -fmodules -fsyntax-only -Xclang -ast dump main.m
|-ImportDecl 0x7fc3e68673c0 <main.m:9:1> col:1 implicit Darwin.C.stdio
|-FunctionDecl 0x7fc3e6926e78 <line:13:1, line:19:1> line:13:5 main 'int (int, const char **)'
| |-ParmVarDecl 0x7fc3e6867410 <col:10, col:14> col:14 argc 'int'
| |-ParmVarDecl 0x7fc3e6867520 <col:20, col:38> col:33 argv 'const char **':'const char **'
| `-CompoundStmt 0x7fc3e6927878 <col:41, line:19:1>
| |-DeclStmt 0x7fc3e6927048 <line:14:5, col:14>
| | `-VarDecl 0x7fc3e6926fc8 <col:5, col:13> col:9 used a 'int' cinit
| | `-IntegerLiteral 0x7fc3e6927028 <col:13> 'int' 1
| |-DeclStmt 0x7fc3e69270f8 <line:15:5, col:14>
| | `-VarDecl 0x7fc3e6927078 <col:5, col:13> col:9 used b 'int' cinit
| | `-IntegerLiteral 0x7fc3e69270d8 <col:13> 'int' 2
| |-DeclStmt 0x7fc3e6927688 <line:16:5, col:26>
| | `-VarDecl 0x7fc3e6927128 <col:5, line:11:15> line:16:9 used c 'int' cinit
| | `-BinaryOperator 0x7fc3e6927280 <col:13, line:11:15> 'int' '+'
| | |-BinaryOperator 0x7fc3e6927238 <line:16:13, col:17> 'int' '+'
| | | |-ImplicitCastExpr 0x7fc3e6927208 <col:13> 'int' <LValueToRValue>
| | | | `-DeclRefExpr 0x7fc3e6927188 <col:13> 'int' lvalue Var 0x7fc3e6926fc8 'a' 'int'
| | | `-ImplicitCastExpr 0x7fc3e6927220 <col:17> 'int' <LValueToRValue>
| | | `-DeclRefExpr 0x7fc3e69271c8 <col:17> 'int' lvalue Var 0x7fc3e6927078 'b' 'int'
| | `-IntegerLiteral 0x7fc3e6927260 <line:11:15> 'int' 3
| |-CallExpr 0x7fc3e69277c0 <line:17:5, col:18> 'int'
| | |-ImplicitCastExpr 0x7fc3e69277a8 <col:5> 'int (*)(const char *, ...)' <FunctionToPointerDecay>
| | | `-DeclRefExpr 0x7fc3e69276a0 <col:5> 'int (const char *, ...)' Function 0x7fc3e69272b0 'printf' 'int (const char *, ...)'
| | |-ImplicitCastExpr 0x7fc3e6927810 <col:12> 'const char *' <BitCast>
| | | `-ImplicitCastExpr 0x7fc3e69277f8 <col:12> 'char *' <ArrayToPointerDecay>
| | | `-StringLiteral 0x7fc3e6927708 <col:12> 'char [3]' lvalue "%d"
| | `-ImplicitCastExpr 0x7fc3e6927828 <col:17> 'int' <LValueToRValue>
| | `-DeclRefExpr 0x7fc3e6927738 <col:17> 'int' lvalue Var 0x7fc3e6927128 'c' 'int'
| `-ReturnStmt 0x7fc3e6927860 <line:18:5, col:12>
| `-IntegerLiteral 0x7fc3e6927840 <col:12> 'int' 0
|-FunctionDecl 0x7fc3e6927a40 <line:23:1, line:25:1> line:23:6 test 'void (int, int)'
| |-ParmVarDecl 0x7fc3e69278c8 <col:11, col:15> col:15 used a 'int'
| |-ParmVarDecl 0x7fc3e6927940 <col:18, col:22> col:22 used b 'int'
| `-CompoundStmt 0x7fc3e6927c88 <col:24, line:25:1>
| `-DeclStmt 0x7fc3e6927c70 <line:24:5, col:24>
| `-VarDecl 0x7fc3e6927b20 <col:5, col:21> col:9 c 'int' cinit
| `-BinaryOperator 0x7fc3e6927c48 <col:13, col:21> 'int' '+'
| |-BinaryOperator 0x7fc3e6927c00 <col:13, col:17> 'int' '*'
| | |-ImplicitCastExpr 0x7fc3e6927bd0 <col:13> 'int' <LValueToRValue>
| | | `-DeclRefExpr 0x7fc3e6927b80 <col:13> 'int' lvalue ParmVar 0x7fc3e69278c8 'a' 'int'
| | `-ImplicitCastExpr 0x7fc3e6927be8 <col:17> 'int' <LValueToRValue>
| | `-DeclRefExpr 0x7fc3e6927ba8 <col:17> 'int' lvalue ParmVar 0x7fc3e6927940 'b' 'int'
| `-IntegerLiteral 0x7fc3e6927c28 <col:21> 'int' 100
`-<undeserialized declarations>

我们编写了一个函数

void test(int a, int b){
 int c = a * b + 100;
}

5.png

对应的语法树模型如下图

6.png

语意分析(生成语法树AST的基础上进行语意检查)之后就会生成中间代码IR

3.中间代码IR
LLVM IR有三种表现形式,类似于水有三种形态,气态,液态和固态

1.文本text型

便于阅读的文本,类似于汇编语言,.ll文件

clang -S -emit-llvm main.m
; Function Attrs: noinline nounwind optnone ssp uwtable 
define void @test(i32, i32) #2 { ; 有个全局函数@test (a,b)
 %3 = alloca i32, align 4 ; 局部变量 c 
%4 = alloca i32, align 4 ; 局部变量 d
 %5 = alloca i32, align 4 ; 局部变量 e
 store i32 %0, i32* %3, align 4 ; %0 赋值给%3 c = a
 store i32 %1, i32* %4, align 4 ; %1 赋值给%4 d = b
 %6 = load i32, i32* %3, align 4 ; 读取%3,赋值给%6 就是函数参数a
 %7 = load i32, i32* %4, align 4 ; 读取%4,赋值给%7 就是函数参数b
 %8 = mul nsw i32 %6, %7 ; a * b
 %9 = add nsw i32 %8, 100 ; a * b + 100
 store i32 %9, i32* %5, align 4 ; 参数 %9 赋值给 %5 e ===> 就是转换前函数写的int c变量
 ret void
}
上面就是test函数对应的中间代码

IR基本语法

注释以;开头

全局表示以@开头,局部变量以%开头

alloca在函数栈帧中分配内存

i32 32位 4个字节的意思

align 字节对齐

store写入

load读取

2.Memory

3.bitcode二进制格式

拓展名.dc

clang -c -emit-llvm main.m

参考:iOS编译器LLVM + Clang架构分析以及三种混淆方案实践分析

标签: LLVM

非特殊说明,本博所有文章均为博主原创。

评论啦~