一起养成写作习惯!这是我参与「掘金日新计划 · 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
结构体 - 打印符号名称
- 循环读取
循环引发的大坑
运行上述案例:
touchesBegan
方法出现死递归 在touchesBegan
方法中设置断点,运行项目,查看汇编代码
- 方法中被插入三次
__sanitizer_cov_trace_pc_guard
函数的调用 这就是循环引发的大坑,SanitizerCoverage
不但拦截方法、函数、Block
,还会对循环进行HOOK
案例中,while
循环被HOOK
,循环的执行会进入回调函数。回调函数中存入队列的还是touchesBegan
的函数地址,这会导致队列中永远存在一个到两个touchesBegan
,next
永远获取不完
解决办法:
在Build Setting
→Other C Flags
中,将配置修改为-fsanitize-coverage=func,trace-pc-guard
,对其增加func
参数
再次运行项目,点击屏幕,输出以下内容:
-[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...
选中案例App
,点击Downlad Container...
选择路径,下载.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(){
}
}
在ViewController
的load
方法和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:]
- 使用
OC
和Swift
混编,成功得到Swift
函数符号