iOS底层原理探索 —– Clang插庄

资源准备

官方文档地址

简介

在项目中,对于OC方法,可以对objc_msgSend方法进行HOOK。这样仅适用于OC方法,对于C函数BlockSwift的方法/函数,都无法拦截

LLVM内置了一个简单的代码覆盖率检测工具(SanitizerCoverage)。它在函数级、基本块级和边缘级上插入对用户定义函数的调用,通过这种方式,可以顺利对OC方法、C函数BlockSwift的方法/函数进行全面HOOK

Clang 插庄

配置

搭建测试项目,在Build Setting –> Other C Flags中,增加-fsanitize-coverage=trace-pc-guard的配置

E05640B3-AAC6-450A-A260-DF9A511BB8AD.png

按照文档,在项目中加入示例代码:

#import "ViewController.h" 
#include <stdint.h> 
#include <stdio.h> 
#include <sanitizer/coverage_interface.h>

@implementation ViewController


- (void)viewDidLoad {
    [super viewDidLoad];
    
 }

void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop) { 
    static uint64_t N; 
    if (start == stop || *start) return; 
    
    printf("INIT: %p %p\n", start, stop); 
    for (uint32_t *x = start; x < stop; x++) 
        *x = ++N; 
}

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {

    if (!*guard) return;
    
    void *PC = __builtin_return_address(0); 
    
    char PcDescr[1024]; 
    
    // printf("guard: %p %x PC %s\n", guard, *guard, PcDescr); 
}

@end
复制代码

__sanitizer_cov_trace_pc_guard_init

运行项目,打印以下内容:

INIT: 0x100bbd4c0 0x100bbd4f8
复制代码
  • 打印来自__sanitizer_cov_trace_pc_guard_init函数;

  • 通过for代码中的循环,不难看出,从startstop的地址中,存储的是uint32_t类型的值;

  • 循环中xuint32_t指针类型,x++表示指针运算,步长+1会增加数据类型的长度;

  • uint32_t4字节,所以循环中的代码含义,每四字节记录一个++N的值。

使用lldb验证:

//读取start 
(lldb) x 0x100bbd4c0
0x100bbd4c0: 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 ................ 
0x100bbd4d0: 05 00 00 00 06 00 00 00 07 00 00 00 08 00 00 00 ................ 

//读取stop 
(lldb) x 0x100bbd4f8-4
0x100bbd4f4: 0e 00 00 00 c0 d2 e0 00 01 00 00 00 00 00 00 00 ................ 
0x100bbd504: 00 00 00 00 66 73 bb 00 01 00 00 00 00 00 00 00 ....fs..........
复制代码
  • 读取最后一个值,要在stop地址的基础上减去4字节

  • startstop,读出值为01~0e,这些值表示当前项目中方法/函数的符号个数。

__sanitizer_cov_trace_pc_guard

__sanitizer_cov_trace_pc_guard函数中设置断点,运行项目。来到断点,查看函数调用栈:

image.png

  • main函数调用。

继续执行程序,又会进入该函数的断点:

  • didFinishLaunchingWithOptions方法调用。

我们会发现一个现象,项目中每一个方法和函数的调用,都会触发__sanitizer_cov_trace_pc_guard的断点,并且由当前执行的方法/函数调用。

写入测试代码:

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) { 
    NSLog(@"__sanitizer_cov_trace_pc_guard");
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { 
    NSLog(@"touchesBegan方法执行"); 
    test(); 
}

void(^block)(void) = ^(void) { 
    NSLog(@"Block执行");
};

void test() { 
    NSLog(@"test函数执行");
    block();
}
复制代码

输出结果:

E149A95D-4C53-4DA3-8F44-F5C161970A04.png

  • 从运行结果来看,方法和函数全部被HOOK

  • 被拦截的方法和函数,仅限当前项目中的符号,例如:NSLog等外部符号不会被HOOK

  • 二进制重排的本意,就是将代码实现的二进制中方法/函数符号,在启动时刻按照顺序排列在前面。外部符号的方法/函数实现,并不在当前项目中,所以它们的符号也不在重排的范围之内。

原理

查看汇编代码:

150D74CD-DB29-4D66-AD2E-3B11880C147E.png

  • 在每一个方法和函数的汇编代码中,都多了一句bl指令,调用的正是__sanitizer_cov_trace_pc_guard函数;

  • Clang插庄的实现原理:只要添加Clang插庄的标记,编译器就会在当前项目中,在所有方法、函数、Block的代码实现的边缘,插入一句__sanitizer_cov_trace_pc_guard函数的调用代码,达到方法/函数/Block100%覆盖;

  • 相当于编译器在编译时期,修改了当前的二进制文件;

  • 修改时机,有可能是语法分析之后,生成IR中间代码时进行修改(未验证)。

获取符号名称

示例代码中,使用了一个__builtin_return_address函数:

83DA3536-AD9E-469E-9237-B4A0E2524B0A.png

  • 函数的作用,获取当前返回地址,也就是调用者的函数地址。

得到调用者的函数地址,获取符号名称:

#include <dlfcn.h> 

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) { 

    NSLog(@"__sanitizer_cov_trace_pc_guard"); 
    
    if (!*guard) return; 
    
    void *PC = __builtin_return_address(0);
    
    Dl_info info; 
    dladdr(PC, &info);
    
    NSLog(@"%s", info.dli_fname); 
    NSLog(@"%p", info.dli_fbase);
    NSLog(@"%s", info.dli_sname);
    NSLog(@"%p", info.dli_saddr); 
}
复制代码
  • 使用dladdr函数,将传入的函数地址,获取基本信息,存入Dl_info结构体。

Dl_info结构体的定义:

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;
复制代码
  • dli_fname:当前MachO路径
  • dli_fbase:当前MachO基地址
  • dli_sname:函数名称
  • dli_saddr:函数地址

运行项目,测试打印结果:

__sanitizer_cov_trace_pc_guard 
dli_fname:/private/var/containers/Bundle/Application/E4DBCC4F-B132-4462-A148-03B398B476F5/SanitizerCoverage.app/SanitizerCoverage 
dli_fbase:0x104cb0000 
dli_sname:-[ViewController touchesBegan:withEvent:] 
dli_saddr:0x104cb5a64
复制代码
  • 通过dli_sname可以得到函数名称。

修改测试代码,运行项目:

#import "ViewController.h"
#include <stdint.h> 
#include <stdio.h> 
#include <sanitizer/coverage_interface.h>
#include <dlfcn.h>

@interface ViewController ()

@end

@implementation ViewController

+ (void)load {
    // NSLog(@"load函数");
}

- (void)viewDidLoad {
    [super viewDidLoad];
    test();
}

void(^block)(void) = ^(void){ 
    // NSLog(@"Block执行"); 
}; 

void test(){ 
    // NSLog(@"test函数执行"); 
    block(); 
}

void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop) { 
    static uint64_t N; 
    if (start == stop || *start) return; 
    
    for (uint32_t *x = start; x < stop; x++) 
        *x = ++N; 
}

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    void *PC = __builtin_return_address(0);
    Dl_info info;
    dladdr(PC, &info);
    NSLog(@"%s", info.dli_sname);
}

@end
复制代码

打印结果:

+[ViewController load] 
main 
-[AppDelegate application:didFinishLaunchingWithOptions:] 
-[SceneDelegate window] 
-[SceneDelegate setWindow:] 
-[SceneDelegate window] 
-[SceneDelegate window]
-[SceneDelegate scene:willConnectToSession:options:] 
-[SceneDelegate window] 
-[SceneDelegate window] 
-[SceneDelegate window] 
-[ViewController viewDidLoad] 
test 
block_block_invoke 
-[SceneDelegate sceneWillEnterForeground:] 
-[SceneDelegate sceneDidBecomeActive:]
复制代码
  • 获取到启动时刻,所有被调用的方法、函数、Block的函数名称。其中部分函数多次调用,出现了重复符号,还需要对其排重。

实际应用

日常开发中,我们经常会使用多线程开发。如果函数处于子线程,那__sanitizer_cov_trace_pc_guard函数也会在子线程进行回调。

所以,当我们通过回调收集函数名称时,也要保证线程安全。

收集返回地址

以下案例,我们使用线程相对安全的原子队列进行返回地址的收集:

//定义原子队列 
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;

//定义结构体 
typedef struct { 
    void *pc; 
    void *next; 
} SYNode;

void __sanitizer_cov_trace_pc_guard(uint32_t *guard)  {

    void *PC = __builtin_return_address(0);
    
    //创建结构体
    SYNode *node = malloc(sizeof(SYNode));
    *node = (SYNode){PC, NULL};
    
    //结构体入栈
    //offsetof:参数1传入类型,将下一个节点的地址返回给参数
    2OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { 
    while (YES) {
        SYNode *node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next)); 
        
        //取空则停止循环 
        if(node == NULL){ 
            break; 
        } 
        
        Dl_info info; 
        dladdr(node->pc, &info); 
        NSLog(@"%s", info.dli_sname);
    }
}
复制代码
  • 定义:

    • 定义原子队列

    • 定义结构体,pc存储当前返回地址,next存储下一个节点地址

  • 收集

    • 创建结构体,对pc赋值,next设置为NULL

    • 结构体入栈

    • offsetof:宏,参数1传入类型,将下一个节点的地址返回给参数2

  • 测试

    • 循环读取node,取空则停止循环

    • 将返回地址写入Dl_info结构体

    • 打印符号名称

循环引发的大坑

运行上述案例:

  • touchesBegan方法出现死递归。

touchesBegan方法中设置断点,运行项目,查看汇编代码:

  • 方法中被插入三次__sanitizer_cov_trace_pc_guard函数的调用。

这就是循环引发的大坑,SanitizerCoverage不但拦截方法、函数、Block,还会对循环进行HOOK

案例中,while循环被HOOK,循环的执行会进入回调函数。回调函数中存入队列的还是touchesBegan的函数地址,这会导致队列中永远存在一个到两个touchesBegannext永远获取不完。

解决办法:
Build Setting –> Other C Flags中,将配置修改为-fsanitize-coverage=func,trace-pc-guard,对其增加func参数:

19D8300B-A06A-498B-816C-EAC65A206714.png

再次运行项目,点击屏幕,输出以下内容:

-[ViewController touchesBegan:withEvent:] 
-[SceneDelegate sceneDidBecomeActive:]
-[SceneDelegate sceneWillEnterForeground:] 
block_block_invoke
test 
-[ViewController viewDidLoad]
-[SceneDelegate window] 
-[SceneDelegate window]
-[SceneDelegate window]
-[SceneDelegate scene:willConnectToSession:options:]
-[SceneDelegate window] 
-[SceneDelegate window] 
-[SceneDelegate setWindow:]
-[SceneDelegate window] 
-[AppDelegate application:didFinishLaunchingWithOptions:] 
main
+[ViewController load]
复制代码
  • 修改配置项,仅拦截方法的调用,成功解决循环引发的大坑。

获取函数符号并排重

案例还要解决几个问题:

  • 过滤掉自身touchesBegan的函数名称;

  • 函数和Block的符号,需要在函数名称之前增加_

  • 相同的函数符号,需要进行排重;

  • 队列原则,先进后出。所以我们需要的符号顺序需要反转。

修改touchesBegan方法,解决遗留问题:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    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); 
        
        if([name isEqualToString:@(__func__)]){ 
            continue;
        } 
        
        if(![name hasPrefix:@"+["] && ![name hasPrefix:@"-["]) { 
            name = [@"_" stringByAppendingString:name]; 
        } 
        
        if([symbolNames containsObject:name]){ 
            continue; 
        } 
        
        [symbolNames addObject:name]; 
    } 
    
    symbolNames = (NSMutableArray<NSString *> *)[[symbolNames reverseObjectEnumerator] allObjects]; 
    
    for (NSString *symbol in symbolNames) {
        NSLog(@"%@", symbol); 
    } 
}
复制代码

打印结果:

+[ViewController load] 
_main
-[AppDelegate application:didFinishLaunchingWithOptions:] 
-[SceneDelegate setWindow:] 
-[SceneDelegate scene:willConnectToSession:options:] 
-[SceneDelegate window] 
-[ViewController viewDidLoad]
_test 
_block_block_invoke 
-[SceneDelegate sceneWillEnterForeground:] 
-[SceneDelegate sceneDidBecomeActive:]
复制代码
  • 过滤掉自身touchesBegan的函数名称;

  • 获取符号名称,如果不是+[-[开头,视为函数或Block,前面加_

  • 如果符合名称在数组中存在,跳过。否则,添加到数组;

  • 将数组反转,并循环打印。

写入文件并配置

修改touchesBegan方法,将符号列表写入.order文件:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { 
    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); 
        
        if([name isEqualToString:@(__func__)]) {
            continue; 
        } 
        
        if(![name hasPrefix:@"+["] && ![name hasPrefix:@"-["]) { 
            name = [@"_" stringByAppendingString:name]; 
        } 
        
        if([symbolNames containsObject:name]){
            continue; 
        } 
        
        [symbolNames addObject:name]; 
    } 
    
    symbolNames = (NSMutableArray<NSString *> *)[[symbolNames reverseObjectEnumerator] allObjects]; 
    
    NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"hk.order"]; 
    
    NSString *symbolStr = [symbolNames componentsJoinedByString:@"\n"];
    
    NSData *symbolData = [symbolStr dataUsingEncoding:kCFStringEncodingUTF8];
    
    [[NSFileManager defaultManager] createFileAtPath:filePath contents:symbolData attributes:nil];
    
    NSLog(@"%@", symbolStr); }
复制代码

拿到.order文件,选择Add Additional Simulators...

2D0FC4A0-1076-4463-9A05-A32A60BFF507.png

选中案例App,点击Downlad Container...,在弹出的弹框中,做下图操作:

79CC31FE-2553-4692-8EC3-FA0BA19929EA.png

选择路径,下载.xcappdata文件。右键显示包内容,在AppData/tmp目录下,找到.order文件:

.order文件拷贝到工程根目录,在Build Setting –> Order File进行配置:

Build Settings –> Write Link Map File,设置为YES

编译项目,打开LinkMap文件

  • 配置生效,二进制重排成功。

swift的函数符号

Other C Flags中的配置,仅对Clang编译器生效。而Swift使用swiftc编译器,要想获得swift函数符号,需要对Other Swift Flags进行配置:

  • Clang的配置参数略有出入;

  • 添加-sanitize-coverage=func-sanitize=undefined两项。

创建SwiftTest.swift文件,写入测试代码:

import Foundation class SwiftTest: NSObject { 
    @objc class func swiftTest1() { 
    
    } 
    
    @objc class func swiftTest2(){
    
    } 
}
复制代码

ViewControllerload方法和Block中分别调用:

+ (void)load { 
    [SwiftTest swiftTest1]; 
} 

- (void)viewDidLoad { 
    [super viewDidLoad]; 
    test(); 
} 

void(^block)(void) = ^(void){ 
    [SwiftTest swiftTest2]; 
}; 

void test(){
    block(); 
}
复制代码

运行项目,点击屏幕,输出以下内容:

+[ViewController load]
_$s17SanitizerCoverage9SwiftTestC10swiftTest1yyFZTo 
_$s17SanitizerCoverage9SwiftTestC10swiftTest1yyFZ 
_main 
-[AppDelegate application:didFinishLaunchingWithOptions:] 
-[SceneDelegate setWindow:] 
-[SceneDelegate scene:willConnectToSession:options:]
-[SceneDelegate window] 
-[ViewController viewDidLoad]
_test 
_block_block_invoke 
_$s17SanitizerCoverage9SwiftTestC10swiftTest2yyFZTo 
_$s17SanitizerCoverage9SwiftTestC10swiftTest2yyFZ 
-[SceneDelegate sceneWillEnterForeground:] 
-[SceneDelegate sceneDidBecomeActive:]
复制代码
  • 使用OCSwift混编,成功得到Swift函数符号.
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享