iOS启动优化 —— LLVM编译流程 & Clang插件开发
<h2 id="1-llvm">1. LLVM</h2><h3 id="11-llvm概述">1.1 LLVM概述</h3>
<p><code>LLVM</code>是架构编译器的框架系统,以<code>C++</code>编写而成,用于优化任意程序语言编写的程序的<code>编译时间(compile-time)</code>、<code>链接时间(link-time)</code>、<code>运行时间(run-time)</code>以及<code>空闲时间(idle-time)</code>。对开发者保持开放,并兼容已有脚本。目前LLVM已经被苹果IOS开发工具,Xilinx Vivado, Facebook,Google等各大公司采用。</p>
<h3 id="12-传统编译器设计">1.2 传统编译器设计</h3>
<p>源码 <code>Source Code</code> + <code>前端 Frontend</code> + <code>优化器 Optimizer</code> + <code>后端 Backend</code>(代码生成器 CodeGenerator)+ <code>机器码 Machine Code</code>,如下图所示</p>
<p><img src="https://img2020.cnblogs.com/blog/1870864/202108/1870864-20210831143651471-1000082399.png"></p>
<ul>
<li>
<p><code>前端Frontend</code>:负责<code>解析源代码</code>,它会进行:<code>词法分析</code>、<code>语法分析</code>、<code>语义分析</code>,检查源代码是否存在<code>错误</code>。然后构建针对语言的<code>抽象语法树</code>(<code>AST:Abstract Syntax Tree</code>。LLVM 的前端还会生成<code>中间代码(intermediate representation,简称IR)</code>。</p>
</li>
<li>
<p><code>优化器 Optimizer</code>:优化器负责进行<code>各种优化</code>,改善代码的<code>运行时间</code>,例如<code>消除冗余计算</code>等;</p>
</li>
<li>
<p><code>后端 Backend</code>(代码生成器 Code Generator):将代码映射到目标指令集,生成机器代码,并且进行机器代码相关的代码优化;</p>
</li>
</ul>
<h3 id="13-ios的编译器架构">1.3 ios的编译器架构</h3>
<p><code>OC、C、C++</code>使用的编译器前端是<code>Clang</code>,<code>Swift</code>是swift,后端都是LLVM,如下图所示</p>
<p><img src="https://img2020.cnblogs.com/blog/1870864/202108/1870864-20210831143708336-1022298785.png"></p>
<h3 id="14-llvm的设计">1.4 LLVM的设计</h3>
<p>LLVM设计的最重要方面是,使用通用的代码表示形式<code>(IR)</code>,它是用来在编译器中表示代码的形式,所有LLVM可以为任何编程语言独立编写前端,并且可以为任意硬件架构独立编写后端,做到了前后端分离如下所示</p>
<p><img src="https://img2020.cnblogs.com/blog/1870864/202108/1870864-20210831143715245-1894377188.png"></p>
<p>传统的编译器,前端,优化器和后端是连在一起的,是一个项目。但是在llvm中,前端和后端分开了,两者中间有一个通用的中间层,也就是<code>IR</code>。前端<code>解析源代码</code>,然后<code>词法分析</code>、<code>语法分析</code>、<code>语义分析</code>、<code>AST</code>等工作完成之后,生成IR输出给<code>优化器</code>,优化器负责<code>优化IR代码</code>,然后后端接受IR代码后根据需要适配的设备生成X86、ARM64等。所以,当出现一个新设备,只需要研发一个新设备的后端。出现一个高级语言,就研发高级语言的前端。这样就能支持所有的语言和设备。</p>
<h3 id="15-clang">1.5 Clang</h3>
<p><code>clang</code>是<code>LLVM</code>项目中的一个<code>子项目</code>,它是基于LLVM架构图的<code>轻量级编译器</code>,诞生之初是为了替代<code>GCC</code>,提供<code>更快的编译速度</code>,它是负责C、C++、OC语言的编译器,属于整个LLVM架构中的 编译器<code>前端</code>,对于开发者来说,研究Clang可以给我们带来很多好处</p>
<h2 id="2-编译流程">2. 编译流程</h2>
<p>可以通过以下命令打印源码的编译阶段:</p>
<pre><code>clang -ccc-print-phases main.m
</code></pre>
<p>这里新建一个后通过命令打印源码的编译阶段:</p>
<p><img src="https://img2020.cnblogs.com/blog/1870864/202108/1870864-20210831143725268-1353299653.png"></p>
<ul>
<li>0 - <code>输入文件</code>:找到源文件</li>
<li>1 - <code>预处理阶段</code>:这个过程处理包括宏的替换,头文件的导入</li>
<li>2 - <code>编译阶段</code>:进行词法分析、语法分析、检测语法是否正确,最终生成IR</li>
<li>3 - <code>后端</code>:这里LLVM会通过一个一个的pass去优化,每个pass做一些事情,最终生成汇编代码</li>
<li>4 - <code>汇编代码生成目标文件</code></li>
<li>5 - <code>链接</code>:链接需要的动态库和静态库,生成可执行文件</li>
<li>6 - <code>绑定</code>:通过不同的架构,生成对应架构的可执行文件</li>
</ul>
<p>在main.m中输入一些代码。</p>
<p><img src="https://img2020.cnblogs.com/blog/1870864/202108/1870864-20210831143733361-1881374189.png"></p>
<p>然后通过 指令<code>clang -E main.m >> main1.m</code>生成预处理之后的文件。</p>
<p>开头是一些宏展开和.h文件的展开。</p>
<p><img src="https://img2020.cnblogs.com/blog/1870864/202108/1870864-20210831143740052-422635690.png"></p>
<p>然后最后看到main函数,这里成C没有了,变成了30.</p>
<p><img src="https://img2020.cnblogs.com/blog/1870864/202108/1870864-20210831143747491-385722562.png"></p>
<p>所以我们得出:</p>
<ul>
<li><code>typedef不是预处理指令</code>,也就是说:typedef可以给<code>数据类型取别名</code>,但是在<code>预处理阶段不会被替换掉</code>。</li>
<li><code>define则在预处理阶段会被替换</code>,所以经常被是用来进行<code>代码混淆</code>,目的是为了<code>app安全</code>。</li>
</ul>
<h2 id="3-编译阶段">3. 编译阶段</h2>
<p>编译阶段会进行<code>词法分析</code>、<code>语法分析</code>、<code>检测语法</code>是否正确,最终生成IR。</p>
<h3 id="31-词法分析">3.1 词法分析</h3>
<p>预处理完成后就会进行<code>词法分析</code>,这里会把代码切成一个个<code>Token</code>,比如<code>大小括号</code>,<code>等于号</code>还有<code>字符串</code>等。 通过下列指令来查看词法分析</p>
<pre><code>clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
</code></pre>
<p>词法分析结果:</p>
<p><img src="https://img2020.cnblogs.com/blog/1870864/202108/1870864-20210831143758264-652053848.png"></p>
<h3 id="32-语法分析">3.2 语法分析</h3>
<p>词法分析完成后就是<code>语法分析</code>,它的任务是验证语法是否正确,在词法分析的基础上将单词序列组合成各类词法短语,如<code>程序</code>、<code>语句</code>、<code>表达式</code> 等等,然后将所有节点组成<code>抽象语法树(Abstract Syntax Tree,AST)</code>,语法分析程序判断程序在结构上是否正确。</p>
<p>通过下列指令来查看语法分析</p>
<pre><code>clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
</code></pre>
<p>得到下面的结果(这里面的地址是虚拟地址,还没开辟内存,可以看作是文件的偏移地址):</p>
<p><img src="https://img2020.cnblogs.com/blog/1870864/202108/1870864-20210831143808395-1255115748.png"></p>
<ul>
<li><code>FunctionDecl</code> :函数方法声明。</li>
<li><code>ParmVarDecl</code>: 参数声明。</li>
<li><code>CompoundStmt</code>:复合语句。</li>
<li><code>CallExpr</code>:函数调用。</li>
<li><code>BinaryOperator</code>: 运算符。</li>
<li><code>ImplicitCastExpr</code>:函数指针。</li>
<li><code>DeclRefExpr</code>:函数类型。</li>
</ul>
<h3 id="33-生成中间代码ir">3.3 生成中间代码IR</h3>
<p>完成以上步骤后,就开始生成<code>中间代码IR</code>了,<code>代码生成器(Code Generation</code>)会将语法树自顶向下遍历逐步翻译成LLVM IR。</p>
<p>简化一下代码:</p>
<p><img src="https://img2020.cnblogs.com/blog/1870864/202108/1870864-20210831143816817-2039413090.png"></p>
<p>然后通过下列指令来生成 .ll 的文本文件,查看IR代码。</p>
<pre><code>clang -S -fobjc-arc -emit-llvm main.m
</code></pre>
<p>生成IR代码如下(这一步会进行语法检查):</p>
<p><img src="https://img2020.cnblogs.com/blog/1870864/202108/1870864-20210831143827112-874651595.png"></p>
<ul>
<li><code>@</code>:全局标识</li>
<li><code>%</code>:局部标识</li>
<li><code>alloca</code>: 开辟空间</li>
<li><code>align</code>: 内存对齐</li>
<li><code>i32</code>: 32bit,4个字节</li>
<li><code>store</code>: 写入内存</li>
<li><code>load</code>: 读取数据</li>
<li><code>call</code>: 调用函数</li>
<li><code>ret</code>: 返回</li>
</ul>
<p>上面的IR代码是没有经过优化的,所以会比较长。 LLVM的优化级别分别是: <code>-O0</code> , <code>-O1</code> , <code>-O2</code> , <code>-O3</code> , <code>-Os</code> 。 可以在<code>xcode</code>里面 <code>target</code> -> <code>Build Settings</code> -> <code>optimization Level</code> 设置优化等级。</p>
<p><img src="https://img2020.cnblogs.com/blog/1870864/202108/1870864-20210831143834908-195299633.png"></p>
<p>输入下列指令来<code>生成优化后的IR代码</code>。</p>
<pre><code>clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll
</code></pre>
<p>下面是优化后的IR代码,可以明显看出来代码少了很多。<code>优化等级并不是越高越好的</code>,一般情况下,<code>debug</code>模式下是<code>不进行优化的</code>,而在<code>release</code>模式下是<code>-Os 优化等级</code>。</p>
<p><img src="https://img2020.cnblogs.com/blog/1870864/202108/1870864-20210831143847428-1329248297.png"></p>
<h3 id="34-bitcode">3.4 bitCode</h3>
<p><code>xcode7</code>以后开启<code>bitcode</code>,苹果会做进一步优化,生成.bc的中间代码,我们通过优化后的IR代码生成.bc代码。Bitcode的目的是<code>根据不同的CPU架构,苹果能够在APPStore直接下载不同的架构的包</code>。</p>
<p>输入下列指令来生成bc代码。</p>
<pre><code>clang -emit-llvm -c main.ll -o main.bc
</code></pre>
<h2 id="4-生成汇编代码">4. 生成汇编代码</h2>
<p>到了这一步,这里就到了<code>backend</code>。这里<code>LLVM</code>会通过一个一个的<code>pass去优化</code>,每个pass做一些事情,最终生成汇编代码。</p>
<p>我们通过生成的.bc或者.ll代码生成汇编代码。</p>
<pre><code> clang -S -fobjc-arc main.bc -o main.s
clang -S -fobjc-arc main.ll -o main.s
</code></pre>
<p>这里分别通过<code>main.ll</code>,<code>main.bc</code>,<code>main.m</code>来生成汇编之后进行对比。</p>
<p><img src="https://img2020.cnblogs.com/blog/1870864/202108/1870864-20210831143856878-1938670419.png"></p>
<p>main.bc生成的汇编代码:</p>
<p><img src="https://img2020.cnblogs.com/blog/1870864/202108/1870864-20210831143904905-452364599.png"></p>
<p>main.ll生成的汇编代码:</p>
<p><img src="https://img2020.cnblogs.com/blog/1870864/202108/1870864-20210831143914715-1661010601.png"></p>
<p>main.m生成的汇编代码:</p>
<p><img src="https://img2020.cnblogs.com/blog/1870864/202108/1870864-20210831143925574-1759319020.png"></p>
<p>这里发现通过main.bc 和 main.ll 生成的汇编代码都是54行,说明并没有额外进行代码优化。main.m是没有经过优化的源码,转化为汇编后则多了几行代码。那么这里的代码是否还能进行优化呢?试一下。 输入以下代码</p>
<pre><code>clang -Os -S -fobjc-arc main.bc -o main3.s
</code></pre>
<p>这是指令运行后得到的代码,发现比之前的又少了几行,这就说明:当选定了优化等级了之后,在不同的节点上,还能进行优化。</p>
<p><img src="https://img2020.cnblogs.com/blog/1870864/202108/1870864-20210831143935368-2129991006.png"></p>
<h2 id="5-生成目标文件汇编器">5. 生成目标文件(汇编器)</h2>
<p>目标文件的生成,是汇编器以汇编代码作为插入,将汇编代码转换为机器代码,最后输出目标文件(object file)。</p>
<p>通过以下指令来生产.o文件</p>
<pre><code>clang -fmodules -c main.s -o main.o
</code></pre>
<p>可以通过nm命令,查看下main.o中的符号</p>
<pre><code>$xcrun nm -nm main.o
</code></pre>
<p>指令执行后发现输出下面的结果:</p>
<p><img src="https://img2020.cnblogs.com/blog/1870864/202108/1870864-20210831143944827-323463140.png"></p>
<ul>
<li><code>_printf</code>函数是一个是<code>undefined</code> 、<code>external</code> 的</li>
<li><code>undefined</code>表示在当前文件暂时<code>找不到符号_printf</code></li>
<li><code>external</code>表示这个符号是<code>外部可以访问</code>的</li>
</ul>
<p>这里为什么undefined了呢?因为这里调用了外部的方法,这个时候就需要链接了。</p>
<h2 id="6-生成可执行文件链接">6. 生成可执行文件(链接)</h2>
<p>链接主要是<code>链接需要的动态库和静态库,生成可执行文件。</code></p>
<p>连接器把编译生成的.o文件和 .dyld .a文件链接,生成一个mach-o文件</p>
<pre><code>clang main.o -o main
</code></pre>
<p>查看链接之后的符号</p>
<pre><code>$xcrun nm -nm main
</code></pre>
<p>指令执行后得到下面的结果:</p>
<p><img src="https://img2020.cnblogs.com/blog/1870864/202108/1870864-20210831144041460-918712271.png"></p>
<p>这里看到有<code>两个undefined</code>,一个是<code>_printf</code>,一个是<code>dyld_stub_binder</code>,但是后面都有<code>(from libSystem)</code>。这里的dyld_stub_binder也是一个<code>外部函数</code>,在dyld里面,当mach-o 进入到内存之后,外部符号就会和binder进行绑定。这个过程是dyld强制绑定的,这里就是去绑定_printf。 链接就是要知道<code>内部的符号是在外面的哪个库里面</code>。绑定就是将外面的函数的地址和内部的符号进行绑定。<code>链接</code>在<code>编译期</code>,<code>绑定</code>在<code>执行期</code>。所以只要链接就一定有一个外部函数也就是dyld_stub_binder。</p>
<h2 id="7-clang-插件">7. clang 插件</h2>
<h3 id="71-llvm下载">7.1 LLVM下载</h3>
<pre><code> 由于国内网络限制,需要借助镜像下载llvm的源码链接: (https://mirror.tuna.tsinghua.edu.cn/help/llvm/).
复制代码
</code></pre>
<p>下载LLVM项目</p>
<pre><code>git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/llvm.git
</code></pre>
<p>在LLVM的tool目录下下载Clang</p>
<pre><code>cd llvm/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang.git
</code></pre>
<p>在LLVM的projects目录下下载compiler-rt、libcxx、libcxxabi</p>
<pre><code>cd ../projects
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/compiler-rt.g it
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/libcxx.git git clone
https://mirrors.tuna.tsinghua.edu.cn/git/llvm/libcxxabi.git
</code></pre>
<p>在Clang的tools下安装extra工具</p>
<pre><code>cd ../tools/clang/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang-tools-extra.git
</code></pre>
<h3 id="72-llvm-编译">7.2 LLVM 编译</h3>
<p>由于最新的LLVM只支持cmake来编译,所以需要安装cmake</p>
<p>查看brew是否安装cmake,如果已经安装,则跳过下面步骤</p>
<pre><code>brew list
</code></pre>
<p>通过brew安装cmake</p>
<pre><code>brew install cmake
</code></pre>
<h3 id="73-编译llvm">7.3 编译LLVM</h3>
<p><strong>通过xcode编译LLVM</strong></p>
<ul>
<li>cmake编译成Xcode项目</li>
</ul>
<pre><code>mkdir build_xcode
cd build_xcode
cmake -G Xcode ../llvm
</code></pre>
<ul>
<li>使用xcode编译Clang</li>
</ul>
<p>选择手动创建schemes</p>
<p>编译(CMD + B),选择ALL_BUILD Secheme进行编译,预计1+小时。</p>
<p><strong>通过ninja编译LLVM</strong></p>
<p>使用ninja进行编译则还需要安装ninja,使用以下命令安装ninja</p>
<pre><code>brew install ninja
</code></pre>
<p>在LLVM源码根目录下新建一个build_ninja目录,最终会在build_ninja目录下生成``build.ninja`</p>
<p>在LLVM源码根目录下新建llvm_release目录,最终编译文件会在llvm_release文件夹路径下</p>
<pre><code>cd llvm_build
//注意DCMAKE_INSTALL_PREFIX后面不能有空格
cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX= 安装路径(本机为/ Users/xxx/xxx/LLVM/llvm_release)
</code></pre>
<p>依次执行编译,安装指令</p>
<pre><code>ninja
ninja install
</code></pre>
<h3 id="74-创建插件">7.4 创建插件</h3>
<p>在/llvm/tools/clang/tools下新建插件LSPlugin</p>
<p><img src="https://img2020.cnblogs.com/blog/1870864/202108/1870864-20210831144056160-1551019714.png"></p>
<p>在/llvm/tools/clang/tools目录下的CMakeLists.txt文件,新增add_clang_subdirectory(LSPlugin)。</p>
<p><img src="https://img2020.cnblogs.com/blog/1870864/202108/1870864-20210831144104989-825359420.png"></p>
<p>在LSPlugin目录下新建 LSPlugin.cpp 和CMakeLists.txt,并在CMakeLists.txt中加上以下代码</p>
<pre><code>add_llvm_library( HKPlugin MODULE BUILDTREE_ONLY
LSPlugin.cpp
)
</code></pre>
<p><img src="https://img2020.cnblogs.com/blog/1870864/202108/1870864-20210831144115043-1593628097.png"></p>
<p>接下来利用cmake重新生成Xcode项目,在build_xcode目录下执行以下命令</p>
<pre><code>cmake -G Xcode ../llvm
</code></pre>
<p>最后可以在LLVM的xcode项目中可以看到Loadable modules目录下由自定义的LSPlugin目录了,然后可以在里面编写插件代码了。</p>
<h3 id="面试基础">面试基础</h3>
<p>iOS面试基础知识 (一)</p>
<p>iOS面试基础知识 (二)</p>
<p>iOS面试基础知识 (三)</p>
<p>iOS面试基础知识 (四)</p>
<p>iOS面试基础知识 (五)</p>
<h3 id="知识详解">知识详解</h3>
<p>iOS面试要点之GCD面试要点</p>
<p>iOS面试要点之多线程面试要点</p>
<p>iOS面试要点之block面试要点</p>
<p>iOS面试要点之Runtime面试要点</p>
<p>iOS面试要点之RunLoop面试要点</p>
<p>iOS面试要点之内存管理面试要点</p>
<p>iOS面试要点之MVC、MVVM面试要点</p>
<p>iOS面试要点之网络性能优化要点</p>
<p>iOS面试要点之网络编程面试要点</p>
<p>iOS面试要点之KVC&KVO面试要点</p>
<p>iOS面试要点之数据存储面试要点</p>
<p>iOS面试要点之混编技术面试要点</p>
<p>iOS面试要点之设计模式面试要点</p>
<p>iOS面试要点之UI面试要点</p><br><br>
来源:https://www.cnblogs.com/Julday/p/15210286.html
頁:
[1]