【iOS】class-dump源码学习(三)

上一篇:【iOS】class-dump源码学习(二)

续上一篇讲完CDObjectiveCProcessor的process方法中加载符号表的两个方法,今天继续process方法学习


回到CDObjectiveCProcessor的process方法继续,紧接着加载符号表的是

[self loadProtocols];
[self.protocolUniquer createUniquedProtocols];

顾名思义loadProtocols方法是加载协议表,这里先以CDObjectiveC2Processor的处理做说明

- (void)loadProtocols;
{
    CDSection *section = [[self.machOFile segmentWithName:@"__DATA"] sectionWithName:@"__objc_protolist"];
    
    CDMachOFileDataCursor *cursor = [[CDMachOFileDataCursor alloc] initWithSection:section];
    while ([cursor isAtEnd] == NO)
        [self protocolAtAddress:[cursor readPtr]];
}

很好理解,读取__Data段的__objc_protolist区,然后遍历区获得指针address

看一下CDObjectiveC2Processor的protocolAtAddress:方法对获得的指针address如何处理

首先指针address会被当作键,class-dump定义的CDOCProtocol对象作为值存储在一个CDProtocolUniquer管理类的字典中。

然后根据指针address的地址声明一个CDMachOFileDataCursor的对象cursor,开始读取协议具体字段

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;

系统使用cd_objc2_protocol结构来记录协议在mach-o文件中的信息。

如果size字段的大小大于8个指针+两个Int32,意味着extendedMethodTypes字段还有内容

cursor再读取一个指针,说明extendedMethodTypes是一个指针指向了一个新的区域,用于说明extendedMethodTypes

接着开始把cd_objc2_protocol中的字段映射到CDOCProtocol对象中。

  • protocols字段指向了一个储存多个协议地址(应该是父类协议)的地址,储存形式是一个表示count的字段加上count个指向新协议的指针,这些协议指针被放在CDOCProtocol的_protocols字段。
  • instanceMethods字段指向了一个方法列表,由一个cd_objc2_list_header结构加上cd_objc2_list_header.count个cd_objc2_method结构组成。如果前面extendedMethodTypes指针不为空,cd_objc2_method的types会被extendedMethodTypes指向的字符串覆盖。最后cd_objc2_method构成了class-dump的CDOCMethod类对象并作为数组保存在CDOCProtocol的_instanceMethods字段。
//cd_objc2.h
struct cd_objc2_list_header {
    uint32_t entsize;
    uint32_t count;
};

struct cd_objc2_method {
    uint64_t name;
    uint64_t types;
    uint64_t imp;
};
  • classMethods、optionalInstanceMethods、optionalClassMethods字段和instanceMethods处理相同,最后CDOCMethod类对象并作为数组保存在CDOCProtocol的_classMethods等字段。
  • instanceProperties字段指向了一个属性列表,由一个cd_objc2_list_header结构加上cd_objc2_list_header.count个cd_objc2_property结构组成。最后cd_objc2_property构成了class-dump的CDOCProperty类对象并作为数组保存在CDOCProtocol的_properties字段。
struct cd_objc2_property {
    uint64_t name;
    uint64_t attributes;
};

至此CDObjectiveC2Processor的protocolAtAddress:方法结束,把协议的信息全部解析存储到了CDProtocolUniquer的_protocolsByAddress字典中。

(CDObjectiveC1Processor的处理喵了一眼,读取了不同的区段,更多细节暂时挂起吧)

接下来是CDProtocolUniquer的createUniquedProtocols方法。

这个方法比较简单,主要是对CDProtocolUniquer的_protocolsByAddress字典按序(顺序由compare:方法定义)读取并且转存到了CDProtocolUniquer的_uniqueProtocolsByAddress字典中。


回到CDObjectiveCProcessor的process方法继续,紧接着协议加载的是

// Load classes before categories, so we can get a dictionary of classes by address.
[self loadClasses];
[self loadCategories];

先看一下CDObjectiveC2Processor的loadClasses方法,顾名思义就是一个加载类的方法

- (void)loadClasses;
{
    CDSection *section = [[self.machOFile segmentWithName:@"__DATA"] sectionWithName:@"__objc_classlist"];
    
    CDMachOFileDataCursor *cursor = [[CDMachOFileDataCursor alloc] initWithSection:section];
    while ([cursor isAtEnd] == NO) {
        uint64_t val = [cursor readPtr];
        CDOCClass *aClass = [self loadClassAtAddress:val];
        if (aClass != nil) {
            [self addClass:aClass withAddress:val];
        }
    }
}

读取__Data段的__objc_classlist区,然后遍历区获得指针val,通过loadClassAtAddress:构造了CDOCClass对象之后分别保存在_classes数组和_classesByAddress字典(val作为键)中。

类似加载协议,看一下CDObjectiveC2Processor的loadClassAtAddress:方法对获得的指针address(val)如何处理

根据指针address的地址声明一个CDMachOFileDataCursor的对象cursor,开始读取OC类结构cd_objc2_class(对应系统的objc_class)的具体字段

    struct cd_objc2_class objc2Class;
    objc2Class.isa        = [cursor readPtr];
    objc2Class.superclass = [cursor readPtr];
    objc2Class.cache      = [cursor readPtr];
    objc2Class.vtable     = [cursor readPtr];
    objc2Class.data       = [cursor readPtr];
    objc2Class.reserved1  = [cursor readPtr];
    objc2Class.reserved2  = [cursor readPtr];
    objc2Class.reserved3  = [cursor readPtr];

这里专注data字段,指向了class_ro_t。

class_ro_t是什么?class_ro_t是编译期间就已经确定的表示类的结构,此时objc_class的data指向的就是class_ro_t。直到运行时runtime,具体就是在运行runtime的realizeClass 方法时,会生成class_rw_t结构体,该结构体包含了class_ro_t,并且更新data部分,换成class_rw_t结构体的地址。可以说class_rw_t是class_ro_t的超集,当然实际访问类的方法、属性等也都是访问的class_rw_t中的内容

参考:class_ro_t 和 class_rw_t 的区别?

在class-dump中使用cd_objc2_class_ro_t结构表示class_ro_t,cursor继续读取

[cursor setAddress:objc2Class.data];

struct cd_objc2_class_ro_t objc2ClassData;
objc2ClassData.flags         = [cursor readInt32];
objc2ClassData.instanceStart = [cursor readInt32];
objc2ClassData.instanceSize  = [cursor readInt32];
if ([self.machOFile uses64BitABI])
    objc2ClassData.reserved  = [cursor readInt32];
else
    objc2ClassData.reserved = 0;
    
objc2ClassData.ivarLayout     = [cursor readPtr];
objc2ClassData.name           = [cursor readPtr];
objc2ClassData.baseMethods    = [cursor readPtr];
objc2ClassData.baseProtocols  = [cursor readPtr];
objc2ClassData.ivars          = [cursor readPtr];//成员变量
objc2ClassData.weakIvarLayout = [cursor readPtr];
objc2ClassData.baseProperties = [cursor readPtr];

完成了objc2Class和objc2ClassData的读取,接着是CDOCClass aClass的构建(CDOCClass是继承CDOCProtocol的,应该是为了复用一些变量)

    NSString *str = [self.machOFile stringAtAddress:objc2ClassData.name];
    
    CDOCClass *aClass = [[CDOCClass alloc] init];
    [aClass setName:str];

一、

类名aClass.name通过objc2ClassData.name指针指向的地址(根据class-dump代码来看是指向了段中的地址)读取string来获取的

二、

实例方法aClass.instanceMethods通过objc2ClassData.baseMethods调用加载协议时分析过的加载方法的loadMethodsAtAddress:extendedMethodTypesCursor:来获取

三、

实例变量aClass.instanceVariables通过objc2ClassData.ivars调用加载变量的方法loadIvarsAtAddress:来获取

- (NSArray *)loadIvarsAtAddress:(uint64_t)address;
{
    NSMutableArray *ivars = [NSMutableArray array];
    
    if (address != 0) {
        CDMachOFileDataCursor *cursor = [[CDMachOFileDataCursor alloc] initWithFile:self.machOFile address:address];
        NSParameterAssert([cursor offset] != 0);
        //NSLog(@"ivar list data offset: %lu", [cursor offset]);
        
        struct cd_objc2_list_header listHeader;
        
        listHeader.entsize = [cursor readInt32];
        listHeader.count = [cursor readInt32];
        NSParameterAssert(listHeader.entsize == 3 * [self.machOFile ptrSize] + 2 * sizeof(uint32_t));
        
        for (uint32_t index = 0; index < listHeader.count; index++) {
            struct cd_objc2_ivar objc2Ivar;
            
            objc2Ivar.offset    = [cursor readPtr];
            objc2Ivar.name      = [cursor readPtr];
            objc2Ivar.type      = [cursor readPtr];
            objc2Ivar.alignment = [cursor readInt32];
            objc2Ivar.size      = [cursor readInt32];
            
            if (objc2Ivar.name != 0) {
                NSString *name       = [self.machOFile stringAtAddress:objc2Ivar.name];
                NSString *typeString = [self.machOFile stringAtAddress:objc2Ivar.type];
                CDMachOFileDataCursor *offsetCursor = [[CDMachOFileDataCursor alloc] initWithFile:self.machOFile address:objc2Ivar.offset];
                NSUInteger offset = (uint32_t)[offsetCursor readPtr]; // objc-runtime-new.h: "offset is 64-bit by accident" => restrict to 32-bit
                
                CDOCInstanceVariable *ivar = [[CDOCInstanceVariable alloc] initWithName:name typeString:typeString offset:offset];
                [ivars addObject:ivar];
            }
        }
    }
    return ivars;
}

根据代码可以看到,objc2ClassData.ivars指向了一个cd_objc2_list_header加上cd_objc2_list_header.count个cd_objc2_ivar的区域。cd_objc2_list_header之前都出现过就不做解释了,cd_objc2_ivar代表了一个实例变量,最后转化为class-dump的CDOCInstanceVariable对象,方法返回一个CDOCInstanceVariable对象的数组。

四、

加载完aClass.instanceVariables之后,需要给aClass.isExported字段赋值

    {
        CDSymbol *classSymbol = [[self.machOFile symbolTable] symbolForClassName:str];
        
        if (classSymbol != nil)
            aClass.isExported = [classSymbol isExternal];
    }

能够明白是根据类名str在符号表中找到对应项,在isExternal方法中判断CDSymbol的nlist(nlist_64结构,系统定义)n_type是否定义了N_EXT,但是不明白isExported的意义。

五、

接着是给aClass.superClassName赋值,这一段稍微复杂一些

    {
        uint64_t classNameAddress = address + [self.machOFile ptrSize];
        
        if ([self.machOFile hasRelocationEntryForAddress2:classNameAddress]) {
            [aClass setSuperClassName:[self.machOFile externalClassNameForAddress2:classNameAddress]];
            //NSLog(@"class: got external class name (2): %@", [aClass superClassName]);
        } else if ([self.machOFile hasRelocationEntryForAddress:classNameAddress]) {
            [aClass setSuperClassName:[self.machOFile externalClassNameForAddress:classNameAddress]];
            //NSLog(@"class: got external class name (1): %@", [aClass superClassName]);
        } else if (objc2Class.superclass != 0) {
            CDOCClass *sc = [self loadClassAtAddress:objc2Class.superclass];
            [aClass setSuperClassName:[sc name]];
        }
    }

classNameAddress指针取得是objc2Class.superClass的地址。先看第二和第三个判断。

第二个判断是在我们之前加载的重定义符号表中找到对应的CDRelocationInfo对象,然后返回CDRelocationInfo对应的CDSymbol对象的name的string赋值给aClass.superClassName。

第三个判断是直接按CDOCClass的方式从objc2Class.superclass处获得一个对象,并取CDOCClass的name作为aClass.superClassName

第一个判断涉及到了CDLCDyldInfo,回到CDLoadCommand的静态方法loadCommandWithDataCursor:,看到CDLCDyldInfo对应的是LC_DYLD_INFO/LC_DYLD_INFO_ONLY load command(动态库链接信息),对应的系统结构是dyld_info_command

struct dyld_info_command {
   uint32_t   cmd;		/* LC_DYLD_INFO or LC_DYLD_INFO_ONLY */
   uint32_t   cmdsize;		/* sizeof(struct dyld_info_command) */

    uint32_t   rebase_off;	/* file offset to rebase info  */
    uint32_t   rebase_size;	/* size of rebase info   */
    
    uint32_t   bind_off;	/* file offset to binding info   */
    uint32_t   bind_size;	/* size of binding info  */
        
    uint32_t   weak_bind_off;	/* file offset to weak binding info   */
    uint32_t   weak_bind_size;  /* size of weak binding info  */
    
    uint32_t   lazy_bind_off;	/* file offset to lazy binding info */
    uint32_t   lazy_bind_size;  /* size of lazy binding infs */

    uint32_t   export_off;	/* file offset to lazy binding info */
    uint32_t   export_size;	/* size of lazy binding infs */
};

字段以offset和size成对出现,分别指向了基地址重定位(Rebase)、绑定数据、弱绑定数据、延时绑定数据和导出符号数据

  1. 基地址重定位。
    在对程序进行编译链接时会为生成的可执行文件或者动态库指定一个默认的虚拟基地址,后续所有生成的代码中的绝对地址值都是基于这个虚拟基地址来构建的。这个虚拟基地址就是__TEXT的结构体struct segment_command中的vmaddr字段,一般情况下可执行程序的默认基地址都是0x100000000。
    但是每次程序加载到内存的基地址是不一样的,是一个随机值。因此程序加载的真实基地址和程序生成时的基地址值之间就有一个slide值,也就是地址差值。这时候就需要在程序运行时对以虚拟地址为基础的地址值进行基地址重定位处理。
    具体做法:根据rebase_off和rebase_size找到基地址重定位区域,指针先读一个字节8位,高4位表示命令名,低4位是一个立即数
    #define REBASE_OPCODE_MASK   0xF0
    #define REBASE_IMMEDIATE_MASK  0x0F
    
    const uint8_t *ptr = start;
    uint8_t immediate = *ptr & REBASE_IMMEDIATE_MASK;
    uint8_t opcode = *ptr & REBASE_OPCODE_MASK;

    根据命令名opcode进行不同的处理来完成基地址重定位,介绍可见MAC系统中可执行文件格式(Mach-O)的学习(四)
    需要理解的是完成一次基地址重定位是多组opcode+immediate来完成的,最后用REBASE_OPCODE_DONE的opcode来表示基地址重定位的结束

  2. 绑定数据。
    将二进制调用的外部符号进行绑定的过程。 比如我们objc代码中需要使用到NSObject, 即符号_OBJC_CLASS_$_NSObject,但是这个符号又不在我们的二进制中,在系统库 Foundation.framework中,因此就需要binding这个操作将对应关系绑定到一起。
    具体做法:根据bind_off和bind_size找到绑定数据区域,指针先读一个字节8位,高4位表示命令名,低4位是一个立即数
    #define BIND_OPCODE_MASK					0xF0
    #define BIND_IMMEDIATE_MASK					0x0F
    
    const uint8_t *ptr = start;
    uint8_t immediate = *ptr & BIND_IMMEDIATE_MASK;
    uint8_t opcode = *ptr & BIND_OPCODE_MASK;
    这里看一下opcode的操作:BIND_OPCODE_DONE
  3. 弱绑定数据、延时绑定数据和绑定数据的操作是一样的,包括opcode的定义和使用。
    他们具体的含义不是很懂,查到的说法:弱绑定表,该表中的符号,如果和动态库中的符号冲突时,用弱绑定表的符号;延迟加载表。延迟加载,对于一些符号,我们不必在装载时进行重定位,这样会加快程序的启动速度
  4. 导出符号数据。同样不是很清晰用法,查到的说法是:暴露给外界的符号表偏移。

还记得第二篇里提到了,完成了CDLoadCommand子类的构建后调用了CDLCDyldInfo的machOFileDidReadLoadCommands:方法,当时没有分析,现在来看看machOFileDidReadLoadCommands:中的处理

- (void)machOFileDidReadLoadCommands:(CDMachOFile *)machOFile;
{
    [self parseBindInfo];
    [self parseWeakBindInfo];
}

这说明其实class-dump只关心绑定数据和弱绑定数据,毕竟基地址重定位、延时绑定数据都是运行时的东西。

而parseBindInfo和parseWeakBindInfo的实现其实就是前面<2.绑定数据>中的logBindOps:end:endisLazy:方法。

综上,第一个判断其实就是在绑定数据表里面找aClass的aClass.superClassName

至于为什么采用这种判断顺序(绑定数据表->重定义符号表->直接按类读取),应该和dyld的执行逻辑有关了(应该是个天坑)。

六、

接着aClass.classMethods使用了loadMethodsOfMetaClassAtAddress:方法从objc2Class.isa获取类方法

这里又涉及一个知识点,实例对象的isa指针指向了这个对象的类对象,那么这个类对象(也就是我们的objc2Class)的isa指针指向什么呢?回答是指向了元类对象(meta-class),meta-class元类对象与class类对象,具有相同的结构,只不过存储的信息不同,并且元类对象的isa指针指向基类的元类对象,基类的元类对象的isa指针指向自己。元类对象的superclass指针指向其父类的元类对象,基类的元类对象的superclass指针指向其类对象。

【iOS】class-dump源码学习(三)_第1张图片 的确很直白的一张图。

所以上面所说的loadMethodsOfMetaClassAtAddress:方法其实就是根据objc2Class.isa获得了objc2Class的元类对象,不过因为他们结构相同,所以class-dump还是使用cd_objc2_class存储信息。

由于只需要objc2Class的类方法,因此只需要返回(((元类对象的data字段指向的cd_objc2_class_ro_t结构)的baseMethods指针)指向的方法列表)转存成的(CDOCMethod数组)就可以了。

七、

最后aClass.protocols、aClass.properties分别对应objc2ClassData.baseProtocol、objc2ClassData.baseProperties,通过指针和对应方法获得了赋值。(protocolAddressListAtAddress:和loadPropertiesAtAddress:方法在前面协议加载时都已经分析过了这里不再赘述)

至此CDObjectiveC2Processor的loadClassAtAddress:就分析完了

CDOCClass对象被分别保存在_classes数组和_classesByAddress字典(val作为键)中,loadClasses方法结束了。


这一篇内容先到这里,下一篇继续分析CDObjectiveC2Processor的loadCategories方法:【iOS】class-dump源码学习(四)

你可能感兴趣的:(iOS学习)