深入剖析 iOS 编译 Clang / LLVM

浏览: 87 发布日期: 2017-01-07 分类: ios

前言

iOS 开发中 Objective-C 和 Swift 都用的是 Clang / LLVM 来编译的。LLVM是一个模块化和可重用的编译器和工具链技术的集合,Clang 是 LLVM 的子项目,是 C,C++ 和 Objective-C 编译器,目的是提供惊人的快速编译,比 GCC 快3倍,其中的 clang static analyzer 主要是进行语法分析,语义分析和生成中间代码,当然这个过程会对代码进行检查,出错的和需要警告的会标注出来。LLVM 核心库提供一个优化器,对流行的 CPU 做代码生成支持。lld 是 Clang / LLVM 的内置链接器,clang 必须调用链接器来产生可执行文件。

LLVM 比较有特色的一点是它能提供一种代码编写良好的中间表示 IR,这意味着它可以作为多种语言的后端,这样就能够提供语言无关的优化同时还能够方便的针对多种 CPU 的代码生成。

编译流程

在列出完整步骤之前可以先看个简单例子。看看是如何完成一次编译的。

#import <Foundation/Foundation.h>
#define DEFINEEight 8

int main(){
    @autoreleasepool {
        int eight = DEFINEEight;
        int six = 6;
        NSString* site = [[NSString alloc] initWithUTF8String:"starming"];
        int rank = eight + six;
        NSLog(@"%@ rank %d", site, rank);
    }
    return 0;
}

在命令行编译

xcrun -sdk iphoneos clang -arch armv7 -F Foundation -fobjc-arc -c main.m -o main.o
xcrun -sdk iphoneos clang main.o -arch armv7 -fobjc-arc -framework Foundation -o main

在手机上就能够直接执行main了。这样还没发看清clang的全部过程,可以通过-E查看clang在预处理处理这步做了什么。

clang -E main.m

执行完后可以看到文件

# 1 "/System/Library/Frameworks/Foundation.framework/Headers/FoundationLegacySwiftCompatibility.h" 1 3
# 185 "/System/Library/Frameworks/Foundation.framework/Headers/Foundation.h" 2 3
# 2 "main.m" 2

int main(){
    @autoreleasepool {
        int eight = 8;
        int six = 6;
        NSString* site = [[NSString alloc] initWithUTF8String:"starming"];
        int rank = eight + six;
        NSLog(@"%@ rank %d", site, rank);
    }
    return 0;
}

这个过程的处理包括宏的替换,头文件的导入,以及类似#if的处理。预处理完成后就会进行词法分析,这里会把代码切成一个个 Token,比如大小括号,等于号还有字符串等。

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

然后是语法分析,验证语法是否正确,然后将所有节点组成抽象语法树 AST 。

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

完成这些步骤后就可以开始IR中间代码的生成了,CodeGen 会负责将语法树自顶向下遍历逐步翻译成 LLVM IR,IR 是编译过程的前端的输出后端的输入。

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

这里 LLVM 会去做些优化工作,在 Xcode 的编译设置里也可以设置优化级别-01,-03,-0s,还可以写些自己的 Pass。

Pass 是 LLVM 优化工作的一个节点,一个节点做些事,一起加起来就构成了 LLVM 完整的优化和转化。

如果开启了 bitcode 苹果会做进一步的优化,有新的后端架构还是可以用这份优化过的 bitcode 去生成。

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

生成汇编

clang -S -fobjc-arc main.m -o main.s

生成目标文件

clang -fmodules -c main.m -o main.o

生成可执行文件,这样就能够执行看到输出结果

clang main.o -o main
执行
./main
输出
starming rank 14

下面是完整步骤:

  • 编译信息写入辅助文件,创建文件架构 .app 文件
  • 处理文件打包信息
  • 执行 CocoaPod 编译前脚本,checkPods Manifest.lock
  • 编译.m文件,使用 CompileC 和 clang 命令
  • 链接需要的 Framework
  • 编译 xib
  • 拷贝 xib ,资源文件
  • 编译 ImageAssets
  • 处理 info.plist
  • 执行 CocoaPod 脚本
  • 拷贝标准库
  • 创建 .app 文件和签名

Clang 编译 .m 文件

在 Xcode 编译过后,可以通过 Show the report navigator 里对应 target 的 build 中查看每个 .m 文件的 clang 编译信息。

具体拿编译 AFSecurityPolicy.m 的信息来看看。首先对任务进行描述。

CompileC DerivedData path/AFSecurityPolicy.o AFNetworking/AFNetworking/AFSecurityPolicy.m normal x86_64 objective-c com.apple.compilers.llvm.clang.1_0.compiler

接下来对会更新工作路径,同时设置 PATH

cd /Users/didi/Documents/Demo/GitHub/GCDFetchFeed/GCDFetchFeed/Pods
    export LANG=en_US.US-ASCII
    export PATH="/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/usr/bin:/Applications/Xcode.app/Contents/Developer/usr/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"

接下来就是实际的编译命令

clang -x objective-c -arch x86_64 -fmessage-length=0 -fobjc-arc... -Wno-missing-field-initializers ... -DDEBUG=1 ... -isysroot iPhoneSimulator10.1.sdk -fasm-blocks ... -I -F -c AFSecurityPolicy.m -o AFSecurityPolicy.o

clang 命令参数

-x 编译语言比如objective-c
-arch 编译的架构,比如arm7
-f 以-f开头的。
-W 以-W开头的,可以通过这些定制编译警告
-D 以-D开头的,指的是预编译宏,通过这些宏可以实现条件编译
-iPhoneSimulator10.1.sdk 编译采用的iOS SDK版本
-I 把编译信息写入指定的辅助文件
-F 需要的Framework
-c 标识符指明需要运行预处理器,语法分析,类型检查,LLVM生成优化以及汇编代码生成.o文件
-o 编译结果

构建 Target

编译工程中的第三方依赖库后会构建我们程序的 target,会按顺序输出如下的信息:

Create product structure
Process product packaging
Run custom shell script 'Check Pods Manifest.lock'
Compile ... 各个项目中的.m文件
Link /Users/... 路径
Copy ... 静态文件
Compile asset catalogs
Compile Storyboard file ...
Process info.plist
Link Storyboards
Run custom shell script 'Embed Pods Frameworks'
Run custom shell script 'Copy Pods Resources'
...
Touch GCDFetchFeed.app
Sign GCDFetchFeed.app

从这些信息可以看出在这些步骤中会分别调用不同的命令行工具来执行。

Target 在 Build 过程的控制

在 Xcode 的 Project editor 中的 Build Setting,Build Phases 和 Build Rules 能够控制编译的过程。

Build Phases

构建可执行文件的规则。指定 target 的依赖项目,在 target build 之前需要先 build 的依赖。在 Compile Source 中指定所有必须编译的文件,这些文件会根据 Build Setting 和 Build Rules 里的设置来处理。

在 Link Binary With Libraries 里会列出所有的静态库和动态库,它们会和编译生成的目标文件进行链接。

build phase 还会把静态资源拷贝到 bundle 里。

可以通过在 build phases 里添加自定义脚本来做些事情,比如像 CocoaPods 所做的那样。

Build Rules

指定不同文件类型如何编译。每条 build rule 指定了该类型如何处理以及输出在哪。可以增加一条新规则对特定文件类型添加处理方法。

Build Settings

在 build 的过程中各个阶段的选项的设置。

pbxproj工程文件

build 过程控制的这些设置都会被保存在工程文件 .pbxproj 里。在这个文件中可以找 rootObject 的 ID 值

rootObject = 3EE311301C4E1F0800103FA3 /* Project object */;

然后根据这个 ID 找到 main 工程的定义。

/* Begin PBXProject section */
        3EE311301C4E1F0800103FA3 /* Project object */ = {
            isa = PBXProject;
            ...
/* End PBXProject section */

在 targets 里会指向各个 taget 的定义

targets = (
    3EE311371C4E1F0800103FA3 /* GCDFetchFeed */,
    3EE311501C4E1F0800103FA3 /* GCDFetchFeedTests */,
    3EE3115B1C4E1F0800103FA3 /* GCDFetchFeedUITests */,
);

顺着这些 ID 就能够找到更详细的定义地方。比如我们通过 GCDFetchFeed 这个 target 的 ID 找到定义如下:

3EE311371C4E1F0800103FA3 /* GCDFetchFeed */ = {
    isa = PBXNativeTarget;
    buildConfigurationList = 3EE311651C4E1F0800103FA3 /* configuration list for PBXNativeTarget "GCDFetchFeed" 
    buildPhases = (
        9527AA01F4AAE11E18397E0C /* Check Pods st.lock */,
        3EE311341C4E1F0800103FA3 /* Sources */,
        3EE311351C4E1F0800103FA3 /* Frameworks */,
        3EE311361C4E1F0800103FA3 /* Resources */,
        C3DDA7C46C0308459A18B7D9 /* Embed Pods Frameworks 
        DD33A716222617FAB49F1472 /* Copy Pods Resources 
    );
    buildRules = (
    );
    dependencies = (
    );
    name = GCDFetchFeed;
    productName = GCDFetchFeed;
    productReference = 3EE311381C4E1F0800103FA3 /* chFeed.app */;
    productType = "com.apple.product-type.application";
};

这个里面又有更多的 ID 可以得到更多的定义,其中 buildConfigurationList 指向了可用的配置项,包含 Debug 和 Release。可以看到还有 buildPhases,buildRules 和 dependencies 都能够通过这里索引找到更详细的定义。

接下来看看 Clang 所做的事情。

Clang Static Analyzer静态代码分析

clang 静态分析是通过建立分析引擎和 checkers 所组成的架构,这部分功能可以通过 clang —analyze 命令方式调用。clang static analyzer 分为 analyzer core 分析引擎和 checkers 两部分,所有 checker 都是基于底层分析引擎之上,通过分析引擎提供的功能能够编写新的 checker。

可以通过 clang --analyze -Xclang -analyzer-checker-help 来列出当前 clang 版本下所有 checker。如果想编写自己的 checker,可以在 clang 项目的 StaticAnalyzer/Checkers 目录下找到实例参考。这种方式能够方便用户扩展对代码检查规则或者对 bug 类型进行扩展,但是这种架构也有不足,每执行完一条语句后,分析引擎会遍历所有 checker 中的回调函数,所以 checker 越多,速度越慢。通过 clang -cc1 -analyzer-checker-help 可以列出能调用的 checker,下面是常用 checker

debug.ConfigDumper              Dump config table
debug.DumpCFG                   Display Control-Flow Graphs
debug.DumpCallGraph             Display Call Graph
debug.DumpCalls                 Print calls as they are traversed by the engine
debug.DumpDominators            Print the dominance tree for a given CFG
debug.DumpLiveVars              Print results of live variable analysis
debug.DumpTraversal             Print branch conditions as they are traversed by the engine
debug.ExprInspection            Check the analyzer's understanding of expressions
debug.Stats                     Emit warnings with analyzer statistics
debug.TaintTest                 Mark tainted symbols as such.
debug.ViewCFG                   View Control-Flow Graphs using GraphViz
debug.ViewCallGraph             View Call Graph using GraphViz
debug.ViewExplodedGraph         View Exploded Graphs using GraphViz

这些 checker 里最常用的是 DumpCFG,DumpCallGraph,DumpLiveVars 和 DumpViewExplodedGraph。

clang static analyzer 引擎大致分为 CFG,MemRegion,SValBuilder,ConstraintManager 和 ExplodedGraph 几个模块。clang static analyzer 本质上就是 path-sensitive analysis,要很好的理解 clang static analyzer 引擎就需要对 Data Flow Analysis 有所了解,包括迭代数据流分析,path-sensitive,path-insensitive ,flow-sensitive等。

编译的概念(词法->语法->语义->IR->优化->CodeGen)在 clang static analyzer 里到处可见,例如 Relaxed Live Variables Analysis 可以减少分析中的内存消耗,使用 mark-sweep 实现 Dead Symbols 的删除。

clang static analyzer 提供了很多辅助方法,比如 SVal.dump(),MemRegion.getString 以及 Stmt 和 Dcel 提供的 dump 方法。Clang 抽象语法树 Clang AST 常见的 API 有 Stmt,Decl,Expr 和 QualType。在编写 checker 时会遇到 AST 的层级检查,这时有个很好的接口 StmtVisitor,这个接口类似 RecursiveASTVisitor。

整个 clang static analyzer 的入口是 AnalysisConsumer,接着会调 HandleTranslationUnit() 方法进行 AST 层级进行分析或者进行 path-sensitive 分析。默认会按照 inline 的 path-sensitive 分析,构建 CallGraph,从顶层 caller 按照调用的关系来分析,具体是使用的 WorkList 算法,从 EntryBlock 开始一步步的模拟,这个过程叫做 intra-procedural analysis(IPA)。这个模拟过程还需要对内存进行模拟,clang static analyzer 的内存模型是基于《A Memory Model for Static Analysis of C Programs》这篇论文而来, 在clang里的具体实现代码可以查看这两个文件  MemRegion.h 和  RegionStore.cpp 。

下面举个简单例子看看 clang static analyzer 是如何对源码进行模拟的。

int main()
{
    int a;
    int b = 10;
    a = b;
    return a;
}

对应的 AST 以及 CFG

#----------------AST-------------------
# clang -cc1 -ast-dump
TranslationUnitDecl 0xc75b450 <<invalid sloc>> <invalid sloc>
|-TypedefDecl 0xc75b740 <<invalid sloc>> <invalid sloc> implicit __builtin_va_list 'char *'
`-FunctionDecl 0xc75b7b0 <test.cpp:1:1, line:7:1> line:1:5 main 'int (void)'
  `-CompoundStmt 0xc75b978 <line:2:1, line:7:1>
    |-DeclStmt 0xc75b870 <line:3:2, col:7>
    | `-VarDecl 0xc75b840 <col:2, col:6> col:6 used a 'int'
    |-DeclStmt 0xc75b8d8 <line:4:2, col:12>
    | `-VarDecl 0xc75b890 <col:2, col:10> col:6 used b 'int' cinit
    |   `-IntegerLiteral 0xc75b8c0 <col:10> 'int' 10

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< a = b <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
    |-BinaryOperator 0xc75b928 <line:5:2, col:6> 'int' lvalue '='
    | |-DeclRefExpr 0xc75b8e8 <col:2> 'int' lvalue Var 0xc75b840 'a' 'int'
    | `-ImplicitCastExpr 0xc75b918 <col:6> 'int' <LValueToRValue>
    |   `-DeclRefExpr 0xc75b900 <col:6> 'int' lvalue Var 0xc75b890 'b' 'int'
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

    `-ReturnStmt 0xc75b968 <line:6:2, col:9>
      `-ImplicitCastExpr 0xc75b958 <col:9> 'int' <LValueToRValue>
        `-DeclRefExpr 0xc75b940 <col:9> 'int' lvalue Var 0xc75b840 'a' 'int'
#----------------CFG-------------------
# clang -cc1 -analyze -analyzer-checker=debug.DumpCFG
int main()
 [B2 (ENTRY)]
   Succs (1): B1

 [B1]
   1: int a;
   2: 10
   3: int b = 10;
   4: b
   5: [B1.4] (ImplicitCastExpr, LValueToRValue, int)
   6: a
   7: [B1.6] = [B1.5]
   8: a
   9: [B1.8] (ImplicitCastExpr, LValueToRValue, int)
  10: return [B1.9];
   Preds (1): B2
   Succs (1): B0

 [B0 (EXIT)]
   Preds (1): B1

CFG 将程序拆得更细,能够将执行的过程表现的更直观些,为了避免路径爆炸,函数 inline 的条件会设置的比较严格,函数 CFG 块多时不会进行 inline 分析,模拟栈深度超过一定值不会进行 inline 分析,这个默认是5。

Clang Attributes

以 attribute(xx) 的语法格式出现,是 Clang 提供的一些能够让开发者在编译过程中参与一些源码控制的方法。下面列一些会用到的用法:

attribute((format(NSString, F, A))) 格式化字符串

可以查看 NSLog 的用法

FOUNDATION_EXPORT void NSLog(NSString *format, ...) NS_FORMAT_FUNCTION(1,2) NS_NO_TAIL_CALL;

// Marks APIs which format strings by taking a format string and optional varargs as arguments
#if !defined(NS_FORMAT_FUNCTION)
    #if (__GNUC__*10+__GNUC_MINOR__ >= 42) && (TARGET_OS_MAC || TARGET_OS_EMBEDDED)
    #define NS_FORMAT_FUNCTION(F,A) __attribute__((format(__NSString__, F, A)))
    #else
    #define NS_FORMAT_FUNCTION(F,A)
    #endif
#endif

attribute((deprecated(s))) 版本弃用提示

在编译过程中能够提示开发者该方法或者属性已经被弃用

- (void)preMethod:( NSString *)string __attribute__((deprecated("preMethod已经被弃用,请使用newMethod")));
- (void)deprecatedMethod DEPRECATED_ATTRIBUTE; //也可以直接使用DEPRECATED_ATTRIBUTE这个系统定义的宏

attribute((availability(os,introduced=m,deprecated=n, obsoleted=o,message="" VA_ARGS))) 指明使用版本范围

os 指系统的版本,m 指明引入的版本,n 指明过时的版本,o 指完全不用的版本,message 可以写入些描述信息。

- (void)method __attribute__((availability(ios,introduced=3_0,deprecated=6_0,obsoleted=7_0,message="iOS3到iOS7版本可用,iOS7不能用")));

attribute((unavailable(…))) 方法不可用提示

这个会在编译过程中告知方法不可用,如果使用了还会让编译失败。

attribute((unused))

没有被使用也不报警告

attribute((warn_unused_result))

不使用方法的返回值就会警告,目前 swift3 已经支持该特性了。oc中也可以通过定义这个attribute来支持。

attribute((availability(swift, unavailable, message=_msg)))

OC 的方法不能在 Swift 中使用。

attribute((cleanup(…))) 作用域结束时自动执行一个指定方法

作用域结束包括大括号结束,return,goto,break,exception 等情况。这个动作是先于这个对象的 dealloc 调用的。

Reactive Cocoa 中有个比较好的使用范例,@onExit 这个宏,定义如下:

#define onExit 
    rac_keywordify 
    __strong rac_cleanupBlock_t metamacro_concat(rac_exitBlock_, __LINE__) __attribute__((cleanup(rac_executeCleanupBlock), unused)) = ^

static inline void rac_executeCleanupBlock (__strong rac_cleanupBlock_t *block) {
    (*block)();
}

这样可以在就可以很方便的把需要成对出现的代码写在一起了。同样可以在 Reactive Cocoa 看到其使用

if (property != NULL) {
        rac_propertyAttributes *attributes = rac_copyPropertyAttributes(property);
        if (attributes != NULL) {
            @onExit {
                free(attributes);
            };

            BOOL isObject = attributes->objectClass != nil || strstr(attributes->type, @encode(id)) == attributes->type;
            BOOL isProtocol = attributes->objectClass == NSClassFromString(@"Protocol");
            BOOL isBlock = strcmp(attributes->type, @encode(void(^)())) == 0;
            BOOL isWeak = attributes->weak;

            shouldAddDeallocObserver = isObject && isWeak && !isBlock && !isProtocol;
        }
    }

可以看出 attributes 的设置和释放都在一起使得代码的可读性得到了提高。

attribute((overloadable)) 方法重载

能够在 c 的函数上实现方法重载。即同样的函数名函数能够对不同参数在编译时能够自动根据参数来选择定义的函数

__attribute__((overloadable)) void printArgument(int number){
    NSLog(@"Add Int %i", number);
}

__attribute__((overloadable)) void printArgument(NSString *number){
    NSLog(@"Add NSString %@", number);
}

__attribute__((overloadable)) void printArgument(NSNumber *number){
    NSLog(@"Add NSNumber %@", number);
}

attribute((objc_designated_initializer)) 指定内部实现的初始化方法

  • 如果是 objc_designated_initializer 初始化的方法必须调用覆盖实现 super 的 objc_designated_initializer 方法。
  • 如果不是 objc_designated_initializer 的初始化方法,但是该类有 objc_designated_initializer 的初始化方法,那么必须调用该类的 objc_designated_initializer 方法或者非 objc_designated_initializer 方法,而不能够调用 super 的任何初始化方法。

attribute((objc_subclassing_restricted)) 指定不能有子类

相当于 Java 里的 final 关键字,如果有子类继承就会出错。

attribute((objc_requires_super)) 子类继承必须调用 super

声明后子类在继承这个方法时必须要调用 super,否则会出现编译警告,这个可以定义一些必要执行的方法在 super 里提醒使用者这个方法的内容时必要的。

attribute((const)) 重复调用相同数值参数优化返回

用于数值类型参数的函数,多次调用相同的数值型参数,返回是相同的,只在第一次是需要进行运算,后面只返回第一次的结果,这时编译器的一种优化处理方式。

attribute((constructor(PRIORITY))) 和 attribute((destructor(PRIORITY)))

PRIORITY 是指执行的优先级,main 函数执行之前会执行 constructor,main 函数执行后会执行 destructor,+load 会比 constructor 执行的更早点,因为动态链接器加载 Mach-O 文件时会先加载每个类,需要 +load 调用,然后才会调用所有的 constructor 方法。

通过这个特性,可以做些比较好玩的事情,比如说类已经 load 完了,是不是可以在 constructor 中对想替换的类进行替换,而不用加在特定类的 +load 方法里。

Clang 警告处理

先看看这个

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
        sizeLabel = [self sizeWithFont:font constrainedToSize:size lineBreakMode:NSLineBreakByWordWrapping];
#pragma clang diagnostic pop

如果没有#pragma clang 这些定义,会报出 sizeWithFont 的方法会被废弃的警告,这个加上这个方法当然是为了兼容老系统,加上 ignored “-Wdeprecated-declarations” 的作用是忽略这个警告。通过 clang diagnostic push/pop 可以灵活的控制代码块的编译选项。

使用 libclang 来进行语法分析

使用 libclang 里面提供的方法对源文件进行语法分析,分析语法树,遍历语法数上每个节点。写个 python 脚本来调用 clang

pip install clang

#!/usr/bin/python
# vim: set fileencoding=utf-8

import clang.cindex
import asciitree
import sys

def node_children(node):
    return (c for c in node.get_children() if c.location.file == sys.argv[1])

def print_node(node):
    text = node.spelling or node.displayname
    kind = str(node.kind)[str(node.kind).index('.')+1:]
    return '{} {}'.format(kind, text)

if len(sys.argv) != 2:
    print("Usage: dump_ast.py [header file name]")
    sys.exit()

clang.cindex.Config.set_library_file('/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/libclang.dylib')
index = clang.cindex.Index.create()
translation_unit = index.parse(sys.argv[1], ['-x', 'objective-c'])

print asciitree.draw_tree(translation_unit.cursor,
                          lambda n: list(n.get_children()),
                          lambda n: "%s (%s)" % (n.spelling or n.displayname, str(n.kind).split(".")[1]))

基于语法树的分析还可以针对字符串做加密。

LibTooling 对语法树完全的控制

因为 LibTooling 能够完全控制语法树,那么可以做的事情就非常多了,比如检查命名是否规范,还能够进行语言的转换,比如把 OC 语言转成JS或者 Swift 。可以用这个 tools 自己写个工具去遍历。

#include "clang/Driver/Options.h"
#include "clang/AST/AST.h"
#include "clang/AST/ASTContext.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Frontend/ASTConsumers.h"
#include "clang/Frontend/FrontendActions.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Tooling/CommonOptionsParser.h"
#include "clang/Tooling/Tooling.h"
#include "clang/Rewrite/Core/Rewriter.h"

using namespace std;
using namespace clang;
using namespace clang::driver;
using namespace clang::tooling;
using namespace llvm;

Rewriter rewriter;
int numFunctions = 0;

static llvm::cl::OptionCategory StatSampleCategory("Stat Sample");

class ExampleVisitor : public RecursiveASTVisitor<ExampleVisitor> {
private:
    ASTContext *astContext; // used for getting additional AST info

public:
    explicit ExampleVisitor(CompilerInstance *CI) 
      : astContext(&(CI->getASTContext())) // initialize private members
    {
        rewriter.setSourceMgr(astContext->getSourceManager(), astContext->getLangOpts());
    }

    virtual bool VisitFunctionDecl(FunctionDecl *func) {
        numFunctions++;
        string funcName = func->getNameInfo().getName().getAsString();
        if (funcName == "do_math") {
            rewriter.ReplaceText(func->getLocation(), funcName.length(), "add5");
            errs() << "** Rewrote function def: " << funcName << "
";
        }    
        return true;
    }

    virtual bool VisitStmt(Stmt *st) {
        if (ReturnStmt *ret = dyn_cast<ReturnStmt>(st)) {
            rewriter.ReplaceText(ret->getRetValue()->getLocStart(), 6, "val");
            errs() << "** Rewrote ReturnStmt
";
        }        
        if (CallExpr *call = dyn_cast<CallExpr>(st)) {
            rewriter.ReplaceText(call->getLocStart(), 7, "add5");
            errs() << "** Rewrote function call
";
        }
        return true;
    }
};

class ExampleASTConsumer : public ASTConsumer {
private:
    ExampleVisitor *visitor; // doesn't have to be private

public:
    // override the constructor in order to pass CI
    explicit ExampleASTConsumer(CompilerInstance *CI)
        : visitor(new ExampleVisitor(CI)) // initialize the visitor
    { }

    // override this to call our ExampleVisitor on the entire source file
    virtual void HandleTranslationUnit(ASTContext &Context) {
        visitor->TraverseDecl(Context.getTranslationUnitDecl());
    }
};

class ExampleFrontendAction : public ASTFrontendAction {
public:
    virtual std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef file) {
         return llvm::make_unique<ExampleASTConsumer>(&CI); // pass CI pointer to ASTConsumer
    }
};

int main(int argc, const char **argv) {
    // parse the command-line args passed to your code
    CommonOptionsParser op(argc, argv, StatSampleCategory);        
    // create a new Clang Tool instance (a LibTooling environment)
    ClangTool Tool(op.getCompilations(), op.getSourcePathList());

    // run the Clang Tool, creating a new FrontendAction (explained below)
    int result = Tool.run(newFrontendActionFactory<ExampleFrontendAction>().get());

    errs() << "
Found " << numFunctions << " functions.

";
    // print out the rewritten source code ("rewriter" is a global var.)
    rewriter.getEditBuffer(rewriter.getSourceMgr().getMainFileID()).write(errs());
    return result;
}

ClangPlugin

通过自己写个插件,可以将这个插件添加到编译的流程中,对编译进行控制,可以在 LLVM 的这个目录下查看一些范例 llvm/tools/clang/tools

孙源主导的动态化方案 DynamicCocoa 中就是使用了一个将 OC 源码转 JS 的插件来进行代码的转换。

滴滴的王康在做瘦身时也实现了一个自定义的 clang 插件

编译后生成的二进制内容 Link Map File

在 Build Settings 里设置 Write Link Map File 为 Yes 后每次编译都会在指定目录生成这样一个文件。文件内容包含 Object files,Sections,Symbols。下面分别说说这些内容

Object files

这个部分的内容都是 .m 文件编译后的 .o 和需要 link 的 .a 文件。前面是文件编号,后面是文件路径。

Sections

这里描述的是每个 Section 在可执行文件中的位置和大小。每个 Section 的 Segment 的类型分为 __TEXT 代码段和 __DATA 数据段两种。

Symbols

Symbols 是对 Sections 进行了再划分。这里会描述所有的 methods,ivar 和字符串,及它们对应的地址,大小,文件编号信息。

每次编译后生成的 dSYM 文件

在每次编译后都会生成一个 dSYM 文件,程序在执行中通过地址来调用方法函数,而 dSYM 文件里存储了函数地址映射,这样调用栈里的地址可以通过 dSYM 这个映射表能够获得具体函数的位置。一般都会用来处理 crash 时获取到的调用栈 .crash 文件将其符号化。

可以通过 Xcode 进行符号化,将 .crash 文件,.dSYM 和 .app 文件放到同一个目录下,打开 Xcode 的 Window 菜单下的 organizer,再点击 Device tab,最后选中左边的 Device Logs。选择 import 将 .crash 文件导入就可以看到 crash 的详细 log 了。

还可以通过命令行工具 symbolicatecrash 来手动符号化 crash log。同样先将 .crash 文件,.dSYM 和 .app 文件放到同一个目录下,然后输入下面的命令

export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer
symbolicatecrash appName.crash appName.app > appName.log

Mach-O 文件

记录编译后的可执行文件,对象代码,共享库,动态加载代码和内存转储的文件格式。不同于 xml 这样的文件,它只是二进制字节流,里面有不同的包含元信息的数据块,比如字节顺序,cpu 类型,块大小等。文件内容是不可以修改的,因为在 .app 目录中有个 _CodeSignature 的目录,里面包含了程序代码的签名,这个签名的作用就是保证签名后 .app 里的文件,包括资源文件,Mach-O 文件都不能够更改。

Mach-O 文件包含三个区域

  • Mach-O Header:包含字节顺序,magic,cpu 类型,加载指令的数量等
  • Load Commands:包含很多内容的表,包括区域的位置,符号表,动态符号表等。每个加载指令包含一个元信息,比如指令类型,名称,在二进制中的位置等。
  • Data:最大的部分,包含了代码,数据,比如符号表,动态符号表等。

Mach-O 文件的解析

解析前先看看可以描述该文件的结构体

struct mach_header {
  uint32_t      magic;
  cpu_type_t    cputype;
  cpu_subtype_t cpusubtype;
  uint32_t      filetype;
  uint32_t      ncmds;
  uint32_t      sizeofcmds;
  uint32_t      flags;
};

struct segment_command {
  uint32_t  cmd;
  uint32_t  cmdsize;
  char      segname[16];
  uint32_t  vmaddr;
  uint32_t  vmsize;
  uint32_t  fileoff;
  uint32_t  filesize;
  vm_prot_t maxprot;
  vm_prot_t initprot;
  uint32_t  nsects;
  uint32_t  flags;
};

根据这个结构体,需要先取出 magic,然后根据偏移量取出其它的信息。遍历 ncmds 能够获得所有的 segment。cputype 包含了 CPU_TYPE_I386,CPU_TYPE_X86_64,CPU_TYPE_ARM,CPU_TYPE_ARM64 等多种 CPU 的类型。

dyld动态链接

生成可执行文件后就是在启动时进行动态链接了,进行符号和地址的绑定。首先会加载所依赖的 dylibs,修正地址偏移,因为 iOS 会用 ASLR 来做地址偏移避免攻击,确定 Non-Lazy Pointer 地址进行符号地址绑定,加载所有类,最后执行 load 方法和 clang attribute 的 constructor 修饰函数。

附:安装编译 LLVM

多种获取方式

svn co http://llvm.org/svn/llvm-project/llvm/trunk llvm
cd llvm/tools
svn co http://llvm.org/svn/llvm-project/cfe/trunk clang
cd ../projects
svn co http://llvm.org/svn/llvm-project/compiler-rt/trunk compiler-rt
cd ../tools/clang/tools
svn co http://llvm.org/svn/llvm-project/clang-tools-extra/trunk extra
  • git
git clone http://llvm.org/git/llvm.git
cd llvm/tools
git clone http://llvm.org/git/clang.git
cd ../projects
git clone http://llvm.org/git/compiler-rt.git
cd ../tools/clang/tools
git clone http://llvm.org/git/clang-tools-extra.git

安装

brew install cmake
mkdir build
cmake /path/to/llvm/source
cmake --build .

 

来自:http://www.starming.com/index.php?v=index&view=107

 

返回顶部