本来最近在追踪class_ro_t
在编译阶段的生成过程,无心插柳对MachO
的理解确更深刻了,先总结下MachO
相关的内容,关于LLVM
相关会在后续整理。
背景
为了跟踪class_ro_t
的生成时机,及如何实category
的方法在主类之前的,只能去下载了LLVM的工程,经过了一番环境配置及运行变量的调试,终于run了起来,并且也得到了最终的可执行文件。
首先为了验证class_ro_t
在编译阶段就实现了category
的方法在主类之前的,因此使用LLVM的项目来编译源文件从而得到一个可执行文件。于是开启了读取内容的阶段。
CodeGen
我们知道LLVM
的编译流程可分成 frontend
和 backend
,而LLVM Backend是给工作与多种语言(语言无关)的。所以需要一个“前端(clang)”来做特定语言的编,之后将编译的结果交个一个“中间人(codeGen)”来做为“前端”的输出和“后端”输入。
而CodeGen
中跟Objective-C
相关的工作如下:
- 根据不同的语言版本及运行环境选择不同的
Objective-C
CodeGen
构造器。 - 根据
Objc ABI
构建相应的ivar flags class_ro_t
等类结构体成员、分类,并写入MachO
中对应的section
中 - 构建方法,属性,协议等相关结构体。处理属性的
setter
,getter
等 - 插入ARC相关代码,objc_autorealeasePool的处理。
- block结构的构建
- objc的一些方法优化,如alloc,allocWithZone等
- objc_msgSend的相关处理。
以上部分可在clangCodeGen
文件下找到相关cpp文件。这里我们只关注第二点。
MachO
MachO
的概念这不在赘述,那么里面究竟是什么呢,我们平时查到的blog定义都是很概念性的结论,其次我们每次都是直接通过编译之后便得到了一个可执行文件,那么这个文件是如何生成的呢,通过本文不但可以清楚的知道每个段的定义,还可以了解到MachO
的生成,最主要的是可以把各个段串联起来。
准备
新建项目,新建Person
和Person
的category test。代码如下1
2
3
4
5
6
7
8
9
10@interface Person : NSObject
- (void)method1;
- (void)method2;
@end
@interface Person (test)
- (void)category1Method1;
@end
段的内容
这里会挑一些下面计算需要的段来做示例,想看全部的段的说明,可查看LLVM 的源码中,codeGen部分
__objc_classlist
根据是否支持NonFragile
来确定构建class内部成员的内容,这里只列出NonFragile
的ABI。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void CGObjCNonFragileABIMac::FinishNonFragileABIModule() {
// nonfragile abi has no module definition.
// Build list of all implemented class addresses in array
// L_OBJC_LABEL_CLASS_$.
for (unsigned i=0, NumClasses=ImplementedClasses.size(); i<NumClasses; i++) {
const ObjCInterfaceDecl *ID = ImplementedClasses[i];
assert(ID);
if (ObjCImplementationDecl *IMP = ID->getImplementation())
// We are implementing a weak imported interface. Give it external linkage
if (ID->isWeakImported() && !IMP->isWeakImported()) {
DefinedClasses[i]->setLinkage(llvm::GlobalVariable::ExternalLinkage);
DefinedMetaClasses[i]->setLinkage(llvm::GlobalVariable::ExternalLinkage);
}
}
//所有的类对象
AddModuleClassList(DefinedClasses, "OBJC_LABEL_CLASS_$",
GetSectionName("__objc_classlist",
"regular,no_dead_strip"));
//非懒加载的类
AddModuleClassList(DefinedNonLazyClasses, "OBJC_LABEL_NONLAZY_CLASS_$",
GetSectionName("__objc_nlclslist",
"regular,no_dead_strip"));
// Build list of all implemented category addresses in array
// L_OBJC_LABEL_CATEGORY_$.
//category 部分
AddModuleClassList(DefinedCategories, "OBJC_LABEL_CATEGORY_$",
GetSectionName("__objc_catlist",
"regular,no_dead_strip"));
AddModuleClassList(DefinedStubCategories, "OBJC_LABEL_STUB_CATEGORY_$",
GetSectionName("__objc_catlist2",
"regular,no_dead_strip"));
AddModuleClassList(DefinedNonLazyCategories, "OBJC_LABEL_NONLAZY_CATEGORY_$",
GetSectionName("__objc_nlcatlist",
"regular,no_dead_strip"));
EmitImageInfo();
}
在FinishNonFragileABIModule
之前,会有一步GenerateClass
方法的调用,改方法内部会构建objc_class
的内部成员:flags,
superCls,metacls,class_ro_t等,在构建了类对象之后,会把类对象与父类元类关联起来,并把得到了类对象放入DefinedClasses
数组中,同理DefinedCategories
也是经过上面相同的步骤。
因此__objc_classlist
存的就是所有的类对象。
__objc_data
下一步我们来看类对象内部成员是什么写入到MachO的,为后面我们直接通过MachO来读取一个类的内容做铺垫。
我们可以通过CodeGen
中下面这部分代码找到线索。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32llvm::GlobalVariable *
CGObjCNonFragileABIMac::BuildClassObject(const ObjCInterfaceDecl *CI,
bool isMetaclass,
llvm::Constant *IsAGV,
llvm::Constant *SuperClassGV,
llvm::Constant *ClassRoGV,
bool HiddenVisibility) {
ConstantInitBuilder builder(CGM);
auto values = builder.beginStruct(ObjCTypes.ClassnfABITy);
values.add(IsAGV);
if (SuperClassGV) {
values.add(SuperClassGV);
} else {
values.addNullPointer(ObjCTypes.ClassnfABIPtrTy);
}
values.add(ObjCEmptyCacheVar);
values.add(ObjCEmptyVtableVar);
values.add(ClassRoGV);
llvm::GlobalVariable *GV =
cast<llvm::GlobalVariable>(GetClassGlobal(CI, isMetaclass, ForDefinition));
values.finishAndSetAsInitializer(GV);
if (CGM.getTriple().isOSBinFormatMachO())
GV->setSection("__DATA, __objc_data");
GV->setAlignment(llvm::Align(
CGM.getDataLayout().getABITypeAlignment(ObjCTypes.ClassnfABITy)));
if (!CGM.getTriple().isOSBinFormatCOFF())
if (HiddenVisibility)
GV->setVisibility(llvm::GlobalValue::HiddenVisibility);
return GV;
}
上面的代码含义如下:
- 创建一个建造器,并设置建造的结构体为
ClassnfABITy
- 对结构体赋值,包括isa,supercls,空的cache,空的vtable,ro。
- 获取GV变量(代表二进制的内容对象),和结构体关联。
- 判断二进制格式是否是MachO,并写入
__objc_data
。
对比下面的objc_class
的定义。1
2
3
4
5
6
7struct objc_class {
uint64_t isa;
uint64_t superclass;
uint64_t cache;
uint64_t vtable;
uint64_t data; // points to class_ro_t
};
由此可以发现,__objc_data
段中存储的就是objc_class
的所有成员。
__objc_const
存放类的元数据,
读取MachO
获取类的信息
首先我们来随机读取一个类的信息,如图
根据上面的分析过的MachO的内容组成,我们有如下思路:
- 在classlist中选取一个类A。
- 获取A的成员:isa,supercls等。这里我们主要获取ro。
- 读取到ro之后,读取其中的数据。
选择一个类
在classlist
段子选择一个类A:我们要读取的类的地址(首地址)为0x100004770
,想要获得其内部信息,则需要在objc_data
中寻找,那么,如何找到A
在objc_data
中的成员信息呢,我们知道MachO中的地址都是以Mach Header的地址为初始地址的,所以要获取0x100004770
执行的内容,需要获取0x100004770
的相对于header 的偏移量。怎根据偏移量找出0x100004770
首地址的内存布局。实际内存偏移量 = 虚拟地址 - mach header(段起始地址 + 偏移量)
mach header 总是为0x100000000。
实例:
虚拟地址:
0x100004770
段起始地址和偏移量可以通过下面几种方式获得:
一种是从MachO的Load Commands 里面找到对应的段。
还可以借助objdump
由此可知:
0x100004770 - 0x100000000 = 4770
获取成员
接下来我们来寻找 偏移量为 4770的段,就是__objc_data
,接下来对照objc_class
的成员分别读出成员的地址。
得到ro
的首地址为0x100003168
读取ro中的内容
要获取ro
的值,我们同样利用上面的公式
虚拟地址-段起始地址+偏移量
上面 objdump
中看出objc_data
的起始地址为0x100004748
偏移为0x4748
。
计算得出偏移量:
0x100003168 - 0x100000000 = 0x3168
接下来我们来寻找 偏移量为 3168 的段,也就是objc_const
,我们参考ro
的成员分别读出对应的地址
name: 0x1000014FB
baseMethods:0x1000030F8
继续套用公式,得出:
name对应的偏移量为:14FB
baseMethods对应的偏移量:30F8
接下来查找14FB的段
所以我们读取的这个class A 的类名为Person
由于30F8
还在当前段(__objc_data)内,所以可以继续读取。首先baseMethods
是entsize_list_tt
结构体的一维数组形式,成员为分别为entsize
, count
,list
,list内部就是method_t
1
2
3
4
5struct method_t {
SEL name;//方法名
const char *types;//方法参数
IMP imp;//方法的实现
}
所以我们按照上面分析的结构来逐步读取。
所以我们的到四个method分别为
name | IMP
-|-
0x10000157B | 0x100000D50
0x100001583 | 0x100000D60
0x10000157B | 0x100000D70
0x100001593 | 0x100000D80
同样的计算方式,这里不在赘述。我们可以得到如下结果
我们看到这4个方法分别跟我们之间准备的一致,而且category的方法也保持在主类的前面。
读取category
如果你想通过MachO 直接查看主类,分类名等信息。方法如下
获取步骤如下:
- 首地址为
0x100003120
- 偏移为3120,读取3120对应段读内存,按照category_t的成员一一对应
获得
name | cls
-|-
0x1000014F6 | 0x1000047F0
这里由于重新编译过修改过person的类结构,使得内存发生了变化,但不影响结论。
可以看出category name 为test
,主类的首地址为0x1000047F0
,重复上面的步骤即可获得主类的信息。