从LLVM看MachO

本来最近在追踪class_ro_t在编译阶段的生成过程,无心插柳对MachO的理解确更深刻了,先总结下MachO相关的内容,关于LLVM相关会在后续整理。

背景

为了跟踪class_ro_t的生成时机,及如何实category的方法在主类之前的,只能去下载了LLVM的工程,经过了一番环境配置及运行变量的调试,终于run了起来,并且也得到了最终的可执行文件。
首先为了验证class_ro_t在编译阶段就实现了category的方法在主类之前的,因此使用LLVM的项目来编译源文件从而得到一个可执行文件。于是开启了读取内容的阶段。

CodeGen

我们知道LLVM的编译流程可分成 frontendbackend,而LLVM Backend是给工作与多种语言(语言无关)的。所以需要一个“前端(clang)”来做特定语言的编,之后将编译的结果交个一个“中间人(codeGen)”来做为“前端”的输出和“后端”输入。
CodeGen中跟Objective-C相关的工作如下:

  • 根据不同的语言版本及运行环境选择不同的Objective-C CodeGen构造器。
  • 根据Objc ABI 构建相应的ivar flags class_ro_t等类结构体成员、分类,并写入MachO中对应的section
  • 构建方法,属性,协议等相关结构体。处理属性的settergetter
  • 插入ARC相关代码,objc_autorealeasePool的处理。
  • block结构的构建
  • objc的一些方法优化,如alloc,allocWithZone等
  • objc_msgSend的相关处理。

以上部分可在clangCodeGen文件下找到相关cpp文件。这里我们只关注第二点。

MachO

MachO的概念这不在赘述,那么里面究竟是什么呢,我们平时查到的blog定义都是很概念性的结论,其次我们每次都是直接通过编译之后便得到了一个可执行文件,那么这个文件是如何生成的呢,通过本文不但可以清楚的知道每个段的定义,还可以了解到MachO的生成,最主要的是可以把各个段串联起来。

准备

新建项目,新建PersonPerson的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
32
llvm::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
7
struct 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

获取类的信息

首先我们来随机读取一个类的信息,如图
img
根据上面的分析过的MachO的内容组成,我们有如下思路:

  1. 在classlist中选取一个类A。
  2. 获取A的成员:isa,supercls等。这里我们主要获取ro。
  3. 读取到ro之后,读取其中的数据。

    选择一个类

    classlist段子选择一个类A:我们要读取的类的地址(首地址)为0x100004770,想要获得其内部信息,则需要在objc_data中寻找,那么,如何找到Aobjc_data中的成员信息呢,我们知道MachO中的地址都是以Mach Header的地址为初始地址的,所以要获取0x100004770执行的内容,需要获取0x100004770的相对于header 的偏移量。怎根据偏移量找出0x100004770首地址的内存布局。

    实际内存偏移量 = 虚拟地址 - mach header(段起始地址 + 偏移量)

mach header 总是为0x100000000。
实例:
虚拟地址:
0x100004770
段起始地址和偏移量可以通过下面几种方式获得:
一种是从MachO的Load Commands 里面找到对应的段。
img
还可以借助objdump
img
由此可知:
0x100004770 - 0x100000000 = 4770

获取成员

接下来我们来寻找 偏移量为 4770的段,就是__objc_data,接下来对照objc_class的成员分别读出成员的地址。
img
得到ro的首地址为0x100003168

读取ro中的内容

要获取ro的值,我们同样利用上面的公式

虚拟地址-段起始地址+偏移量

上面 objdump中看出objc_data的起始地址为0x100004748偏移为0x4748
计算得出偏移量:
0x100003168 - 0x100000000 = 0x3168
接下来我们来寻找 偏移量为 3168 的段,也就是objc_const,我们参考ro的成员分别读出对应的地址
img
name: 0x1000014FB
baseMethods:0x1000030F8
继续套用公式,得出:
name对应的偏移量为:14FB
baseMethods对应的偏移量:30F8
接下来查找14FB的段
img
所以我们读取的这个class A 的类名为Person
由于30F8还在当前段(__objc_data)内,所以可以继续读取。首先baseMethodsentsize_list_tt结构体的一维数组形式,成员为分别为entsize , count ,list,list内部就是method_t

1
2
3
4
5
struct method_t {
SEL name;//方法名
const char *types;//方法参数
IMP imp;//方法的实现
}

所以我们按照上面分析的结构来逐步读取。
img
所以我们的到四个method分别为
name | IMP
-|-
0x10000157B | 0x100000D50
0x100001583 | 0x100000D60
0x10000157B | 0x100000D70
0x100001593 | 0x100000D80
同样的计算方式,这里不在赘述。我们可以得到如下结果
img
img
我们看到这4个方法分别跟我们之间准备的一致,而且category的方法也保持在主类的前面。

读取category

如果你想通过MachO 直接查看主类,分类名等信息。方法如下
img
获取步骤如下:

  1. 首地址为0x100003120
  2. 偏移为3120,读取3120对应段读内存,按照category_t的成员一一对应
    img
    获得
    name | cls
    -|-
    0x1000014F6 | 0x1000047F0
    这里由于重新编译过修改过person的类结构,使得内存发生了变化,但不影响结论。
    img
    img
    可以看出category name 为test,主类的首地址为0x1000047F0,重复上面的步骤即可获得主类的信息。