我以前写了一个开源库TimeProfiler,监控全部的OC方法耗时。能够在开发App阶段,很方便的看到主线程全部OC方法的耗时。可是因为TimeProfiler是经过fishhook基于运行时hook,因此从原理上,它就有局限性:不能选择hook部分类的OC方法。这形成2个很难解决的问题:python
KKMagicHook经过静态插桩的方式来实现Hook,能够选择本身须要hook的模块。git
既然你们有这样的痛点,我就来想办法解决。网上有facebook方案:经过 llvm 插桩;手淘提到的汇编插桩。好吧,对于只能工做以外时间作这个事情,我暂时没有时间去作这个(但确实挺感兴趣的,后面时间容许,我研究完,也会分享出来)。而后看到这篇文章:静态拦截iOS对象方法调用的简易实现,大佬只是大体说了原理,可是网上并无找到任何关于它的实现。我只好本身动手,在作的过程当中,感受仍是挺复杂的(至少你要很是熟悉静态库和目标文件的结构。大佬说的简易,应该是相对于llvm 插桩跟汇编插桩来讲吧),有许多坑~ 因此也写这篇文章分享一下。程序员
我就不一行一行解读具体实现代码了,我挑遇到的坑跟核心逻辑说一下,而后你们结合代码KKMagicHook,就很容易理解了。github
脚本只处理arm64架构的静态库,若是静态库是fat file,包含多种架构。我是先从fat file中提取出arm64架构的静态库,交给脚本处理;处理完以后,在replace fat file中的arm64架构。安全
def deal_fat_file():
global staticLibPath, fatFilePath
fatFilePath = staticLibPath
(fatFileDir, fatFileName) = os.path.split(fatFilePath)
fatFileName = 'tmp-arm64-'+fatFileName
staticLibPath = os.path.join(fatFileDir, fatFileName)
# 提取出arm64架构的静态库
os.system('lipo ' + fatFilePath + ' -thin ' + 'arm64 -output '+ staticLibPath)
def replace_fat_file():
# replace fat file中的arm64架构
os.system('lipo '+fatFilePath+' -replace arm64 '+staticLibPath+' -output '+fatFilePath)
os.remove(staticLibPath)
复制代码
特别说明,处理后,只有arm64里的objc_msgSend方法被替换成了hook_msgSend,因此在arm64平台的设备上运行时候,都是调用hook_msgSend;而在其它架构平台,依然是调用objc_msgSend方法,对其它架构平台没有任何影响。bash
//静态库自己的符号表头跟目标文件头数据结构同样的
struct object_header {
char name[16]; /* 名称 */
char timestamp[12]; /* 生成的时间戳 */
char userid[6]; /* 用户id */
char groupid[6]; /* 组id */
uint64_t mode; /* 文件访问模式 */
uint64_t size; /* 目标文件的字节大小 */
uint32_t endheader; /* 头结束标志 */
char longname[0]; /* 目标文件名(不定长) */
};
复制代码
网上全部文章都说size是目标文件的字节大小,可是我在解析过程当中,发现咋算都对不上。最后看MachOView源码才知道,size表示目标文件的大小 + longname的大小。因此说只有longname长度为0时候,size才表示目标文件的大小。longname长度能够从name中获取,若是name是以"#1/"开头,那"#1/xx",xx就表示longname的长度。不然longname长度为0。数据结构
其实咱们过滤的是须要处理的目标文件,可是目标文件名就是类名(类名是ClassA,目标文件名就是ClassA.o),而且一个类在一个文件中。因此说咱们过滤须要处理的目标文件,就是过滤须要处理的类。架构
脚本中默认是替换静态库中全部类的objc_msgSend方法,当选择处理模式为:need_process_objFile,就只替换need_process_objFile集合里的类的objc_msgSend方法;当选择处理模式为:needless_process_objFile,表示除了needless_process_objFile集合里的类不替换,静态库中其他的类的objc_msgSend方法都替换。less
need_process_objFile = set() # set('xx1', 'xx2') 表示静态库中,仅xx1跟xx2须要处理
needless_process_objFile = set() # set('xx1', 'xx2') 表示静态库中,xx1跟xx2不须要处理,剩下的都须要处理
def process_object_file(name, location, size):
# 根据须要,下面三行中,只需打开一行,另外两行须要注释掉
process_mode = 'default' # 默认处理该静态库中的全部目标文件(类)
#process_mode = 'need_process_objFile' # 只处理need_process_objFile集合(上面的集合,须要赋值)中的类
#process_mode = 'needless_process_objFile' # 除了needless_process_objFile集合(上面的集合,须要赋值)中的类不处理,剩下的都须要处理
# 这里能够过滤不须要处理的目标文件,或者只选择须要处理的目标文件
# 默认处理该静态库中的全部目标文件
if process_mode == 'need_process_objFile':
if name in need_process_objFile:
find_symtab(location, size)
elif process_mode == 'needless_process_objFile':
if not name in need_process_objFile:
find_symtab(location, size)
else:
find_symtab(location, size)
复制代码
遍历目标文件的Load Commands,找到符号表,根据stroff算出location。函数
struct symtab_command {
uint32_t cmd; /* LC_SYMTAB */
uint32_t cmdsize; /* sizeof(struct symtab_command) */
uint32_t symoff; /* symbol table offset */
uint32_t nsyms; /* number of symbol table entries */
uint32_t stroff; /* string table offset */
uint32_t strsize; /* string table size in bytes */
};
复制代码
这块须要知道理论知识:
直接看我开源出来的代码,这块逻辑很好懂。可是我作这块时候,踩好多坑(反思了一下,主要是我不懂python),好比我不知道python不能在原文件中修改指定位置内容(确实查到能够经过os.system调用sed,而后回写等方式),可是静态库只能以二进制方式打开,而那些都是处理文本。
我本来是找到字符串表,而后decode成字符串,而后替换完成,再encode成二进制,可是这样会形成失真。缘由decode过程,\x00会被丢弃。最后发现二进制也能够替换😂。
def replace_Objc_MsgSend(fileLen):
pos = 0
bytes = b''
(loc, size) = symtabList_loc_size[0]
listIndex = 1
with open(staticLibPath, 'rb') as fileobj:
while pos < fileLen:
if pos == loc:
content = fileobj.read(size)
content = content.replace(b'\x00_objc_msgSend\x00', b'\x00_hook_msgSend\x00')
pos = pos + size
if listIndex < len(symtabList_loc_size):
(loc, size) = symtabList_loc_size[listIndex]
listIndex = 1 + listIndex
else:
step = 4
if loc > pos:
step = loc - pos
else:
step = fileLen - pos
content = fileobj.read(step)
pos = pos + step
bytes = bytes + content
with open(staticLibPath, 'wb+') as fileobj:
fileobj.write(bytes)
复制代码
.macro CALL_HOOK_BEFORE
BACKUP_REGISTERS
mov x2, lr
bl _hook_objc_msgSend_before
RESTORE_REGISTERS
.endmacro
.macro CALL_HOOK_AFTER
BACKUP_REGISTERS
bl _hook_objc_msgSend_after
mov lr, x0
RESTORE_REGISTERS
.endmacro
# hookObjcMsgSend.py里定义了函数名为hook_msgSend,若是修改脚本里的函数名,这里的函数名,也需跟脚本保持一致
ENTRY _hook_msgSend
CALL_HOOK_BEFORE
bl _objc_msgSend
CALL_HOOK_AFTER
ret
END_ENTRY _hook_msgSend
复制代码
这个汇编代码详细解说,请见我以前博客监控全部的OC方法耗时。惟独须要注意的是,汇编里的函数名,要跟hookObjcMsgSend.py里定义的函数名一致。
我以为KKMagicHook算是TimeProfiler的进阶版本,虽然能够实现TimeProfiler所有的功能,可是认为若是你要hook全部的OC方法,那为啥不用TimeProfiler,使用更简单。因此能用TimeProfiler就用TimeProfiler吧。
KKMagicHook应该更适用于,你想监控某个模块的OC方法耗时,你把这个模块编译成静态库,而后用KKMagicHook中的脚本处理一下,就能够了。例如项目中使用了TalkingData这个第三方库,咱们想监控/评估一下这个第三方库的性能问题,这个时候就不想监控项目中其它类了,以避免干扰分析。如图,很清晰显示TalkingData这个库全部OC方法的耗时:
这个库自己跟TimeProfiler同样,是可视化OC方法的耗时。可是毫不止于此,KKMagicHook的核心逻辑是静态插桩的方式来实现Hook Method,能够服务更广的场景。这个TimeProfiler和fishhook关系同样,TimeProfiler只能用来可视化方法耗时,可是fishhook能够服务更广的场景。
因此你们可使用KKMagicHook的核心逻辑,来服务本身项目许多方面。