启动优化 - Clang的插桩之项目应用

1,232 阅读7分钟

LLVM的概念 使用CLang命令

LLVM是构架编译器的框架系统,C++编写,支持多种源语言或多种硬件架构。苹果提出LLVM代替了GCC。LLVM最重要的方面是通用的代码形式IR代码形式,做到了链接多种前端语言和生成多种后端语言的能力

截屏2021-09-02 下午3.19.58.png

编译流程

Clang是LLVM项目中的子项目,深入研究Clang对于研发人员带来很多好处。

clang -ccc-print-phases main.m

0: input, "main.m", objective-c\
1: preprocessor, {0}, objective-c-cpp-output 2: compiler, {1}, ir\
3: backend, {2}, assembler\
4: assembler, {3}, object\
5: linker, {4}, image\
6: bind-arch, "x86_64", {5}, image

输入文件:找到源文件
预处理阶段:这个过程处理包括宏的替换,头文件的导入
编译阶段:进行词法分析、语法分析、检测语法是否正确,最终生成`IR`
后端:这里`LLVM`会通过一个一个的`Pass(节点)`去优化,每个`Pass`做一些事情,最终生成汇编代码
生成目标文件
链接:链接需要的动态库和静态库,生成可执行文件
通过不同的架构,生成对应的可行文件

1.预处理阶段,通过预处理将头文件导入和宏进行了替换。

clang -E main.m >> proprocessor.txt

截屏2021-09-02 下午3.51.21.png

2.词法分析

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

词法分析,是写得每个代码切成一个个token,标注出横向纵向的位置。我的理解就是从像一次格式化一样,将代码规整好,并与下一步操作

#define C 30

typedef int HK_INT_64;

int test(int a,int b){
  '		Loc=<main.m:7:1>
typedef 'typedef'	 [StartOfLine]	Loc=<main.m:11:1>
int 'int'	 [LeadingSpace]	Loc=<main.m:11:9>
identifier 'HK_INT_64'	 [LeadingSpace]	Loc=<main.m:11:13>
semi ';'		Loc=<main.m:11:22>
int 'int'	 [StartOfLine]	Loc=<main.m:13:1>
identifier 'test'	 [LeadingSpace]	Loc=<main.m:13:5>
l_paren '('		Loc=<main.m:13:9>
int 'int'		Loc=<main.m:13:10>
identifier 'a'	 [LeadingSpace]	Loc=<main.m:13:14>
comma ','		Loc=<main.m:13:15>
int 'int'		Loc=<main.m:13:16>
identifier 'b'	 [LeadingSpace]	Loc=<main.m:13:20>
r_paren ')'		Loc=<main.m:13:21>
l_brace '{'		Loc=<main.m:13:22>
return 'return'	 [StartOfLine] [LeadingSpace]	Loc=<main.m:14:5>
identifier 'a'	 [LeadingSpace]	Loc=<main.m:14:12>
plus '+'	 [LeadingSpace]	Loc=<main.m:14:14>
identifier 'b'	 [LeadingSpace]	Loc=<main.m:14:16>
plus '+'	 [LeadingSpace]	Loc=<main.m:14:18>
numeric_constant '3'	 [LeadingSpace]	Loc=<main.m:14:20>
semi ';'		Loc=<main.m:14:21>
r_brace '}'	 [StartOfLine]	Loc=<main.m:15:1>
int 'int'	 [StartOfLine]	Loc=<main.m:18:1>
identifier 'main'	 [LeadingSpace]	Loc=<main.m:18:5>
l_paren '('		Loc=<main.m:18:9>
int 'int'		Loc=<main.m:18:10>
identifier 'argc'	 [LeadingSpace]	Loc=<main.m:18:14>
comma ','		Loc=<main.m:18:18>
const 'const'	 [LeadingSpace]	Loc=<main.m:18:20>
char 'char'	 [LeadingSpace]	Loc=<main.m:18:26>
star '*'	 [LeadingSpace]	Loc=<main.m:18:31>
identifier 'argv'	 [LeadingSpace]	Loc=<main.m:18:33>
l_square '['		Loc=<main.m:18:37>
r_square ']'		Loc=<main.m:18:38>
r_paren ')'		Loc=<main.m:18:39>
l_brace '{'	 [LeadingSpace]	Loc=<main.m:18:41>
int 'int'	 [StartOfLine] [LeadingSpace]	Loc=<main.m:19:5>
identifier 'a'	 [LeadingSpace]	Loc=<main.m:19:9>
equal '='	 [LeadingSpace]	Loc=<main.m:19:11>
identifier 'test'	 [LeadingSpace]	Loc=<main.m:19:13>
l_paren '('		Loc=<main.m:19:17>
numeric_constant '1'		Loc=<main.m:19:18>
comma ','		Loc=<main.m:19:19>
numeric_constant '30'		Loc=<main.m:19:20 <Spelling=main.m:9:11>>
r_paren ')'		Loc=<main.m:19:21>
semi ';'		Loc=<main.m:19:22>
identifier 'printf'	 [StartOfLine] [LeadingSpace]	Loc=<main.m:20:5>
l_paren '('		Loc=<main.m:20:11>
string_literal '"%d"'		Loc=<main.m:20:12>
comma ','		Loc=<main.m:20:16>
identifier 'a'		Loc=<main.m:20:17>
r_paren ')'		Loc=<main.m:20:18>
semi ';'		Loc=<main.m:20:19>
return 'return'	 [StartOfLine] [LeadingSpace]	Loc=<main.m:21:5>
numeric_constant '0'	 [LeadingSpace]	Loc=<main.m:21:12>
semi ';'		Loc=<main.m:21:13>
r_brace '}'	 [StartOfLine]	Loc=<main.m:22:1>
eof ''		Loc=<main.m:22:2>

3.语法分析

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

//UIKit找不到的时候,导入sdk进行分析
clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk -fmodules -fsyntax-only -Xclang -ast-dump main.m

语法分析是生成了抽象语法书(AST),判断语法结构是否正确。

截屏2021-09-02 下午11.05.17.png

4.生成中间代码IR

  clang -S -fobjc-arc -emit-llvm main.m
  //优化 -O0...-Os优化级别从小到大
  clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll

优化的IR文件代码行数更节俭,同时在xcode中也是有相应的配置

截屏2021-09-02 下午11.25.02.png

xCode7后苹果开启了Bitcode进一步优化,可以将IR文件.ll生成.bc文件,然后再生成汇编文件。对应不同的苹果架构。

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

有了.ll or .bc 就可以生成汇编文件

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

我们可以通过clang对ll/bc/m文件直接生成汇编文件,从汇编文件的行数比较,通过ll/bc生成的汇编文件比m文件生成的文件更加简洁。

下一步生成目标文件

clang -fmodules -c main.s -o main.o
nm -nm main.o
(undefined) external _printf
  0000000000000000 (__TEXT,__text) external _test
  000000000000000a (__TEXT,__text) external _main

解释:

截屏2021-09-03 下午5.52.57.png

最后一步就是生成可执行文件(链接),连接器把编译产生的o文件和.dylib .a文件生成一个mach-o文件

clang main.o -o 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
0000000100003f20 (__TEXT,__text) external _test
0000000100003f40 (__TEXT,__text) external _main
0000000100008008 (__DATA,__data) non-external __dyld_private

可以看到,printf来自libSystem。找到了相应的库,这就是完成的clang流程。

启动优化和虚拟内存

启动优化,以main函数之前pre-main和main函数之后。通过DYLD反馈pre-main的启动时间,以自己的项目为例,我们看一下分析:思路:不要有资源的浪费(动态库,类) 启动的时候 开启多线程

截屏2021-09-06 上午10.48.31.png

Total pre-main time: 1.1 seconds (100.0%)
         dylib loading time: 177.26 milliseconds (15.5%) 动态库载入 苹果建议不要大于6个动态库。苹果自己的动态库在共享缓存中,加载速度很快。涉及到动态库绑定。 
        rebase/binding time: 107.88 milliseconds (9.4%)  重定位 绑定 虚拟内存
            ObjC setup time: 158.31 milliseconds (13.9%)  OC类注册 读取二进制,映射表,全剧表。优化角度:优化废弃的类
           initializer time: 694.16 milliseconds (61.0%)  load 构造函数 初始化函数 优化角度:不要做延迟加载的东西
           slowest intializers :
             libSystem.B.dylib :   7.18 milliseconds (0.6%)
    libMainThreadChecker.dylib :  26.72 milliseconds (2.3%)
          libglInterpose.dylib : 326.65 milliseconds (28.7%)
                 BaiduTraceSDK :  95.95 milliseconds (8.4%)
                       NemoSDK :  73.64 milliseconds (6.4%)
           easydoctor_appstore : 321.61 milliseconds (28.2%)

含义解释: 编译时期 链接外部文件,告诉dyld我需要一个外部动态库,留一个位置等待启动运行时刻进行绑定。 虚拟内存:程序运行起来访问的内存空间是虚拟内存,虚拟内存和物理内存之前有一张映射关系,是有MMCU内存管理单元进行分配。 截屏2021-09-06 下午1.37.27.png

rebase 虚拟内存出现 ASLR(操作系统)让每次生产的虚拟的列表,生成一个随机值作为起始位置。生产mach-o的时候每块代码的地址都是生产好得,偏移地址。offset+ALSR 就是rebase的操作。 binging 内部符号要找到外部函数的时候(懒加载绑定)

二进制重排

当我们代码访问到没有载入物理内存的空间时,会出现pagefault。缺页中断。毫秒级别。如果出现大量的pagefault就会对用户有感知。 看一下项目中pagefault的情况,项目出现了1494次pagefault,用掉了299ms,个人感觉可能不太需要优化。 通过link map设置,我们可以看到函数实现的排列编译顺序,那些函数rebase到page中。

截屏2021-09-06 下午2.47.02.png 通过xcode link map来观察方法加载编译的方法。思路,我们把启动时候需要调用的方法都放到前面来。就可以在启动时刻减少pagefault的次数。、 截屏2021-09-06 下午2.52.50.png

截屏2021-09-06 下午3.47.53.png

截屏2021-09-06 下午3.40.20.png

按照order文件里面的符号顺序,对二机制进行排列.在项目的根目录中创建一个xxx.order的文件。然后在xcode中进行配置。可以看到link map文件发生了变化,main方法的内存地址是第一。

截屏2021-09-06 下午4.01.11.png

截屏2021-09-06 下午4.01.44.png

clang插桩

我们遇到的问题是我们不知道自己的项目启动时候函数调用的顺序,通过clang插桩的方式获取到函数调用的顺序,然后写入到order文件中LLVM文档地址

第一步在xcode中配置 other c flags中 -fsanitize-coverage=trace-pc-guard参数

截屏2021-09-06 下午4.21.53.png

第二步,将两个回调函数放在任意一个m文件中

#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>

void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
                                                    uint32_t *stop) {
  static uint64_t N;  // Counter for the guards.
  if (start == stop || *start) return;  // Initialize only once.
  printf("INIT: %p %p\n", start, stop);
  for (uint32_t *x = start; x < stop; x++)
    *x = ++N;  // Guards should start from 1.
}

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
  if (!*guard) return;

  void *PC = __builtin_return_address(0);
  SYNode * node = malloc(sizeof(SYNode));
  *node = (SYNode){PC,NULL};
  //结构体入栈
  OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
  char PcDescr[1024];
  __sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
  printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}

第三部我们通过一些方法生成order文件

- (void)writeOrderFile
{
    //定义数组
    NSMutableArray<NSString *> * symbleNames = [NSMutableArray array];
    
    while (YES) {//循环体内!进行了拦截!!
        SYNode * node = OSAtomicDequeue(&symbolList, offsetof(SYNode,next));
        
        if (node == NULL) {
            break;
        }
        
        Dl_info info;
        dladdr(node->pc, &info);
        NSString * name = @(info.dli_sname);//转字符串
        //给函数名称添加 _
//        if ([name hasPrefix:@"+["] || [name hasPrefix:@"-["]) {//OC方法 直接存
//            [symbleNames addObject:name];
//            continue;
//        }
//        [symbleNames addObject:[@"_" stringByAppendingString:name]];
        
        BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
        NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
        [symbleNames addObject:symbolName];
          
    }
    //反向遍历数组
//    symbleNames = (NSMutableArray<NSString *> *)[[symbleNames reverseObjectEnumerator] allObjects];
//    NSLog(@"%@",symbleNames);
    NSEnumerator * em = [symbleNames reverseObjectEnumerator];
    NSMutableArray * funcs = [NSMutableArray arrayWithCapacity:symbleNames.count];
    NSString * name;
    while (name = [em nextObject]) {
        if (![funcs containsObject:name]) {//数组没有name
            [funcs addObject:name];
        }
    }
    //去掉自己!
    [funcs removeObject:[NSString stringWithFormat:@"%s",__func__]];
    
    //写入文件
    //1.编程字符串
    NSString * funcStr = [funcs componentsJoinedByString:@"\n"];
    NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"ngaridoctor.order"];
    NSData * file = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
    
    [[NSFileManager defaultManager] createFileAtPath:filePath contents:file attributes:nil];
    
    NSLog(@"%@",funcStr);
}

然后再将order文件放到项目中,然后再看link map文件。然后通过system trace再看一下效果,发现好像作用不大,但是通过这种方式,有些函数是需要进行优化处理的。

截屏2021-09-06 下午10.42.04.png

完结!