深入研究了一下mach-o

1,561 阅读5分钟

阅读本文之前,你需要 1 理解编译器,链接器的基本概念

2 llvm 是啥

3 熟悉2进制16进制

4 理解可执行文件的基本结构(elf,mach-o 都可)

5 熟悉libobjc的源码,熟悉objective-c语言体系。

进入正题 __objc_nlclslist的研究

关于利用macho文件找哪些类实现了+load 方法,我第一反应是利用符号表查+[xxxxx load],其实有更简单直接的思路,就是macho文件中有个节专门记录了实现了 +load方法的类,就是__objc_nlclslist (即非懒加载的类列表,关于load方法和类加载相关的上下文这里就不多扯了)

这个问题的最佳答案后,我有一些引申思考:

1 这个节在哪个阶段产出的?

2 +load方法是被谁探测到?又被谁记录的?

在我未做实践之前,我给出了一些猜测(这些想法可能不对先记录下)

1:产出阶段在汇编生成单个可重定位的目标文件的过程中,然后经过链接器链接,将相同的节合并,最终生成了可执行文件的__objc_nlclslist节。

2:编译过程中“探测”到 类实现了load方法,然后记录到对应的__objc_nlclslist 节里面。也就是由编译器去完成。

带着这些猜测疑问开始实践

写了个demo,里面有个SSObject 实现了+load方法,工程结构如图

image.png

先用machoview 看下最终的可执行文件 ↓↓↓↓↓↓

image.png 清楚的看到这个节(objc_nlclslist)是存在的,且里面有一条内容。

顺便看下__objc_classlist ↓↓↓↓↓↓

image.png 里面的也有一条内容,里面的的值和__objc_nlclslist一样。 结合工程不难想到,这两个节里面的内容都是代表SSObject 且__objc_nlclslist是__objc_classlist的子集。

我们人工读取一下内容,是0x0000000100008130(倒着读这跟大小端模式有关)

得到这个地址后,去macho里面找一下↓↓↓↓↓↓(这里面所有的地址都是虚拟地址,后面不赘述)

image.png

0x8130这个地址就代表了一个class结构体的首地址,根据内存布局我们找ro结构体即偏移4*8个字节, 即0x00000001000080B0(代表class_ro_t的首地址),接着按地址找 ↓↓↓↓↓↓

image.png

80A8+8 即80B0,然后按照ro的布局偏移4+4+4+4+8个字节,找到name的地址0x00000100003f9B,这也是__objc_classname节的首地址 ↓↓↓↓↓↓(因为只有一个条目) (关于这些结构体的内存布局,请自行查看libobjc的源码)

image.png

地址下面存的就是类的名字 SSObject了( 看ascii码也可以看出来) (PS:要不要继续找地址,要看具体代码判断这个地址下存的数据是值还是地址) 这种方式也可以查到其他内容,此处不再试了。

通过__objc_nlclslist里面的信息,找到对应的class,并通过偏移计算在macho找到了他的name(就是不断的通过地址层层索引,找到最终的值数据) 我们通过探索,验证了 __objc_nlclslist。接下来我计划从可重定位文件SSObject.o研究下,看看有啥信息值得关注 ↓↓↓↓↓↓

也找到了__objc_nlclslist 这个节,但是没有具体内容?不慌,这是因为当前的.o文件是可重定位文件,这些将来要被合并的节里的内容要等到重定位结束之后才会有正确的地址。我们根据这个结论去看下 Relocations节,这里面存放了一些关于重定位的信息。↓↓↓↓↓↓

image.png

可以看到 __objc_nlclslist的确是需要重定位的,关于具体重定位过程,此处不展开。

探索到这里,我们了解了__objc_nlclslist这个节的产生的大致过程,在编译汇编阶段在对应的.o文件里面产生,最终经过链接器 链接合并,重定位,就是最终的可执行的macho里面的那个“汇总”__objc_nlclslist节了。

编译汇编阶段是怎么产生的小节呢?

我们通过clang 看下编译后的SSObject.cpp,在这里我截图展示相关可参考的信息↓↓↓↓↓↓

image.png

image.png 可以看到代码中 往 __DATA__objc_const,__DATA__objc_data,__DATA__objc_classlist里面写了内容。

image.png 从这里都可以找到对应代码中的结构体↑↑↑↑

cpp中最后定义了一个结构体数组,里面存了一条数据。不难看出是通过一个静态数组保存了当前文件里的懒加载类。从这里可以确定:在编译过程中,编译器就已经确定好了哪些类是非懒加载的类了。↓↓↓

其次,cpp中没有往__DATA__objc_nlclslist里填内容。

看下SSObject.ll,也有相关信息↓↓↓↓

需要提一下clang 编译汇编过程的中间产物 .cpp --> .ll ---> .bc--->.s --->.o

从ll代码中发现,此时编译器此时已经知道__objc_nlclslist节里放的就是非懒加载的节,呼应了之前的结论。那ll 代码又是如何生成的?我们细化一下,会有 预处理,词法分析,语法分析,语义分析,中间代码生成。

完成这些步骤的就是llvm里面的clang前端了,那我们后续要接着看一下他的源码(下好源码+配置)

从上面的代码生成过程中,我们不难想到,语义分析结束到中间代码的生成,就是编译器确定往节里面写内容的时机,结合我们之前对cpp代码的分析,在cpp代码里,就已经确定了哪些类是nonlazy,我们可以从CGOBJCMac.cpp 的generateclass函数中看到 ↓↓↓

image.png 可以看到编译生成class的过程中,会判断如果一个class是nonlazy的话,就会将它加入DefinedNonLazyClasses的数组中,然后经过

FinishNonFragileABIModule() -->AddModuleClassList()的调用 ↓↓↓

image.png ↑↑↑ 将DefinedNonLazyClasses中的内容 写入到__objc_nlclslist 节中。 具体实现 ↓↓↓

image.png 至此,我们对于这个节的生成过程有了比较全面的认识。

后续:通过对一个节的探索,可以在一定程度类比到其他节的生成过程。

(本文为作者原创)