JHHK

欢迎来到我的个人网站
行者常至 为者常成

启动优化(二)

目录

Pagefault调试

程序在启动的时候首先会载入内存,这个时候会发生Pagefault(缺页异常),我们怎么来看下发生缺页的次数呢?

使用xcode提供的 Instruments –> system trace 工具来看下程序的冷启动和热启动发生的缺页次数。

一、冷启动的缺页次数查看

将APP杀掉,启动6-9个其它APP后,点击system trace的start按钮 启动要调试的APP,出现第一个界面后点击stop

img

可以看到程序在冷启动时发生缺页的次数是 1349,用时 353.17ms

二、热启动缺页次数查看

直接点击system trace的start按钮 启动要调试的APP,出现第一个界面后点击stop

img

可以看到程序在热启动时发生缺页的次数是 53,用时 18.61ms

LinkMap

在xcode工程中配置linkmap

img

根据路径找到bindingDemo-LinkMap-normal-arm64.txt文件,看下文件的内容

# Path: /Users/LKLC/Library/Developer/Xcode/DerivedData/Build/Products/Debug-iphoneos/bindingDemo.app/bindingDemo
# Arch: arm64
# Object files:
[  0] linker synthesized
[  1] /Users/LKLC/Library/Developer/Xcode/DerivedData/Build/Intermediates.noindex/bindingDemo.build/Debug-iphoneos/bindingDemo.build/Objects-normal/arm64/AppDelegate.o
[  2] /Users/LKLC/Library/Developer/Xcode/DerivedData/Build/Intermediates.noindex/bindingDemo.build/Debug-iphoneos/bindingDemo.build/Objects-normal/arm64/ViewController.o
[  3] /Users/LKLC/Library/Developer/Xcode/DerivedData/Build/Intermediates.noindex/bindingDemo.build/Debug-iphoneos/bindingDemo.build/Objects-normal/arm64/main.o
[  4] /Users/LKLC/Library/Developer/Xcode/DerivedData/Build/Intermediates.noindex/bindingDemo.build/Debug-iphoneos/bindingDemo.build/Objects-normal/arm64/SceneDelegate.o
[  5] /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.2.sdk/System/Library/Frameworks//Foundation.framework/Foundation.tbd
[  6] /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.2.sdk/usr/lib/libobjc.tbd
[  7] /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.2.sdk/System/Library/Frameworks//UIKit.framework/UIKit.tbd
# Sections:
# Address	Size    	Segment	Section
0x100005E88	0x00000684	__TEXT	__text
0x10000650C	0x00000090	__TEXT	__stubs
0x10000659C	0x000000A8	__TEXT	__stub_helper
0x100006644	0x000000C5	__TEXT	__cstring
0x100006709	0x00000D46	__TEXT	__objc_methname
0x10000744F	0x00000070	__TEXT	__objc_classname
0x1000074BF	0x00000AE5	__TEXT	__objc_methtype
0x100007FA4	0x0000005C	__TEXT	__unwind_info
0x100008000	0x00000008	__DATA	__got
0x100008008	0x00000060	__DATA	__la_symbol_ptr
0x100008068	0x00000040	__DATA	__cfstring
0x1000080A8	0x00000018	__DATA	__objc_classlist
0x1000080C0	0x00000010	__DATA	__objc_nlclslist
0x1000080D0	0x00000020	__DATA	__objc_protolist
0x1000080F0	0x00000008	__DATA	__objc_imageinfo
0x1000080F8	0x00001390	__DATA	__objc_const
0x100009488	0x00000038	__DATA	__objc_selrefs
0x1000094C0	0x00000010	__DATA	__objc_classrefs
0x1000094D0	0x00000008	__DATA	__objc_superrefs
0x1000094D8	0x00000004	__DATA	__objc_ivar
0x1000094E0	0x000000F0	__DATA	__objc_data
0x1000095D0	0x00000188	__DATA	__data
# Symbols:
# Address	Size    	File  Name
0x100005E88	0x0000003C	[  1] +[AppDelegate load]
0x100005EC4	0x0000006C	[  1] -[AppDelegate application:didFinishLaunchingWithOptions:]
0x100005F30	0x00000114	[  1] -[AppDelegate application:configurationForConnectingSceneSession:options:]
0x100006044	0x00000064	[  1] -[AppDelegate application:didDiscardSceneSessions:]
0x1000060A8	0x00000064	[  2] -[ViewController viewDidLoad]
0x10000610C	0x00000070	[  2] -[ViewController viewDidAppear:]
0x10000617C	0x0000003C	[  2] +[ViewController load]
0x1000061B8	0x00000014	[  2] -[ViewController test1]
0x1000061CC	0x00000014	[  2] -[ViewController test2]
0x1000061E0	0x000000A8	[  3] _main
0x100006288	0x00000088	[  4] -[SceneDelegate scene:willConnectToSession:options:]
0x100006310	0x00000040	[  4] -[SceneDelegate sceneDidDisconnect:]
0x100006350	0x00000040	[  4] -[SceneDelegate sceneDidBecomeActive:]
0x100006390	0x00000040	[  4] -[SceneDelegate sceneWillResignActive:]
0x1000063D0	0x00000040	[  4] -[SceneDelegate sceneWillEnterForeground:]
0x100006410	0x00000040	[  4] -[SceneDelegate sceneDidEnterBackground:]
0x100006450	0x0000002C	[  4] -[SceneDelegate window]
0x10000647C	0x0000004C	[  4] -[SceneDelegate setWindow:]
0x1000064C8	0x00000044	[  4] -[SceneDelegate .cxx_destruct]
0x10000650C	0x0000000C	[  5] _NSLog

Object files:是文件的编译顺序,也是链接顺序

Sections:MachO文件的代码段和数据段。代码段只读起始地址的偏移是5E88,数据段可读可写。

#Symbols: 具体的代码在MachO中的偏移地址,可以看到 +[AppDelegate load]的偏移地址也是5E88,证明代码段的第一个方法就是AppDelegate的load方法。注意这并不是方法的调用顺序,是方法在MachO文件中的位置顺序

+[AppDelegate load]打个断点

img

通过image list指令看先Macho的起始地址

(lldb) image list
[  0] D18EE100-DAB2-301D-8C85-640C258DC84F 0x0000000102bb4000 /Users/LKLC/Library/Developer/Xcode/DerivedData/Build/Products/Debug-iphoneos/bindingDemo.app/bindingDemo 

计算下load函数的地址

(lldb) p/x 0x0000000102bb4000 + 0x5E88
(long) $0 = 0x0000000102bb9e88
(lldb) dis -s 0x0000000102bb9e88
bindingDemo`+[AppDelegate load]:
    0x102bb9e88 <+0>:  sub    sp, sp, #0x30             ; =0x30 
    0x102bb9e8c <+4>:  stp    x29, x30, [sp, #0x20]
    0x102bb9e90 <+8>:  add    x29, sp, #0x20            ; =0x20 
    0x102bb9e94 <+12>: stur   x0, [x29, #-0x8]
    0x102bb9e98 <+16>: str    x1, [sp, #0x10]
->  0x102bb9e9c <+20>: adrp   x0, 1
    0x102bb9ea0 <+24>: add    x0, x0, #0x64e            ; =0x64e 
    0x102bb9ea4 <+28>: mov    x1, sp

可以看到计算出来的地址跟截图里load函数的地址都是0x0000000102bb9e88

xxx-LinkMap-normal-arm64文件的#Symbols部分决定了代码在Macho文件中的偏移,由于内存加载是分页加载的。试想一下假如我们的程序有5000页数据,启动所需调用的函数分布在第10、100、300、500、2000页的内存内,那么我们启动程序就要把10、100、300、500、2000这5页的内存全部载入(即使只用到了某一页的很少部分数据也要把整个页表都载入)。假如我们通过技术手段把这5页内启动所需的代码提取出来放在第1页内存,那么我们启动程序就只需要载入第1页内存数据就可以了,这样就大大减少了pagefault的发生,节约了启动时间,这就是二进制重排的思路。

二进制重排

一、如何进行重排

如果我们不进行配置,那么文件的编译顺序就是符号的排列顺序,相应的方法或函数就会被分配在前几页如下:

# Address	Size    	File  Name
0x100005E88	0x0000003C	[  1] +[AppDelegate load]
0x100005EC4	0x0000006C	[  1] -[AppDelegate application:didFinishLaunchingWithOptions:]
0x100005F30	0x00000114	[  1] -[AppDelegate application:configurationForConnectingSceneSession:options:]
0x100006044	0x00000064	[  1] -[AppDelegate application:didDiscardSceneSessions:]
0x1000060A8	0x00000064	[  2] -[ViewController viewDidLoad]
0x10000610C	0x00000070	[  2] -[ViewController viewDidAppear:]
0x10000617C	0x0000003C	[  2] +[ViewController load]
0x1000061B8	0x00000014	[  2] -[ViewController test1]
0x1000061CC	0x00000014	[  2] -[ViewController test2]
0x1000061E0	0x000000A8	[  3] _main

进行自定义配置

我们在工程的根目录下创建一个binding.order的文件,在文件内添加如下内容

img

_main
+[ViewController load]
+[AppDelegate load]
-[LCTest test]

配置Order file

img

然后编译工程,再找到bindingDemo-LinkMap-normal-arm64.txt文件

# Address	Size    	File  Name
0x100005E88	0x000000A8	[  3] _main
0x100005F30	0x0000003C	[  2] +[ViewController load]
0x100005F6C	0x0000003C	[  1] +[AppDelegate load]
0x100005FA8	0x0000006C	[  1] -[AppDelegate application:didFinishLaunchingWithOptions:]
0x100006014	0x00000114	[  1] -[AppDelegate application:configurationForConnectingSceneSession:options:]
0x100006128	0x00000064	[  1] -[AppDelegate application:didDiscardSceneSessions:]
0x10000618C	0x00000064	[  2] -[ViewController viewDidLoad]
0x1000061F0	0x00000070	[  2] -[ViewController viewDidAppear:]
0x100006260	0x00000014	[  2] -[ViewController test1]
0x100006274	0x00000014	[  2] -[ViewController test2]

我们看到前三个方法变成了我们在binding.order文件内配置的方法。

因为 -[LCTest test] 方法并不存在,编译系统还帮我们自动清除了。

以上这就是一次二进制重拍的操作,操作很简单,难的是:

如何找到启动调用的所有方法?

这些方法在MachO中如何排列?

二、如何hook所有符号

1、通过FishHook,hook掉objc_msgsend方法,但这样是有遗漏的,不能保证拿到所有的符号。FishHook的使用自行搜索吧。

2、Clang插庄

Clang插庄

一、先来看看如何配置clang插庄

OC配置:-fsanitize-coverage=func,trace-pc-guard

img

swift配置:-sanitize-coverage=func -sanitize=undefined

img

在任意一个类文件内添加如下方法:

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);
        
    printf("pc:%p\n",PC);

    char PcDescr[1024];    
    printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}

我们在bindingDemo工程中的ViewController.m文件添加了上述两个方法,并实现touchesBegan方法

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    
}

打一个符号断点-[ViewController touchesBegan:withEvent:],查看汇编代码

img

我们看到在函数完成栈平衡的操作后,立马就调用了方法

__sanitizer_cov_trace_pc_guard

也就是说每一个方法被调用时都会在其内部调用一次我们实现的方法__sanitizer_cov_trace_pc_guard,这就为我们拿到所有的调用符号提供了基础。

我们在打一个符号断点 __sanitizer_cov_trace_pc_guard 看下汇编代码

img

看下x30寄存器(函数调用返回的跳转地址存在这个寄存器)的值

(lldb) register read x30
      lr = 0x0000000100399bdc  bindingDemo`-[ViewController touchesBegan:withEvent:] + 44 at ViewController.m:48

看下控制台输出的pc的值

pc:0x100399bdc

在看看touchesBegan汇编代码截图内绿色框框内的地址 0x100399bdc

也就是说在函数调用时,当栈平衡完成后,立马插入了一个函数调用__sanitizer_cov_trace_pc_guard,当该插入的函数调用结束时就会跳回到原有函数的执行 指令0x100399bdc处接着向下执行,这就是Clang插庄的原理。

另外在我们实现的 __sanitizer_cov_trace_pc_guard 函数内通过代码

void *PC = __builtin_return_address(0);

获取到的pc值也同样是下条指令调用地址0x100399bdc,这就为我们找到具体的符号提供了基础。

引用头文件 #import <dlfcn.h>__sanitizer_cov_trace_pc_guard 内添加代码

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    if (!*guard) return;
    
    void *PC = __builtin_return_address(0);
    printf("pc:%p\n",PC);
    char PcDescr[1024];
    
    Dl_info info;
    dladdr(PC, &info);
    
    NSLog(@"dli_fname:%s",info.dli_fname);
    NSLog(@"dli_fbase:%p",info.dli_fbase);
    NSLog(@"dli_sname:%s",info.dli_sname);
    NSLog(@"dli_saddr:%p",info.dli_saddr);
    printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}

看下控制台输出:

dli_fname:/private/var/containers/Bundle/Application/3AE65342-D442-4D87-A845-92E96057A18E/bindingDemo.app/bindingDemo
dli_fbase:0x100390000
dli_sname:-[ViewController touchesBegan:withEvent:] //符号
dli_saddr:0x100399bb0                               //符号起始地址

dli_sname:被调用符号

dli_saddr:被调用符号的起始指令地址

至此就完成了hook所有符号的工作。

二、生成order文件

引入头文件 #import <libkern/OSAtomic.h>

//原子队列
static  OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//定义符号结构体
typedef struct {
    void *pc;
    void *next;
}SYNode;


void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    if (!*guard) return;
    
    void *PC = __builtin_return_address(0);
    printf("pc:%p\n",PC);
    char PcDescr[1024];
    
    SYNode *node = malloc(sizeof(SYNode));
    *node = (SYNode){PC,NULL};
    //进入
    OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));

    
    Dl_info info;
    dladdr(PC, &info);
    
    NSLog(@"dli_fname:%s",info.dli_fname);
    NSLog(@"dli_fbase:%p",info.dli_fbase);
    NSLog(@"dli_sname:%s",info.dli_sname);
    NSLog(@"dli_saddr:%p",info.dli_saddr);
    printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}

-(void)generateOrderFile{
    NSMutableArray <NSString *> * symbolNames = [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);
        BOOL  isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
        NSString * symbolName = isObjc ? name: [@"_" stringByAppendingString:name];
        [symbolNames addObject:symbolName];
    }
    //取反
    NSEnumerator * emt = [symbolNames reverseObjectEnumerator];
    //去重
    NSMutableArray<NSString *> *funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
    NSString * name;
    while (name = [emt nextObject]) {
        if (![funcs containsObject:name]) {
            [funcs addObject:name];
        }
    }
    //干掉自己!
    [funcs removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
    //将数组变成字符串
    NSString * funcStr = [funcs  componentsJoinedByString:@"\n"];
    
    NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"binding.order"];
    NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
    [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
    NSLog(@"%@",funcStr);
}

程序启动后在合适的时机调用generateOrderFile方法,比如在第一个页面的touchesBegan方法内

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self generateOrderFile];
}

取出binding.order文件,按照上面的二进制重拍配置binding.order。

配置完成后Clang插庄的配置和代码就可以删掉了


行者常至,为者常成!





R
Valine - A simple comment system based on Leancloud.