4. class-dump

538 阅读2分钟

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.mmain函数。 前面是一些参数指令,不是核心代码。可以从下面代码开始入手分析。

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_protolistsection, 这里面存放的是所有协议的地址,然后遍历所有地址,跳到协议的实际存放位置来读取协议的信息。 通过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 对应的offsetaddr分别为0x00000000000080000000000100008000。 计算可得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 计算后得到的offset0x1EC40, 找到对应的值为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];

objcobjc_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的,cachevtable

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的值0x00000001000085780x00007ffffffffff8 进行&运算后 结果还是0x0000000100008578。 按照上面公式计算后得到文件偏移位置0x20578,这里存放的是class_ro_t

3.3 解析方法

类的实例方法列表的地址存放在class_ro_tbaseMethodList成员变量中。

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方法的实现: 发现方法已经转换成汇编代码了。