iOS-底层原理26-LLVM

《iOS底层原理文章汇总》

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

什么是编译器

.c文件和.py文件的编译差别

解释器语言:一边去读去解释一边让CPU去执行
编译器语言:让编译器先去编译生成一个可执行文件如MachO文件(a.out),这个文件是二进制组合文件010101,虽然helloDemo.py和helloDemo.c也是二进制数据010101的组合,但CPU无法直接执行,a.out的二进制数据010101组合,CPU能直接运行的指令。

python和c编译的区别@2x.png
a.out@2x.png
  • 二进制数据010101的组合分为两种,一种是数据,数据读了之后给不同的应用程序去解析,如helloDemo.py和helloDemo.c,一种是指令,cpu读了直接分析直接执行,指令和数据没有区分

  • 编译器的作用是将高级语言代码编译成CPU(硬件)能看的懂的010101组合,也叫机器语言,这些010101的组合就叫做指令

  • Xcode中Cmd+B在Product文件夹下面会生成XXX.app文件,显示包内容里面可以看到XXX的二进制可执行文件

  • 函数调用栈
    函数申请一段栈空间,往栈中开始压栈,压完数据后还要出栈,给栈平衡,函数调用完成后,告诉编译器要出栈,寻址跳转,开辟内存空间,给内存空间输入值,再执行相应代码,将栈平衡,再跳回去

test(){
   func1();
   func();
   func2();
}
func() -- 申请一段栈空间。 (16字节内存)
{
   int a = 10;//4字节
   int b = 10;//4字节
}
  • 高级语言的出现让程序更简洁,可读性更好,但是程序运行更复杂了

LLVM概述

编译器@2x.png
LLVM的设计@2x.png

Clang

Clang@2x.png

Clang是如何编译源代码的

clang -ccc-print-phases main.m

静态库会和MachO文件进行合并,动态库是独立的,执行的时候才链接绑定

Clang编译流程@2x.png
Clang编译七步@2x.png
  • 静态库和动态库的区别
    静态库会和MachO文件进行合并,动态库是独立的,执行的时候才链接绑定

预处理阶段

clang -E main.m 执行完毕后可以看到头文件的导入和宏的替换

main.png
宏定义.png

编译阶段

编译阶段.png
Token.png

语法分析

语法分析.png
抽象语法树.png
加法运算.png

语法错误的情况

语法错误.png
抽象语法书识别错误.png

生成中间代码

IR.png
main-test.png
main.ll.png
#import <stdio.h>
int test(int a,int b){
    return a + b + 3;
}
int main(int argc, const char * argv[]) {
    int a = test(1, 2);
    printf("%d",a);
    return 0;
}

IR代码

IR代码.png

查看生成的test代码里面有重复的地方,没有做任何优化的情况下,生成的IR代码有55行

; Function Attrs: noinline nounwind optnone ssp uwtable
define i32 @test(i32, i32) #0 { int a0 int a1
  %3 = alloca i32, align 4 int a3  
  %4 = alloca i32, align 4 int a4
  store i32 %0, i32* %3, align 4 a3 = a0
  store i32 %1, i32* %4, align 4 a4 = a1
  %5 = load i32, i32* %3, align 4 a5 = a3
  %6 = load i32, i32* %4, align 4 a6 = a4
  %7 = add nsw i32 %5, %6 a7 = a5 + a6
  %8 = add nsw i32 %7, 3  a8 = a7 + 3
  ret i32 %8
}

在Build Settings中Code generation中有优化器设置,Optimization Level里面有None,Fast,Faster,Fastest,Fastest Smallest,Fastest Aggressive Optimization,Smallest Aggressive Size Optimization,选择后再次进行编译的时候,能进行相应等级的优化

优化器等级设置.png

对main.m进行-Qs等级的优化,生成IR代码main2.ll如下,代码行数变为42行,test和main函数代码明显变少
clang -Os -S -fobjc-arc -emit-llvm main.m -o main2.ll

; Function Attrs: norecurse nounwind optsize readnone ssp uwtable
define i32 @test(i32, i32) local_unnamed_addr #0 {
  %3 = add i32 %0, 3
  %4 = add i32 %3, %1
  ret i32 %4
}
-Os和没做优化对比@2x.png

bitCode

bitCode@2x.png

IR源代码main.ll生成中间代码main.bc文件

clang -emit-llvm -c main.ll -o main.bc

main.m main.ll main.bc三种代码都能生成汇编代码,下面三种都是没有优化的,生成的汇编代码行数一致。在生成汇编代码的时候不会进行优化了,优化的结果是在生成IR中间代码的时候

clang -S -fobjc-arc main.bc -o main.s
clang -S -fobjc-arc main.ll -o main1.s
clang -S -fobjc-arc main.m -o main2.s
bc,ll,.m生成汇编.s文件@2x.png
没有优化生成汇编代码@2x.png

生成汇编代码.s的时候也可以增加优化参数clang -Os -S -fobjc-arc main.m -o main3.s,此时生成的汇编代码只有48行了

优化后的汇编代码@2x.png

前面将.m文件用优化器-Os生成得到main2.ll文件,main3.s是直接用main.m通过优化参数-Os等级优化后的汇编代码,现将main2.ll文件通过优化等级-Os生成汇编代码main4.s,clang -Os -S -fobjc-arc main2.ll -o main4.s查看两者的大小区别,发现一模一样,说明优化只有一次,无论前端给参数还是后端给参数,只是经过不断不断的PASS,一个又一个的llvm的PASS来进行优化的

直接优化和经过中间代码优化得到相同汇编代码@2x.png

生成目标文件

一个MachO文件是多个.o文件的集合,clang -fmodules -c main3.s -o main.o生成main.o文件,main3.s是汇编assembler source text文件,main.o是MachO文件,优化过后和优化之前的目标文件大小是不一样的

生成目标文件@2x.png
main.s和main.o文件格式@2x.png

函数中printf是外部符号,既然有外部符号,那就需要链接,链接后,运行时才能找到对应的动态库。静态库直接进行拷贝合并。静态、动态指的是 链接的过程。

【静态库】的特点如下:

1.静态库对函数库的链接是在编译期完成的。执行期间代码装载速度快。

2.使可执行文件变大,浪费空间和资源(占空间)。

3.对程序的更新、部署与发布不方便,需要全量更新。如果 某一个静态库更新了,所有使用它的应用程序都需要重新编译、发布给用户。


静态库.png

【动态库】在程序编译时并不会链接到目标代码中,而是在运行时才被载入。不同的应用程序如果调用相同的库,那么在内存中只需要有一份该共享库的实例,避免了空间浪费问题。同时也解决了静态库对程序的更新的依赖,用户只需更新动态库即可。

【动态库】在内存中只存在一份拷贝,如果某一进程需要用到动态库,只需在运行时动态载入即可。

【动态库】的特点:
1.动态库把对一些库函数的链接载入推迟到程序运行时期(占时间)。

2.可以实现进程之间的资源共享。(因此动态库也称为共享库)

3.将一些程序升级变得简单,不需要重新编译,属于增量更新。


动态库.png
printf@2x.png
链接.o文件和动态库进行绑定生成mach-o文件@2x.png
生成可执行文件@2x.png

推荐阅读更多精彩内容