iOS之Clang插庄的实践

92 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第11天,点击查看活动详情

日常开发中,我们经常会使用多线程开发。如果函数处于子线程,那__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传入类型,将下一个节点的地址返回给参数2
    OSAtomicEnqueue(&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结构体
    • 打印符号名称
循环引发的大坑

运行上述案例:

image-70.png

  • touchesBegan方法出现死递归 在touchesBegan方法中设置断点,运行项目,查看汇编代码

image-71.png

  • 方法中被插入三次__sanitizer_cov_trace_pc_guard函数的调用 这就是循环引发的大坑,SanitizerCoverage不但拦截方法、函数、Block,还会对循环进行HOOK

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

解决办法:

Build SettingOther C Flags中,将配置修改为-fsanitize-coverage=func,trace-pc-guard,对其增加func参数

image-72.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...

image-73.png 选中案例App,点击Downlad Container...

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

image-75.png.order文件拷贝到工程根目录,在Build SettingOrder File进行配置

image-76.pngBuild SettingsWrite Link Map File,设置为YES

编译项目,打开LinkMap文件

image-77.png

  • 配置生效,二进制重排成功
swift的函数符号

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

image-78.png

  • 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函数符号