iOS-LLVM初探

前言

  计算机是只能直接理解机器语言,而不能直接理解高级语言的,所以计算机要执行高级语言编写的程序,就必须要把高级语言翻译成机器语言。这种翻译有两种方式,一是编译,一是解释。所以不能被计算机直接识别的编程语言也可以分为解释型语言和编译型语言。

解释型语言

  程序不需要提前编译,只需要在运行时使用专门的解释器对源程序逐行解释成特定平台的机器码并执行,代码在执行时才被解释器一行行动态翻译和执行。而且每执行一次都要翻译一次,因此效率比较低。由于其跨平台性比较好,一般用于脚本、辅助开发等。常见的解释型语言有PythonJavaScriptVBScriptPerlRubyMATLAB等等。

其特点如下:

  • 每次执行都需要将源代码逐行进行解释,效率较低
  • 需要一个专门的解释器
  • 跨平台性好,只要平台提供相应的解释器,就可以运行源代码,方便源程序移植

编译型语言

  程序在执行之前需要一个专门的编译过程,使用专门的编译器,针对特定的平台,将源代码一次性的编译成可被该平台硬件执行的代码,并包装成该平台所能识别的可执行性程序的格式。在运行时不需要重新翻译,直接使用编译的结果即可。程序执行效率高,依赖编译器,跨平台性差些。一般用于开发操作系统、大型应用程序、数据库系统等。常见的编译型语言CC++Objective-C等等。

其特点如下:

  • 在执行前有一个专门的编译过程,将编码编译为可执行文件
  • 运行时不需要编译,执行效率高
  • 依赖编译器,跨平台性差

iOS指令架构

Objective-C作为一门编译型语言,在编译完成之后,其执行还需要依赖相关的指令集架构。苹果平台的iPhone的处理器的指令集有armv7armv7sarm64,而Mac处理器的指令集i386x86_64i386是针对intel通用微处理器32位处理器,x86_64是针对x86架构的64位处理器。

不同机型的指令集

真机的指令集:

  • iPhone5S及以上机型使用都是arm64指令集,64位处理器
  • iPhone5iPhone5CiPad4使用的则是armv7s指令集,32位处理器
  • 更低版本的手机和iPad则是armv7指令集,32位处理器

模拟器指令集:

  • 模拟器32位处理器测试需要i386架构,
  • 模拟器64位处理器测试需要x86_64架构,

Xcode中指令集相关Build Setting

  • Architectures:指定工程被编译成可支持哪些指令集类型,而支持的指令集越多,就会编译出包含多个指令集代码的数据包,对应生成二进制包就越大,也就是ipa包会变大。

  • Valid Architectures:限制可能被支持的指令集的范围,也就是Xcode编译出来的二进制包类型最终从这些类型产生,而编译出哪种指令集的包,则由ArchitecturesValid Architectures的交集来确定。因此这个选项不能为空。

  • Build Active Architecture Only:指定是否只对当前连接设备所支持的指令集编译。当其值设置为YES,是为了debug的时候编译速度更快,它只编译Architecture版本,而设置为NO时,会编译所有的版本。所以,一般debug的时候设置为YESrelease的时候设置为NO

编译器相关

  简单的说,编译器就是将“一种语言(通常为高级语言)”翻译为“另一种语言(通常为低级语言)”的程序。其主要工作流程:源代码(source code) → 预处理器(preprocessor) → 编译器(compiler) → 目标代码(object code) → 链接器(Linker) → 可执行程序(executable)

总结一下,如图所示:

image
  • Frontend:编译器前端,负责解析源代码。用于词法分析、语法分析、语义分析、检查代码的语法错误、构建抽象语法树AST(Abstract Syntax Tree)
  • Optimizer:优化器,负责优化代码。如消除冗余等。
  • Backend:后端,负责生成机器代码。将代码映射到对应的指令集,并负责生成相关的代码,并且优化。

Objective-C作为一门编译型语言,其编译器就是clang,而clang又是LLVM的一部分。

LLVM概述

LLVM命名最早源自于底层虚拟机(Low Level Virtual Machine)的缩写,由于命名带来的混乱,目前LLVM就是该项目的全称。

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

当编译器需要支持多种语言或者多种硬件架构的时候,LLVM的强大之处就体现出来了。普通的编译器都是作为整体应用程序来编写的,只能对单语言进行支持。而LLVM最核心之处就是使用通用的代码表示形式IR(intermediate representation),即不同的前端语言最终都转换成同一种IR,让其可以为任何语言编写前端、为任何硬件编写后端。

image

Clang是一个C++编写、基于LLVM、发布于LLVM BSD许可证下的C/C++/Objective-C/Objective-C++编译器。Clang的诞生是为了代替GCC,它是一个高度模块化开发的轻量级编译器,编译速度快、占用内存小、非常方便进行二次开发。

编译流程

首先我们在main.m中实现如下代码:

image

通过clang命令可以打印代码的编译流程:

 clang -ccc-print-phases main.m
    1. input, "main.m", objective-c 输入文件:找到源文件
    1. preprocessor, {0}, objective-c-cpp-output 预处理阶段:包含导入头文件、替换宏定义等
    1. compiler, {1}, ir 编译阶段:进行词法分析、语法分析、检查语法的正确性,最终生成IR
    1. backend, {2}, assembler 后端优化,生成汇编代码
    1. assembler, {3}, object 生成目标文件
    1. linker, {4}, image 链接需要的动态库和静态库,生成可执行文件
    1. bind-arch, "x86_64", {5}, image 通过不同的架构,生成相应的镜像文件

预处理阶段

使用如下命令,就可以查看预处理阶段做了什么:

clang -E main.m >> mian2.m

打开main2.m

image

可以看出头文件相关的信息都被导入,而且宏定义也被替换了。需要注意的是typedef不是预处理阶段处理的。

编译阶段

词法分析

预处理阶段完成之后就会进入词法分析,这里会把代码切成一个个Token,比如大小括号、字符串等等。使用如下命令,就可以查看词法分析做了什么:

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

从结果可以看出,词法分析是把代码一个词一个词的分开,标记了具体的位置,如行号和第一个字符的位置。

语法分析

词法分析之后就是语法分析,语法分析所做的事情就是检查语法是否正确。它是在词法分析的基础上将拆分的单词组合成短语,然后将所有节点组合成抽象语法树(AST abstract syntax tree)。使用如下命令,就可以查看语法分析做了什么:

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

结合原来的代码,可以看出FunctionDecl这就是main函数的标识,说明从13行第1个字符到18行第1个字符就是main函数。DeclStmt就是参数节点。CallExpr是调用函数的返回值,ImplicitCastExpr是函数的参数指针,BinaryOperator是运算符。ReturnStmt则是返回值。

生成中间代码(IR)

这是LLVM最独特的一点。语法分析之后,代码生成器就会根据语法树生成IR

IR的语法:

  • @ 全局标识
  • % 局部标识
  • alloca 开辟空间
  • align 内存对齐
  • i32 4字节
  • store 写入内存
  • load 读取数据
  • call 调用数据
  • ret 返回

使用如下命令,就可以查看生成的IR

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

IR的优化级别:O0O1O2O3Os,第一个字符是英文大写字母的o(哦)。其中O0表示NoneO1表示FastO2表示FasterO3表示FastestOs表示Fastest Samllest

使用如下命令可以查看不同优化程度的IR代码:

clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll
image

可以看出代码进行了很大幅度的优化,大部分冗余代码都被去掉了。

还可以使用如下命令将IR代码生成bitCode:

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

生成汇编代码

我们可以将IR代码、bitCode、或者是源码生成汇编代码,这里就不一一展示了。

 clang -S -fobjc-arc main.bc -o main.s 
 clang -S -fobjc-arc main.ll -o main.s
 clang -Os -S -fobjc-arc main.m -o main.s
image

生成目标文件

使用如下命令将汇编代码生成可执行文件:

clang -fmodules -c main.s -o main.o

我们可以通过如下命令查看main.o文件的内容:

xcrun nm -nm main.o

结果如下:

xcrun nm -nm main.o
                 (undefined) external _printf
0000000000000000 (__TEXT,__text) external _main

其中external表示外部可以访问符号,undefined表示当前文件未找到符号。

生成可执行文件

链接器把.o文件和动态库、静态库进行链接,生成可执行文件。

通过如下命令即可:

clang main.o -o main

另外我们还可以通过如下命令,查看生成的可执行文件的符号。

xcrun nm -nm main

结果如下:

xcrun nm -nm main
                 (undefined) external _printf (from libSystem)
                 (undefined) external dyld_stub_binder (from libSystem)
0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
0000000100000f40 (__TEXT,__text) external _main
0000000100002008 (__DATA,__data) non-external __dyld_private

(undefined) external _printf (from libSystem)表示_printf这个符号来自于libSystem

LLVM下载及编译

相关文件下载

由于国内网络的显示,需要借助镜像下载LLVM
地址:https://mirror.tuna.tsinghua.edu.cn/help/llvm/

  • 下载地址:
https://mirror.tuna.tsinghua.edu.cn/help/llvm/
  • llvmtools目录下下载clang
cd llvm/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang.git
  • llvmprojects目录下下载compiler-rtlibcxxlibcxxabi
cd ../projects
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/compiler-rt.git
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/libcxx.git git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/libcxxabi.git
  • clangtools目录下安装clang-tools-extra
cd ../tools/clang/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang-tools-extra.git
  • 安装cmake
brew install cmake

clang编译

在我们下载的llvm的同级文件中创建一个文件夹,使用cmake编译成xcode项目:

mkdir build_llvm_xcode
cd build_llvm_xcode
cmake -G Xcode ../llvm
image

打开工程,先不要编译(直接编译会非常的非常的非常的耗时。。。),选择手动创建Scheme,添加clanglibclang

实现clang插件

前面我们大概熟悉了LLVM工程,现在就来简单实现一个Xcode的插件用以检测NSString *属性未使用copy修饰词并给予提示,更进一步的了解一下其原理。

创建插件文件

    1. /llvm/tools/clang/tools目录下创建一个文件夹TPlugin
    1. 修改/llvm/tools/clang/tools目录下的CMakeLists.txt文件,添加一行add_clang_subdirectory(TPlugin)
    1. TPlugin目录下创建两个文件TPlugin.cppCMakeLists.txt;在CMakeLists.txt中输入如下代码:
add_llvm_library( TPlugin MODULE BUILDTREE_ONLY 
    TPlugin.cpp
)
    1. build_llvm_xcode文件夹中,重新执行如下代码:
cmake -G Xcode ../llvm

这样插件文件就创建成功了,我们可以从LLVM的这个Xcode工程的Loadable modules目录下就可以看见自己创建的目录了。

编写插件代码

我们知道编译器检查语法错误是在语法分析生成抽象语法树的时候,所以这个插件的核心就是重写抽象语法树的相关功能。

我们实现如下代码,然后对其进行编译,查看语法树:

#import "TASTProperty.h"

@interface TASTProperty()

@property (nonatomic, copy) NSString *mString;
@property (nonatomic, copy) NSDictionary *mDict;
@property (nonatomic, strong) NSString *str01;
@property (nonatomic, strong) NSString *str02;

@end

@implementation TASTProperty

@end
image

可以清楚的看到,ObjCPropertyDecl就是我们需要的语法树的节点,而且定义的属性都能看到其对应的修饰词。所以我们可以读取到语法树的节点,根据节点信息来做判断。

  1. 导入相关的库
#include 
#include "clang/AST/AST.h"
#include "clang/AST/DeclObjC.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/Frontend/FrontendPluginRegistry.h"

using namespace clang;
using namespace std;
using namespace llvm;
using namespace clang::ast_matchers;
  1. 创建自定义的ASTAction,注册插件
namespace TPlugin {
    
    // 继承PluginASTAction实现我们自定义的 Action
    class TASTAction: public PluginASTAction {
    public:
        bool ParseArgs(const CompilerInstance &CI,const vector &arg){
            return true;
        }
        
        unique_ptr  CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
            return unique_ptr  (new TConsumer(CI));
        }
    };
}

// 注册插件
static FrontendPluginRegistry:: Add
    X("TPlugin", "This is the description of the plugin");
  1. 自定义TConsumer继承自ASTConsumer,重写解析语法树的节点的方法
class TConsumer: public ASTConsumer {
        
    private:
        MatchFinder matcher;
        TMatchCallback callback;
        
    public:
        TConsumer(CompilerInstance &CI):callback(CI) {
            // 1. 添加一个MatchFinder去匹objcPropertyDecl节点
            // 2. 回调在TMatchCallback的run方法里面
            matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"),&callback);
        }

        // 在整个文件都解析完后被调用
        void HandleTranslationUnit(ASTContext &context) {
            cout<<"语法树解析完毕了!"<
  1. 自定义TMatchCallback继承自MatchFinder用来匹配节点 也就是选中特殊的节点,然后回调给TConsumer
class TMatchCallback: public MatchFinder::MatchCallback {
        
    private:
        CompilerInstance &CI;
        //判断是否是自己的文件
        bool isUserSourceCode(const string filename) {
            if (filename.empty()) return false;

            // 非Xcode中的源码都认为是用户源码
            if (filename.find("/Applications/Xcode.app/") == 0) return false;

            return true;
        }

        // 暂时就判断NSString才应该用copy修饰。
        bool isNeedCopy(const string typeStr) {
            return typeStr.find("NSString") != string::npos;
        }

    public:
        TMatchCallback(CompilerInstance &CI):CI(CI) { }

        void run(const MatchFinder::MatchResult &Result) {
            //通过结果获取到节点。
            const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs("objcPropertyDecl");
            //获取文件名称
            string filename = CI.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str();

            if (propertyDecl && isUserSourceCode(filename)) {//如果节点有值,并且是用户文件
                //拿到属性的类型
                string typeStr = propertyDecl->getType().getAsString();
                //拿到节点的描述信息
                ObjCPropertyDecl::PropertyAttributeKind attrKind = propertyDecl->getPropertyAttributes();

                //判断是不是应该用Copy
                if (isNeedCopy(typeStr) && !(attrKind & ObjCPropertyDecl::OBJC_PR_copy)) {
                    cout<getBeginLoc(),diag.getCustomDiagID(DiagnosticsEngine::Warning, "%0这个地方推荐用Copy"))<

这样插件的代码基本上就实现了,下面我们将插件继承到Xcode上。

Xcode集成插件

打开需要的测试的工程,修改Build Settings相关设置。

  1. Other C Flags中添加如下内容:
-Xclang -load -Xclang TPlugin生成的动态库地址() -Xclang -add-plugin -Xclang TPlugin
  1. Enable Index-Wihle-Building Functionalityvalue改成NO
  2. Build Settings目录栏点击加号,选择Add User-Defined Setting,加入两项,一项keyCCvalue是自己编译的clang的路径,一项keyCXXvalue是自己编译的clang++的路径(.../build_llvm_xcode/Debug/bin/clang++)

编译程序,就可以看到会有提示:

image

总结

clang编译程序的流程如下:

    1. 输入文件:找到源文件
    1. 预处理阶段:包含导入头文件、替换宏定义等
    1. 编译阶段:进行词法分析、语法分析、检查语法的正确性,最终生成IR
    1. 后端优化,生成汇编代码
    1. 生成目标文件
    1. 链接需要的动态库和静态库,生成可执行文件
    1. 通过不同的架构,生成相应的镜像文件

你可能感兴趣的:(iOS-LLVM初探)