Mach-O简介及实际应用

 

一、前言

    在正题开始之前,我们先来聊聊iOS中的hook技术。一谈到hook,很多人首先想到的是runtime,runtime确实强大,但是它存在很多局限性:

1)、侵入性:一旦hook了某个类的方法,那么只能这个类的所有对象的方法都会被hook。

2)、语言上的局限性:runtime 的hook 只能作用于OC方法。

    开源框架Aspects很巧妙的解决了第一个问题,Aspects通过动态创建子类的方式将对当前类的hook转换为对当前类动态生成的子类的hook,以此避免对当前类其他对象的代码侵入,这与KVO的实现思路是一致的。而fishhook能从一定程度上辅助runtime解决hook对语言局限性的问题。

二、浅谈fishhook

    fishhook是Facebook开源的一个C语言的hook工具,我们可以使用fishhook来hook动态链接的C函数。为什么在这里要强调动态链接呢?因为fishhook只能hook iOS系统的C函数,你自己编写的C函数是无法hook的。

    fishhook使用起来很简单,在这里就不谈了,先来简单介绍下fishhook的实现原理。由于动态库并不参与前期的静态编译链接,所以在程序的可执行文件中,代码段并不包含动态库相关函数的汇编后的指令。那么系统是如何根据函数的调用符号找到真实的函数地址呢?在Mach-O文件中存在符号表和动态符号表以及字符串表,字符串表中存储了所有的字符信息,比如代码int a = 100;这个变量a的名字即存在字符串表中。符号表则存储了所有符号位于字符串表中的位置信息,动态符号表存储了动态库符号位于符号表中的偏移信息。动态库的section中的reserved1存储了该section的偏移量X,动态符号表偏移X后即是该section的符号表索引数组Indices的首地址,以Indices数组中的值为索引,可以在符号表中获取到当前的符号在字符串表中的偏移,从而获取到符号字符串。通过该section的addr字段可以获取到该section的符号绑定表,表中记录着动态符号如:printf所对应的函数地址,修改符号绑定表的内容为指定函数地址即实现了hook。fishhook看起来非常的绕,这是由于动态链接存在复杂的索引关系,在这里就不过多介绍了,有兴趣的可以搜索下有关fishhook的博文,优秀博文非常多。

    fishhook很厉害,但是在刚接触时我有两个疑问:1、fishhook能hook C++函数吗?在我的前篇文章中也提出了hook C++函数的问题,但是在留言中貌似没有得到有效的答案。2、fishhook 为什么不能hook自己写的C函数呢?下面我们来一一解答这两个问题。

三、C++的符号修饰

在程序员还是使用纸带写代码的时候,人们约定在指定的某几位代表指令,不同的0、1组合代表不同的指令。如:01000000中,0100代表跳转指令,后面的0000代表目标地址。由于汇编的出现,0100被用jump来代替,这就是最早的符号,符号表能映根据符号映射到一个指令。C语言也是与此类似,实际上我们也是通过一个符号来代表一个函数的地址,但是随着程序的不断变大,符号冲突的概率逐渐增加。一个程序员在一个.c文件实现了hello函数,可能另一个程序员在另一个.c文件中也实现了一个同名的hello函数,在这两个文件进行编译和汇编后,会在各自的目标文件中形成同名的强符号,导致最终链接时报错。这是由于在C语言中,函数和初始化后的全局变量默认都是强符号,如果你想改为弱符号,那么可以使用__attribute__((weak))修饰。在这里提一下最近58客户端发现的一个有意思的事情。在iOS 8.11.1版本以后,我们发现buggly上崩溃日志都会携带一个来自RN的函数调用栈RCTFBQuickPerformanceLoggerConfigureHooks,在RN中它的声明如下,


 

但是在源码中,这个函数没有任何实现,完全是一个空函数。看名字这个函数是hook使用的,那么它是怎么实现hook的呢?将RCT__EXTERN 展开后为__attribute__((visibility("default"))),其作用为将RCTFBQuickPerformanceLoggerConfigureHooks向外界暴露,如果外界存在同名函数,那么RCTFBQuickPerformanceLoggerConfigureHooks会报符号冲突的错误。那么如何做到即能暴露符号,又不造成符号冲突呢?这就利用了__attribute__((weak)),将RCTFBQuickPerformanceLoggerConfigureHooks生命为弱符号,当外界有同名函数时,SDK内部调用外届的函数,否则调用内部空函数。

    为了防止出现函数名冲突,在UNIX的C环境下,所有的函数会被加上”_”前缀,也就是说void hello ( ),符号实际上为”_hello”,这种机制能够避免与系统函数的冲突。C++为了解决符号冲突的问题,表现的更为彻底。与C相比,C++有命名空间的限制,可以极大地避免函数的冲突,除了命名空间外,C++还存在构造和析构函数,函数重载等特征。这就导致C++的函数符号要比C函数更复杂。同样的一个函数,在C和C++中,函数符号是完全不一样的。假设有函数


 

在C环境下,它的符号为”_cleanup”,而在C++环境下它的符号为“__Z7cleanupPv”,这就表明,同样一个函数在C和C++中,修饰机制是不一样的。为了避免由于符号不同导致的问题,很多开源代码会加上extern "C” {}来限定函数在C环境。但是在C环境中并不识别extern "C”标识,因此你会看到很多的开源代码中存在以下代码


 

其意图在于如果在C++环境中则限定为C环境。那么究竟C++的修饰机制是怎样的呢?我们看到一个C++函数,如何推断出它的符号呢?很遗憾,我没有找到明确的关于C++函数符号修饰的介绍,不同的编译器不同的平台签名有所不同。不过没有关系,办法还是有的,假设我想知道JavaScriptCore中某个C++函数的符号,那么我们可以创建一个cpp文件,将C++函数名复制过去,


声明命名空间和枚举

 


创建函数

然后通过gcc -c将文件编译成目标文件WBIMC++.o,然后调用命令nm WBIMC++.o即可查看相应的符号。


 

能获取到C++的符号,是不是也就意味着hook C++函数是可行的。我们在fishhook的符号中随便传入一个JavaScriptCore的C++函数符号”_ZNK3JSC11SlotVisitor18containsOpaqueRootEPv“,通过代码断点调试发现,fishhook能够正确获取和替换函数指针


 

因此hook C++是可行的。

四、Mach-O文件简介

    在接触Mach-O之前,我有两个疑问,第一个是之前提出的问题,fishhook为什么不能hook自己写的C函数。第二个问题是跟58正在做的技术项目相关,如何动态调用static 函数。弄清楚这两个问题必须要对Mach-O有较为透彻的了解。

    什么是Mach-O,按我的理解就是遵循特定结构的文件。一般比较常见的文件有:应用程序、目标文件、动态库、链接器等,其中应用程序、目标文件.o是尤为重要的。Mach-O可以分为三个部分:

1)、Header

Header是文件的头部信息,包括CPU信息、文件类型、Command条数及Size信息。总体来说,作为开发者Header使用的较少,比较常用的是(uintptr_t)&_mh_execute_header获取header地址进行计算用。

 


Header

2)、Commands

Commands描述的是文件的加载信息,加载信息有很多,加载的段、符号表、动态库信息等都在Commands中取到。这个部分信息还是比较有用的,我们可以从这里获取到符号表和字符串表的偏移量,下文中会有详细的解释。

 


Commands

首先来说下段(Segment),上图中可以看出共加载了4个段,__PAGEZERO是一个空段,它位于文件起始段的位置。__TEXT和__DATA分别是文本段和数据段,分别存储了代码信息和数据信息。__LINKEDIT是链接信息段,可以通过__LINKEDIT进行地址计算。段又可以细分为section,每个Segment可以包含多个section。


段展开

3)、数据区

    除了Header和Commands外所有的原始数据。Commands是对数据的汇总提示,而数据区则是真实的数据。Commands与数据区的关系就像size和char*的关系。


数据展示

接下来先介绍几个比较重要的模块:

1)、(__TEXT,__text)

这里存放的是汇编后的代码,当我们进行编译时,每个.m文件会经过预编译->编译->汇编形成.o文件,称之为目标文件。汇编后,所有的代码会形成汇编指令存储在.o文件的(__TEXT,__text)区((__DATA,__data)也是类似)。链接后,所有的.o文件会合并成一个文件,所有.o文件的(__TEXT,__text)数据都会按链接顺序存放到应用文件的(__TEXT,__text)中。


(__TEXT,__text)

关键字:

50000+
5万行代码练就真实本领
17年
创办于2008年老牌培训机构
1000+
合作企业
98%
就业率

联系我们

电话咨询

0532-85025005

扫码添加微信