iOS-APP启动优化(二)

前言

iOS-APP启动优化(一)中,我们在文章中提到二进制重排的优化方式,对它的概念做了详细的介绍,同时我们也做了一些简单的应用,即通过编辑order file来实现二进制的重排,但是问题是我们如何将启动阶段的所有调用符号全覆盖到呐?通过我们这样一行行的手写,一定不是最佳实现方式,而且也不能做到真正的全覆盖。本片文章,我们就通过Clang 插桩 的方式来编写一个完整的,全覆盖的order file

Clang 插桩

Clang 插桩?这又是个什么牛逼的东西呐? 简单理解就是一个更加高级的Hook方式,比如我们通过Method swizling方式来实现运行时的hook,而Clang 插桩则是在编译器层面对二进制文件的操作,它会在所有的方法(包括OC,C/C++,Swift)边缘插入一些函数,当方法被调用时,插入的函数也会执行。我们通过实现这些函数,来达到更高级的Hook。 具体来看下官方文档:clang-llvm-SanitizerCoverage-tracing-pcs

LLVM has a simple code coverage instrumentation built in (SanitizerCoverage). It inserts calls to user-defined functions on function-, basic-block-, and edge- levels. Default implementations of those callbacks are provided and implement simple coverage reporting and visualization, however if you need just coverage visualization you may want to use SourceBasedCodeCoverage instead.

使用-fsanitize-coverage=trace-pc-guard编译器将在每个边缘插入以下代码:

__sanitizer_cov_trace_pc_guard(&guard_variable)

编译器还将插入对模块构造函数的调用:

// The guards are [start, stop).
// This function will be called at least once per DSO and may be called
// more than once with the same values of start/stop.
__sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop);

在每个间接调用中都会插入一个附加...=trace-pc,indirect-calls标志。__sanitizer_cov_trace_pc_indirect(void *callee)方法由开发者来实现。
这里我们直接使用文档中的示例代码,添加到我们项目中的任意的一个.m文件中,同时配置编译器添加-fsanitize-coverage=trace-pc-guard
1.配置编译器添加-fsanitize-coverage=trace-pc-guard
Target -> Build Settings -> Custom Compiler Flags

Custom Compiler Flags

此时Command + B 会报错:
Undefined symbol

这里跟文档说明是一致的,当配置完编译器后,需要开发者实现这2个函数。

  1. 实现2个插桩函数(这里我们的实现写在ViewController.m
@implementation ViewController

void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,uint32_t *stop){
    // Counter for the guards.
    static uint64_t N;
    // Initialize only once.
    if (start == stop || *start) return;
    printf("INIT: %p %p\n", start, stop);
    for (uint32_t *x = start; x < stop; x++)
    // Guards should start from 1.
    *x = ++N;
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
  if (!*guard) return;
  void *PC = __builtin_return_address(0);
  char PcDescr[1024];
  //__sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
  printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}

这段代码直接参考了文档中的示例代码,并做了一些简化。
__sanitizer_cov_trace_pc_guard_init () 该方法主要是统计了被插桩的符号数量,启动时会执行一次。当没添加一个方法时,数量就会加1。通过startstop读取数据:

INIT: 0x1047b2100 0x1047b21dc
(lldb) x/4gx 0x1047b2100
0x1047b2100: 0x0000000200000001 0x0000000400000003
0x1047b2110: 0x0000000600000005 0x0000000800000007
(lldb) x/4gx 0x1047b21dc-4
0x1047b21d8: 0x0000000000000037 0x0000000104aa4380
0x1047b21e8: 0x0000000000000000 0x00000001047af187

(这里的stop指针-4,是由于从startstop是一个完整的数据段,而stop指向的是数据端的末端,通过减4来读取数据段中最后一个元素)。这里是0x37即十进制55,当前统计到有55个符号数量。
如果这个方法只能获取到符号数量的话,对我们想要完善order File来说意义就不大了。
__sanitizer_cov_trace_pc_guard(): 当有方法调用时,会被该函数Hook;
比如这里我们通过添加一个touchesBegan:withEvent:来验证一下:

@implementation ViewController
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    if (!*guard) return;
    void *PC = __builtin_return_address(0);
    char PcDescr[1024];
    //  __sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
    printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self func1];
}
//C函数
void test1(){

}
//OC方法
- (void)func1
{
    block();
}
//Block
void (^block)(void) = ^{
    test1();
};

运行代码,触发touchesBegan:withEvent:,查看printf()信息:

guard: 0x10442a14c 1a PC \220\244\337\377
guard: 0x10442a154 1c PC �
guard: 0x10442a158 1d PC �
guard: 0x10442a150 1b PC \SB��

这里来了4次,对应的也就是:
a. touchesBegan:withEvent:
b. func1
c. block()
d. test1()
这也就是说,当触发调用,就会触发__sanitizer_cov_trace_pc_guard,而且oc方法,C函数,Block 都能被它hook住。通过点点看下test1()的汇编指令:

test()汇编

这也印证了文档里面所说的,当配置了-fsanitize-coverage=trace-pc-guard后,Clang会在所有的方法边缘插入(生成IR文件前)__sanitizer_cov_trace_pc_guard

那在 __sanitizer_cov_trace_pc_guard() 函数中,能否拿到我们想要的东西呐?
这里,我们要看一下函数内部的一个重要的方法:

void *PC = __builtin_return_address(0);

通过断点,来看下这个方法PC的输出,执行touchesBegan:withEvent::进入断点,输出PC ,同时看下目前的堆栈信息:

PC指针与调用堆栈

这也就是说,__builtin_return_address(0)返回了函数的调用地址, 比如

frame #3: 0x0000000102c392ec TestApp`-[ViewController touchesBegan:withEvent:](self=0x0000000104602870, _cmd="touchesBegan:withEvent:", touches=1 element, event=0x0000000281cd4140) at ViewController.m:48:5

那么,是否可以通过PC指针来拿到对应的符号信息呐?这里要介绍一个库:<dlfcn.h>, 这里面有个核心的方法:

 * Structure filled in by dladdr().
 */
typedef struct dl_info {
        const char      *dli_fname;     /* Pathname of shared object */
        void            *dli_fbase;     /* Base address of shared object */
        const char      *dli_sname;     /* Name of nearest symbol */
        void            *dli_saddr;     /* Address of nearest symbol */
} Dl_info;

extern int dladdr(const void *, Dl_info *);

通过PC指针来获取符号相关信息,这里返回给了一个Dl_info结构体。下面修改函数,来获取符号信息:

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    if (!*guard) return;
    void *PC = __builtin_return_address(0);
    /*
     const char      *dli_fname;
     void            *dli_fbase;
     const char      *dli_sname;
     void            *dli_saddr;
    */
    Dl_info info;
    dladdr(PC, &info); 
     printf("dli_fname:%s\ndli_fbase:%s\ndli_sname:%s\ndli_saddr:%s\n",info.dli_fname,info.dli_fbase,info.dli_sname,info.dli_saddr);
//    char PcDescr[1024];
    //  __sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
//    printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}

执行结果:

dli_fname:/private/var/containers/Bundle/Application/E40E41B2-7D51-494D-99A5-07AA98DED1AD/TestApp.app/TestApp
dli_fbase:\317\372\355\376
dli_sname:-[ViewController touchesBegan:withEvent:]
dli_saddr:\377C�\321\375{�\251\375��\221(
guard: 0x102bee154 1a PC 4\264�\227\201�<�\320�\200�
dli_fname:/private/var/containers/Bundle/Application/E40E41B2-7D51-494D-99A5-07AA98DED1AD/TestApp.app/TestApp
dli_fbase:\317\372\355\376
dli_sname:-[ViewController func1]
dli_saddr:\377\303
guard: 0x102bee15c 1c PC 
dli_fname:/private/var/containers/Bundle/Application/E40E41B2-7D51-494D-99A5-07AA98DED1AD/TestApp.app/TestApp
dli_fbase:\317\372\355\376
dli_sname:block_block_invoke
dli_saddr:\377\303
guard: 0x102bee160 1d PC 
dli_fname:/private/var/containers/Bundle/Application/E40E41B2-7D51-494D-99A5-07AA98DED1AD/TestApp.app/TestApp
dli_fbase:\317\372\355\376
dli_sname:test1
dli_saddr:\375{\277\251\375�
guard: 0x102bee158 1b PC �\223\276��

这里很明显,其实对我们有价值的信息是dli_sname

INIT: 0x102abe0e0 0x102abe1b8
dli_sname:main
dli_sname:-[AppDelegate application:didFinishLaunchingWithOptions:]
dli_sname:-[SceneDelegate window]
dli_sname:-[SceneDelegate setWindow:]
dli_sname:-[SceneDelegate window]
dli_sname:-[SceneDelegate window]
dli_sname:-[SceneDelegate scene:willConnectToSession:options:]
dli_sname:-[SceneDelegate window]
dli_sname:-[SceneDelegate window]
dli_sname:-[SceneDelegate window]
dli_sname:-[ViewController viewDidLoad]
dli_sname:-[ViewController func1]
dli_sname:block_block_invoke
dli_sname:test1
dli_sname:-[ViewController loadOtherObject]
dli_sname:-[TestObject toDoSomething]
dli_sname:-[SceneDelegate sceneWillEnterForeground:]
dli_sname:-[SceneDelegate sceneDidBecomeActive:]
dli_sname:-[SceneDelegate window]
dli_sname:-[SceneDelegate window]

到此,我们通过Clang插桩就能获取到启动时的所有符号信息了,剩下的就是将符号写入到Order File中去,写入的方法就非常多了,可以通过写文件的方式写入,也可以直接控制台输出复制粘贴都可以。需要注意的是,在写入前要对符号做一些简单的处理:

  1. 方法可能重复调用,这就意味着存在重复的符号,需要去重;
  2. order file写入格式要求,C函数需要在方法前添加下划线_;

接下来就举例演示一下(这并不是唯一方式)

编写完整Order File

这里先避开一个坑点:
while循环,for循环的执行会触发__sanitizer_cov_trace_pc_guard()的Hook,
这是我们不希望的,这里修改下Custom Compiler Flags的配置:
-fsanitize-coverage=trace-pc-guard修改为-fsanitize-coverage=func,trace-pc-guard,来屏蔽掉whilefor的干扰。

#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
#include <dlfcn.h>
#include <libkern/OSAtomic.h>

//符号结构体
typedef struct{
    void *pc;
    void *next;
}SymbolNode;
//原子队列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
//    if (!*guard) return; //该判断会屏蔽掉load方法
    void *PC = __builtin_return_address(0);
    //创建结构体
    SymbolNode *node = malloc(sizeof(SymbolNode));
    *node = (SymbolNode){PC,NULL};
    //加入AtomicQueue
    OSAtomicEnqueue(&symbolList, node, offsetof(SymbolNode, next));
}
//-fsanitize-coverage=trace-pc-guard
//-fsanitize-coverage=func,trace-pc-guard
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSMutableArray *nodes = [NSMutableArray array];
    while (YES) {
        SymbolNode *node = OSAtomicDequeue(&symbolList, offsetof(SymbolNode, next));
        if(node == NULL) break;
        Dl_info info = {0};
        dladdr(node->pc, &info);
        NSString *sName = @(info.dli_sname);
        BOOL isOC = ([sName hasPrefix:@"+["] || [sName hasPrefix:@"-["]) ? YES : NO;
        if(isOC){
            if(![nodes containsObject:sName]){
                [nodes addObject:sName];
            }
            continue;
        }
        sName = [@"_" stringByAppendingString:sName];
        if(![nodes containsObject:sName]){
            [nodes addObject:sName];
        }
    }
    NSMutableArray *newNodes = (NSMutableArray *)[[nodes reverseObjectEnumerator]allObjects];
    NSString *nodesStr =  [newNodes componentsJoinedByString:@"\n"];
    NSLog(@"nodesStr == \n%@",nodesStr);
    NSData *strData = [nodesStr dataUsingEncoding:NSUTF8StringEncoding];
    [[NSFileManager defaultManager]createFileAtPath:[NSString stringWithFormat:@"%@app.order",NSTemporaryDirectory()] contents:strData attributes:nil];
}
总结

一切的未知,都只是我们认知范围内的未知,其问题本身都是有答案的;而我们要做的就是在有限的时间里,找到它,让我们认知中的未知,变成已知。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 175,490评论 5 419
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 74,060评论 2 335
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 124,407评论 0 291
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 47,741评论 0 248
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 56,543评论 3 329
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 43,040评论 1 246
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 34,107评论 3 358
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 32,646评论 0 229
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 36,694评论 1 271
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 32,398评论 2 279
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 33,987评论 1 288
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 30,097评论 3 285
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 35,298评论 3 282
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 27,278评论 0 14
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 28,413评论 1 232
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 38,397评论 2 309
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 38,099评论 2 314

推荐阅读更多精彩内容