4. class-dump
class-dump
是一个用于从可执行文件中获取类、方法和属性的工具。
1. 下载安装
官网下载: stevenygard.com/projects/cl…
点击.dmg文件安装,将class-dump
复制到 /usr/local/bin
测试:
终端输入class-dump
命令
$ class-dump
输入如下,说明成功安装:
class-dump 3.5 (64 bit) (Debug version compiled Sep 17 2017 16:24:48)
Usage: class-dump [options] <mach-o-file>
where options are:
-a show instance variable offsets
-A show implementation addresses
--arch <arch> choose a specific architecture from a universal binary (ppc, ppc64, i386, x86_64, armv6, armv7, armv7s, arm64)
-C <regex> only display classes matching regular expression
-f <str> find string in method name
-H generate header files in current directory, or directory specified with -o
-I sort classes, categories, and protocols by inheritance (overrides -s)
-o <dir> output directory used for -H
-r recursively expand frameworks and fixed VM shared libraries
-s sort classes and categories by name
-S sort methods by name
-t suppress header in output, for testing
--list-arches list the arches in the file, then exit
--sdk-ios specify iOS SDK version (will look for /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS<version>.sdk
or /Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS<version>.sdk)
--sdk-mac specify Mac OS X version (will look for /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX<version>.sdk
or /Developer/SDKs/MacOSX<version>.sdk)
--sdk-root specify the full SDK root path (or use --sdk-ios/--sdk-mac for a shortcut)
2. 使用
应用砸壳之后,获得一个 Mach-O
执行文件或 .app
的 应用程序 文件。
然后参考上面的命令执行class-dump
OC与swift混编报错: Error:Cannot find offset for address 0xd80000000101534a in stringAtAddress: 原因: 由于项目使用了Swift和Oc混编。 解决: 1.使用大神的
class-dump
替换 github.com/AloneMonkey… 2.给class-dump文件权限:sudo chmod 777 /usr/local/bin/class-dump
class-dump -H jingling -o Headers
-H: 获取头文件 -o: 头文件的输入路径
3. class-dump原理
下载源码进行分析,github.com/AloneMonkey…
程序的入口在class-dump.m
的main
函数。
前面是一些参数指令,不是核心代码。可以从下面代码开始入手分析。
CDFile *file = [CDFile fileWithContentsOfFile: executablePath searchPathState: classDump.searchPathState];
点进去查看:
+ (id)fileWithContentsOfFile:(NSString *)filename searchPathState:(CDSearchPathState *)searchPathState;
{
NSData *data = [NSData dataWithContentsOfMappedFile:filename];
CDFatFile *fatFile = [[CDFatFile alloc] initWithData:data filename:filename searchPathState:searchPathState];
if (fatFile != nil)
return fatFile;
CDMachOFile *machOFile = [[CDMachOFile alloc] initWithData:data filename:filename searchPathState:searchPathState];
return machOFile;
}
这里根据可执行文件的路径将其加载到内存中,获取到它的二进制文件data
,并根据加载的data
数据创建CDFile
实例。同时根据data
的前4个字节的值判断具体应该创建CDFatFile
还是 CDMachOFile
实例(多架构还是单架构)。
下面回到main
函数,继续向下分析。
[classDump processObjectiveCData];
在这里处理和解析data
数据。点进去查看processObjectiveCData
- (void)processObjectiveCData;
{
for (CDMachOFile *machOFile in self.machOFiles) {
CDObjectiveCProcessor *processor = [[[machOFile processorClass] alloc] initWithMachOFile:machOFile];
//处理解析
[processor process];
[_objcProcessors addObject:processor];
}
}
查看[processor process]
- (void)process;
{
if (self.machOFile.isEncrypted == NO && self.machOFile.canDecryptAllSegments) {
//首先从LC_SYMTAB定位到Symbol Table,然后枚举符号表
[self.machOFile.symbolTable loadSymbols];
//读取LC_DYSYMTAB
[self.machOFile.dynamicSymbolTable loadSymbols];
//从__DATA,__objc_protolist 读取解析协议列表
[self loadProtocols];
//合并
[self.protocolUniquer createUniquedProtocols];
// Load classes before categories, so we can get a dictionary of classes by address.
//从__DATA,__objc_classlist 读取解析类列表
[self loadClasses];
//从__DATA,__objc_catlist 读取解析分类列表
[self loadCategories];
}
}
这里主要是加载符号表,读取解析协议列表、类列表、分类列表。
下面我们就结合class-dump
源码、MachOView
工具、objc
源码进行分析。
3.1 解析协议
查看loadProtocols
- (void)loadProtocols;
{
CDSection *section = [[self.machOFile dataConstSegment] sectionWithName:@"__objc_protolist"];
//遍历所有地址,然后到该地址读取协议结构
CDMachOFileDataCursor *cursor = [[CDMachOFileDataCursor alloc] initWithSection:section];
while ([cursor isAtEnd] == NO)
[self protocolAtAddress:[cursor readPtr]];
}
读取名为__objc_protolist
的section
, 这里面存放的是所有协议的地址,然后遍历所有地址,跳到协议的实际存放位置来读取协议的信息。
通过MachOView
查看可执行文件,找到对应的section
第一个协议的地址是0000000100009068
,这是虚拟地址,通过它并不能直接找到协议的实际存放位置。
继续查看protocolAtAddress
:
CDMachOFileDataCursor *cursor = [[CDMachOFileDataCursor alloc] initWithFile:self.machOFile address:address];
这里创建一个根据虚拟地址address
的游标cursor
,内部实现了从虚拟地址到文件偏移地址的换算。
一路跟踪,找到换算公式。
- (NSUInteger)fileOffsetForAddress:(NSUInteger)address;
{
NSParameterAssert([self containsAddress:address]);
return _section.offset + address - _section.addr;
}
查看MachOView
,当前address
对应的section
为__DATA
。
对应的offset
和addr
分别为0x0000000000008000
和0000000100008000
。
计算可得address 0x0000000100009068
对应的文件偏移地址为0x9068
,当这是在ARM64
架构对应的MachO
文件中的偏移位置。
而当前ARM64
在当前FAT
可执行文件下的偏移为0x00018000
。
所以实际的文件偏移地址为0x00018000 + 0x9068 = 0x0021068
。
如下图所示,协议内容如下:
回到源码,继续查看protocolAtAddress
。在转换成文件偏移位置后,开始读取当前协议的数据。
//开始读取
struct cd_objc2_protocol objc2Protocol;
objc2Protocol.isa = [cursor readPtr];
objc2Protocol.name = [cursor readPtr];
objc2Protocol.protocols = [cursor readPtr];
objc2Protocol.instanceMethods = [cursor readPtr];
objc2Protocol.classMethods = [cursor readPtr];
objc2Protocol.optionalInstanceMethods = [cursor readPtr];
objc2Protocol.optionalClassMethods = [cursor readPtr];
objc2Protocol.instanceProperties = [cursor readPtr];
objc2Protocol.size = [cursor readInt32];
objc2Protocol.flags = [cursor readInt32];
objc2Protocol.extendedMethodTypes = 0;
阅读过objc
的源码后吗,我们知道协议的本质其实也是一个类,是一个继承自objc_object
的结构体。
struct protocol_t : objc_object {
//协议名
const char *mangledName;
// 所继承的协议
struct protocol_list_t *protocols;
// 协议中的实例方法
method_list_t *instanceMethods;
// 协议中的类方法
method_list_t *classMethods;
// 协议中的可选的实例方法
method_list_t *optionalInstanceMethods;
// 协议中可选的类方法
method_list_t *optionalClassMethods;
// 协议中声明的属性
property_list_t *instanceProperties;
// 这个 size 是整个 protocol_t 内容大小
uint32_t size; // sizeof(protocol_t)
uint32_t flags;
}
cursor
游标依次读取相对应的成员变量到objc2Protocol
中保存。
后面会根据拿到成员变量存储的地址,去读取所对应的方法名、实例方法列表等。
其实文件偏移地址的计算还可以用下面的公式来计算:
文件偏移地址 = 虚拟地址 - 模块在内存中的地址 + 模块在文件中的偏移
下面以协议名的读取为例:
- 虚拟地址:
0x0000000100006C40
- 模块在内存中的地址:
0x0000000100000000
- 模块在文件中的偏移:
0x00018000
计算后得到的offset
为0x1EC40
, 找到对应的值为NSObject
。
后续的解析过程,都与此类似。
3.2 解析类
查看loadClassAtAddress
。
struct cd_objc2_class objc2Class;
objc2Class.isa = [cursor readPtr];
objc2Class.superclass = [cursor readPtr];
objc2Class.cache = [cursor readPtr];
objc2Class.vtable = [cursor readPtr];
uint64_t value = [cursor readPtr];
class.isSwiftClass = (value & 0x1) != 0;
if ([self.machOFile uses64BitABI]){
objc2Class.data = value & ~7;
}else{
objc2Class.data = value & ~3;
}
objc2Class.reserved1 = [cursor readPtr];
objc2Class.reserved2 = [cursor readPtr];
objc2Class.reserved3 = [cursor readPtr];
objc
中objc_class
的结构:
struct objc_class : objc_object {
// Class ISA; //8字节
Class superclass; //8字节
cache_t cache; //16字节 // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t *
class_rw_t *data() const {
return bits.data();
}
}
cacht_t
结构:
struct cache_t {//总体16字节
private:
// 掩码和 Buckets 指针共同保存在 uintptr_t 类型的 _bucketsAndMaybeMask 中
explicit_atomic<uintptr_t> _bucketsAndMaybeMask; //是指针,占8字节
union {
struct {
// buckets 数组的最大容量-1
explicit_atomic<mask_t> _maybeMask;//是mask_t 类型,最多占4字节
#if __LP64__
// 如果是 64 位环境的话会多一个 _flags 标志位
uint16_t _flags;//是uint16_t类型, 占 2个字节
#endif
// 缓存数组的已占用量
uint16_t _occupied;//是uint16_t类型,占 2个字节
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache; // 预先构建的IMP缓存 仅在arm64真机上使用 占8字节
};
}
在objc2Class
中是用两个字段存储cache_t
的,cache
和vtable
。
class_data_bits_t
结构体只有一个成员变量bits
,但是却存储了多重信息数据。class_ro_t
的地址就存储它里面。
struct class_data_bits_t {
//声明了 objc_class 为其友元类,objc_class 可以完全访问和调用 class_data_bits_t 的私有成员变量和私有方法。
friend objc_class;
// Values are the FAST_ flags above.
//仅有的一个成员变量 uintptr_t bits,这里之所以把它命名为 bits 也是有其意义的,它通过掩码的形式保存 class_rw_t 指针和是否是 swift 类等一些标志位。
uintptr_t bits;
}
data
方法作用是bits
中读取出class_rw_t
的地址。在编译时其实是class_ro_t
,只有在程序运行时才是class_rw_t
,根据class_ro_t
为其赋初值。所以这里读取时需要按照class_ro_t
格式读取。
// __LP64__: #define FAST_DATA_MASK 0x00007ffffffffff8UL
// !__LP64__: #define FAST_DATA_MASK 0xfffffffcUL
class_rw_t* data() const {
// 与操作取出 class_rw_t 指针
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
class_ro_t
结构:
struct class_ro_t {
// 通过掩码保存的一些标志位
/*
// class is a metaclass
#define RO_META (1<<0)
// class is a root class
#define RO_ROOT (1<<1)
// class has .cxx_construct/destruct implementations
#define RO_HAS_CXX_STRUCTORS (1<<2)
// class has +load implementation
// #define RO_HAS_LOAD_METHOD (1<<3)
// class has visibility=hidden set
#define RO_HIDDEN (1<<4)
// class has attribute(objc_exception): OBJC_EHTYPE_$_ThisClass is non-weak
#define RO_EXCEPTION (1<<5)
// class has ro field for Swift metadata initializer callback
#define RO_HAS_SWIFT_INITIALIZER (1<<6)
// class compiled with ARC
#define RO_IS_ARC (1<<7)
// class has .cxx_destruct but no .cxx_construct (with RO_HAS_CXX_STRUCTORS)
#define RO_HAS_CXX_DTOR_ONLY (1<<8)
// class is not ARC but has ARC-style weak ivar layout
#define RO_HAS_WEAK_WITHOUT_ARC (1<<9)
// class does not allow associated objects on instances
#define RO_FORBIDS_ASSOCIATED_OBJECTS (1<<10)
// class is in an unloadable bundle - must never be set by compiler
#define RO_FROM_BUNDLE (1<<29)
// class is unrealized future class - must never be set by compiler
#define RO_FUTURE (1<<30)
// class is realized - must never be set by compiler
#define RO_REALIZED (1<<31)
*/
uint32_t flags;
// 自己成员变量的起始偏移量
// 因为会继承父类的成员变量, 所以自己的成员变量,要排在父类的成员变量之后
uint32_t instanceStart;
// 根据内存对齐计算成员变量从前到后所占用的内存大小,
// 不过没有进行总体的内存对齐,例如最后一个成员变量是 char 时,
// 则最后只是加 1,instanceSize 的值是一个奇数,
// 再进行一个整体 8/4 字节对齐就好了,
//(__LP64__ 平台下 8 字节对齐,其它则是 4 字节对齐)
// objc_class 的 alignedInstanceSize 函数,
// 完成了这最后一步的整体内存对齐。
// 尚未进行内存对齐的实例大小
uint32_t instanceSize;
#ifdef __LP64__
//仅在 64 位系统架构下的包含的保留位
uint32_t reserved;
#endif
union {
// 记录了哪些是 strong 的 ivar
const uint8_t * ivarLayout;
// 元类
Class nonMetaclass;
};
// name 应该是类名
explicit_atomic<const char *> name;
// With ptrauth, this is signed if it points to a small list, but
// may be unsigned if it points to a big list.
// 实例方法列表
void *baseMethodList;
// 协议列表
protocol_list_t * baseProtocols;
// 成员变量列表
const ivar_list_t * ivars;
// 记录了哪些是 weak 的 ivar
const uint8_t * weakIvarLayout;
// 属性列表
property_list_t *baseProperties;
}
下面以存储的第一个方法为例进行解析。
计算后类的存储偏移为0x2AFA0
。
data
的值0x0000000100008578
与 0x00007ffffffffff8
进行&
运算后 结果还是0x0000000100008578
。
按照上面公式计算后得到文件偏移位置0x20578
,这里存放的是class_ro_t
3.3 解析方法
类的实例方法列表的地址存放在class_ro_t
的baseMethodList
成员变量中。
method_list_t
结构如下:
struct method_list_t : entsize_list_tt<method_t, method_list_t, 0xffff0003, method_t::pointer_modifier> {
...
}
struct entsize_list_tt {
// entsize(entry 的大小) 和 Flags 以掩码形式保存在 entsizeAndFlags 中
uint32_t entsizeAndFlags;
// entsize_list_tt 的容量
uint32_t count;
// 返回指定索引的元素的的引用,orEnd 表示 i 可以等于 count,
// 当 i 等于 count 时返回最后一个元素的后面的位置。
Element& getOrEnd(uint32_t i) const {
// 断言,i 不能超过 count
ASSERT(i <= count);
// 首先取出 this 地址(强转为 uint8_t 指针),然后指针偏移sizeof(*this)个字节长度, 然后再指针偏移 i * ensize() 个字节长度,
// 然后转换为 Element 指针,然后解引用取出指针指向内容作为 Element 引用返回。
return *PointerModifier::modify(*this, (Element *)((uint8_t *)this + sizeof(*this) + i*entsize()));
}
// 在索引范围内返回 Element 引用
Element& get(uint32_t i) const {
ASSERT(i < count);
return getOrEnd(i);
}}
entsize_list_tt
相当于一个容器,存储了元素列表大小和个数。当要获取指定下标的元素时,可以在当前容器地址的基础上加上相对应的偏移得到。
方法method_t
的结构如下:
struct method_t {
SEL name; // 方法名
const char *types; // 方法类型
MethodListIMP imp; // 方法实现
};
在MachOView
中查看method_list_t
。
根据0x0000000100008478
计算出文件偏移地址为0x20478
。
后面跟的就是所有的方法。
查看ptotocolDoSomething
方法的实现:
发现方法已经转换成汇编代码了。