通过fishhook拦截方法的局限性

我之前写了一个开源库TimeProfiler,监控所有的OC方法耗时。可以在开发App阶段,很方便的看到主线程所有OC方法的耗时。但是由于TimeProfiler是通过fishhook基于运行时hook,所以从原理上,它就有局限性:不能选择hook部分类的OC方法。这造成2个很难解决的问题:

不能选择hook一部分类的OC方法,全部hook会有性能问题,所以也不能线上使用。

个别同学反映,TimeProfiler hook某个类的方法,会crash。但是由于代码安全性,不能把代码给我看,因为这个类跟项目强相关,也不能造一个crash的demo给我排查问题。所以我只能盲猜哪里出问题,效率极低。而他们也会因为hook这个类crash,导致不能用到这么好的工具,多可惜~ 而KKMagicHook可以选择不hook这个类,不妨碍使用这个工具。

KKMagicHook通过静态插桩的方式来实现Hook,可以选择自己需要hook的模块。

既然大家有这样的痛点,我就来想办法解决。网上有facebook方案:通过 llvm 插桩;手淘提到的汇编插桩。好吧,对于只能工作之外时间做这个事情,我暂时没有时间去做这个(但确实挺感兴趣的,后面时间允许,我研究完,也会分享出来)。然后看到这篇文章:静态拦截iOS对象方法调用的简易实现,大佬只是大致说了原理,但是网上并没有找到任何关于它的实现。我只好自己动手,在做的过程中,感觉还是挺复杂的(至少你要非常熟悉静态库和目标文件的结构。大佬说的简易,应该是相对于llvm 插桩跟汇编插桩来说吧),有许多坑~ 所以也写这篇文章分享一下。

实现过程遇到的坑跟核心逻辑

我就不一行一行解读具体实现代码了,我挑遇到的坑跟核心逻辑说一下,然后大家结合代码KKMagicHook,就很容易理解了。

静态库是fat file

脚本只处理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方法,对其它架构平台没有任何影响。

目标文件头size的意义

//静态库本身的符号表头跟目标文件头数据结构一样的

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方法都替换。

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)

寻找字符串表的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 */

};

这块需要知道理论知识:

替换字符串表中的objc_msgSend

直接看我开源出来的代码,这块逻辑很好懂。但是我做这块时候,踩好多坑(反思了一下,主要是我不懂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)

_hook_msgSend的实现

.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的适用场景

我觉得KKMagicHook算是TimeProfiler的进阶版本,虽然可以实现TimeProfiler全部的功能,但是认为如果你要hook所有的OC方法,那为啥不用TimeProfiler,使用更简单。所以能用TimeProfiler就用TimeProfiler吧。

KKMagicHook应该更适用于,你想监控某个模块的OC方法耗时,你把这个模块编译成静态库,然后用KKMagicHook中的脚本处理一下,就可以了。例如项目中使用了TalkingData这个第三方库,我们想监控/评估一下这个第三方库的性能问题,这个时候就不想监控项目中其它类了,以免干扰分析。如图,很清晰显示TalkingData这个库所有OC方法的耗时:

KKMagicHook的意义

这个库本身跟TimeProfiler一样,是可视化OC方法的耗时。但是绝不止于此,KKMagicHook的核心逻辑是静态插桩的方式来实现Hook Method,可以服务更广的场景。这个TimeProfiler和fishhook关系一样,TimeProfiler只能用来可视化方法耗时,但是fishhook可以服务更广的场景。

所以大家可以使用KKMagicHook的核心逻辑,来服务自己项目许多方面。

源码

参考

c语言自动插桩,静态插桩的方式来实现Hook Method相关推荐

  1. c语言自动变量与静态变量,C语言的中的静态变量和局部变量(自动变量)

    #include int a=1; int f(int c) { static int a=2; c=c+1; return (a++)+c; } int main() { int i,k=0; fo ...

  2. linux静态插桩工具PEBIL

    文章目录 引言 论文学习 摘要及简介 设计与实现 插桩代码效率 实验结果及其他 具体使用 引言 PEBIL是San Diego Supercomputer Center某实验室研发的工具,用来对ELF ...

  3. 多重插补 均值插补_Feature Engineering Part-1均值/中位数插补。

    多重插补 均值插补 Understanding the Mean /Median Imputation and Implementation using feature-engine-.! 了解使用特 ...

  4. 【函数的定义、调用(嵌套调用、递归调用)、声明、函数的分类(有无返回值、有无参数)、变量(自动变量与静态变量、局部变量与全局变量、只读变量)】(学习笔记7--函数)

    第一篇博文,打卡新星计划第三季3.4~4.4,希望能有质的飞跃,顶峰相见 一.自定义函数 1.函数的定义 函数在使用前也需要定义,定义的格式如下: 数据类型 函数名([数据类型 参数1],[数据类型 ...

  5. C语言中变量的静态分配(Static)和动态分配(StackHeap)

    目录 C语言中变量的静态分配(Static)和动态分配(Stack&Heap) 变量的静态分配 包含了哪些变量? 全局变量和局部变量(staic关键字) 通过一个例子进行诠释 变量的动态分配 ...

  6. C语言自动预订飞机票问题

    C语言自动预订飞机票问题 2.自动预订飞机票问题(难度2) 设民航公司有一个自动预订飞机票的系统.该系统中有一张用单向链表表示的乘客表,下表中的结点按乘客姓氏的字母顺序相链.例如,下面是张某个时刻的乘 ...

  7. c语言自动转化,C语言编程之自动类型转化

    咱们在写程序的时候经常会遇到一些不好找的bug,有的并不是很难,只是大家容易忽略,今天咱们就来看一个,关于C语言自动类型转换的bug. 先看一段代码: void getNext(int * next, ...

  8. gocode+auto-complete搭建emacs的go语言自动补全功能

    上篇随笔记录了在emacs中使用go-mode和goflymake搭建了go语言的简单编程环境(推送门),今天来记录一下使用gocode+auto-complete配置emacs中go语言的自动补全功 ...

  9. 自动局部变量 与 静态局部变量 的区别与用途

    转自http://hi.baidu.com/jxq61/item/78353bec06149c0f570f1d8f 一 局部变量: 在函数体内声明的变量, 称为 局部变量. 二 自动局部变量与静态局部 ...

  10. cfg桩设备型号_试桩、试验桩、工程桩是一回事吗?

    本文以问答为线索,讲述CFG桩技术与造价的融合,以及商务合同签订的相关事项.内容适用于山东地区,其他可做参考. 第一篇 问 答 问:试桩.试验桩.工程桩是一回事吗? 答:不是一回事. 1.试桩:是为设 ...

最新文章

  1. 互联网圈都是什么人年薪百万?这份报告有真相
  2. 独家 | 一文解析统计学在机器学习中的重要性(附学习资源)
  3. java通过ip获取网卡MAC地址
  4. mysql 临时索引_MySQL select in 语句未使用索引,产生磁盘临时表,导致 crash
  5. 如何将结婚当作项目来管理
  6. SQL Server2005 日期字段与字符串比较的怪异问题
  7. 荣耀平板7可以用鸿蒙么,荣耀平板7定档3月23日发布,一屏可同时开启4个应用
  8. html页面能直接用vuex吗,vuex(多用于不同页面之间的数据共用和修改)
  9. 用javascript实现win7系统扫雷游戏
  10. W3school笔记——HTML
  11. 用计算机写作400字,关于电脑的作文400字
  12. IC卡,ID卡,M1卡等各种卡扫盲篇
  13. 洛谷 P3373 【模板】线段树 2 题解
  14. storyboard(故事版)新手教程 图文详解 4.把约束拉成属性 在代码文件里进行修改
  15. 一种永不止步的进取精神的勤奋
  16. 傻瓜式解决pycrypto安装错误
  17. MarkDown首行缩进和换行
  18. Cocos2d-x 游戏中子弹的设计 (一)
  19. 指定位置签到-百度地图
  20. 唯众本科物联网工程技术专业解决方案

热门文章

  1. Ureport2源码启动
  2. 计算机开机密码输入不了,win10开机密码输入不了,win10开机密码输入没反应
  3. 360手机助手电脑版 v2.4.0.1251 官方版
  4. java 面单模板_顺丰电子面单JSON请求格式
  5. 全球及中国卫星产业应用建设布局及投资机会分析报告2022-2028年版
  6. mysql emoji表情_mysql utf8mb4与emoji表情
  7. P问题、NP问题、NPC问题、NPC-hard问题
  8. 遥感原理与应用_专家报告 | 叶绿素荧光卫星遥感—原理与应用
  9. C语言(郝斌)内容整理
  10. 人脸识别相关数据集介绍