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

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

续上一篇简要介绍完load commands,开始分析class-dump对mach-o文件load commands区域的处理


回到CDMachOFile方法_readLoadCommands:count:。

- (void)_readLoadCommands:(CDMachOFileDataCursor *)cursor count:(uint32_t)count;
{
    NSMutableArray *loadCommands      = [[NSMutableArray alloc] init];
    NSMutableArray *dylibLoadCommands = [[NSMutableArray alloc] init];
    NSMutableArray *segments          = [[NSMutableArray alloc] init];
    NSMutableArray *runPaths          = [[NSMutableArray alloc] init];
    NSMutableArray *runPathCommands   = [[NSMutableArray alloc] init];
    NSMutableArray *dyldEnvironment   = [[NSMutableArray alloc] init];
    NSMutableArray *reExportedDylibs  = [[NSMutableArray alloc] init];
    
    for (uint32_t index = 0; index < count; index++) {
        CDLoadCommand *loadCommand = [CDLoadCommand loadCommandWithDataCursor:cursor];
        if (loadCommand != nil) {
            [loadCommands addObject:loadCommand];

            if (loadCommand.cmd == LC_VERSION_MIN_MACOSX)                        self.minVersionMacOSX = (CDLCVersionMinimum *)loadCommand;
            if (loadCommand.cmd == LC_VERSION_MIN_IPHONEOS)                      self.minVersionIOS = (CDLCVersionMinimum *)loadCommand;
            if (loadCommand.cmd == LC_DYLD_ENVIRONMENT)                          [dyldEnvironment addObject:loadCommand];
            if (loadCommand.cmd == LC_REEXPORT_DYLIB)                            [reExportedDylibs addObject:loadCommand];
            if (loadCommand.cmd == LC_ID_DYLIB)                                  self.dylibIdentifier = (CDLCDylib *)loadCommand;

            if ([loadCommand isKindOfClass:[CDLCSourceVersion class]])           self.sourceVersion = (CDLCSourceVersion *)loadCommand;
            else if ([loadCommand isKindOfClass:[CDLCDylib class]])              [dylibLoadCommands addObject:loadCommand];
            else if ([loadCommand isKindOfClass:[CDLCSegment class]])            [segments addObject:loadCommand];
            else if ([loadCommand isKindOfClass:[CDLCSymbolTable class]])        self.symbolTable = (CDLCSymbolTable *)loadCommand;
            else if ([loadCommand isKindOfClass:[CDLCDynamicSymbolTable class]]) self.dynamicSymbolTable = (CDLCDynamicSymbolTable *)loadCommand;
            else if ([loadCommand isKindOfClass:[CDLCDyldInfo class]])           self.dyldInfo = (CDLCDyldInfo *)loadCommand;
            else if ([loadCommand isKindOfClass:[CDLCRunPath class]]) {
                [runPaths addObject:[(CDLCRunPath *)loadCommand resolvedRunPath]];
                [runPathCommands addObject:loadCommand];
            }
        }
        //NSLog(@"loadCommand: %@", loadCommand);
    }
    _loadCommands      = [loadCommands copy];
    _dylibLoadCommands = [dylibLoadCommands copy];
    _segments          = [segments copy];
    _runPaths          = [runPaths copy];
    _runPathCommands   = [runPathCommands copy];
    _dyldEnvironment   = [dyldEnvironment copy];
    _reExportedDylibs  = [reExportedDylibs copy];

    for (CDLoadCommand *loadCommand in _loadCommands) {
        [loadCommand machOFileDidReadLoadCommands:self];
    }
}

首先初始化一系列保存各种loadCommonds的数组

然后调用CDLoadCommand的loadCommandWithDataCursor:方法来生成相应的load command对象(也就是CDLoadCommand的各种子类)。

loadCommandWithDataCursor:的逻辑比较简单,先读取(准确来说是peek而不是read,指针并没有往前移动)一个Int32作为cmd的值,然后根据cmd调用对应的CDLoadCommand子类对象的初始化方法initWithDataCursor:,在子类的初始化方法中根据相应的load command结构读取字段,从而完成了一个load command的读取。(segment中的section以此类推处理)

//上述流程的代码展示
//CDMachOFile.m
CDLoadCommand *loadCommand = [CDLoadCommand loadCommandWithDataCursor:cursor];
//CDLoadCommand.m
+ (id)loadCommandWithDataCursor:(CDMachOFileDataCursor *)cursor
{
    Class targetClass = [CDLCUnknown class];
    uint32_t val = [cursor peekInt32];
    switch (val) {
        case LC_SEGMENT: 
            targetClass = [CDLCSegment32 class]; break;
        ...
    };
    return [[targetClass alloc] initWithDataCursor:cursor];
}
//CDLCSegment32.m
- (id)initWithDataCursor:(CDMachOFileDataCursor *)cursor;
{
     if ((self = [super initWithDataCursor:cursor])) {
        _segmentCommand.cmd     = [cursor readInt32];
        _segmentCommand.cmdsize = [cursor readInt32];
        
        [cursor readBytesOfLength:16 intoBuffer:_segmentCommand.segname];
        _segmentCommand.vmaddr   = [cursor readInt32];
        _segmentCommand.vmsize   = [cursor readInt32];
        _segmentCommand.fileoff  = [cursor readInt32];
        _segmentCommand.filesize = [cursor readInt32];
        _segmentCommand.maxprot  = [cursor readInt32];
        _segmentCommand.initprot = [cursor readInt32];
        _segmentCommand.nsects   = [cursor readInt32];
        _segmentCommand.flags    = [cursor readInt32];
        {
            char buf[17]; 
            memcpy(buf, _segmentCommand.segname, 16);
            buf[16] = 0;
            NSString *str = [[NSString alloc] initWithBytes:buf length:strlen(buf) encoding:NSASCIIStringEncoding];
            [self setName:str];
        }
        NSMutableArray *_sections = [[NSMutableArray alloc] init];
        for (NSUInteger index = 0; index < _segmentCommand.nsects; index++) {
            CDSection32 *section = [[CDSection32 alloc] initWithDataCursor:cursor segment:self];
            [_sections addObject:section];
        }
        self.sections = [_sections copy]; 
    }
    return self;
}

回到_readLoadCommands:count:,完成了CDLoadCommand子类的构建后,再根据其cmd进行分类放入不同数组中。

最后调用了CDLoadCommand的machOFileDidReadLoadCommands:方法,实际上是调用了CDLCDyldInfo的对应方法(其他子类都没有重写这个方法,父类这个方法的不执行任何代码)

CDLCDyldInfo对应的load command是LC_DYLD_INFO_ONLY/LC_DYLD_INFO,记录具体的链接器需要的信息,比如重定向,懒加载,绑定等。machOFileDidReadLoadCommands:中的处理暂时不做分析

至此_readLoadCommands:count:方法结束,CDMachOFile的初始化也完成了。

这也意味着对于class-dump源码的分析,初始化这一步已经完成了,数据都已经转成了相应类和对象保存了起来。

(其实在load commands这一块还有无数的知识点,不过先暂时挂起,之后涉及到或者是有时间再分析)

再次回到class-dump.m,紧接着[classDump loadFile:file error:&error]往下看,接下来就应该是开始数据的处理了

看到loadFile:如果返回成功则有以下调用

[classDump processObjectiveCData];
[classDump registerTypes];

首先关注一下CDClassDump的processObjectiveCData方法。

- (void)processObjectiveCData;
{
    for (CDMachOFile *machOFile in self.machOFiles) {
        CDObjectiveCProcessor *processor = [[[machOFile processorClass] alloc] initWithMachOFile:machOFile];
        [processor process];
        [_objcProcessors addObject:processor];
    }
}

比较好理解,以mach-o文件为单位进行处理,处理的方法封装成了类CDObjectiveCProcessor

这里有个小细节,根据mach-o文件是否包含section(__Data,__objc_imageinfo),会生成CDObjectiveC2Processor(包含)或者是CDObjectiveC1Processor而非基类CDObjectiveCProcessor。

CDObjectiveCProcessor的初始化initWithMachOFile:没有文件的处理操作,略过。


关注CDObjectiveCProcessor的process方法

- (void)process;
{
    if (self.machOFile.isEncrypted == NO && self.machOFile.canDecryptAllSegments) {
        [self.machOFile.symbolTable loadSymbols];
        [self.machOFile.dynamicSymbolTable loadSymbols];

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

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

有两个进入的逻辑:

第一个字段machOFile.isEncrypted是通过判断mach-o文件中的LC_ENCRYPTION_INFO/LC_ENCRYPTION_INFO_64指令(对应系统结构encryption_info_command/encryption_info_command_64,对应class-dump类CDLCEncryptionInfo)的cryptid字段是否为0来决定的,如果是0则isEncrypted返回NO

第二个字段machOFile.canDecryptAllSegments就是检查mach-o文件中的所有segment指令的canDecrypt,如果有一个segment的canDecrypt为NO则canDecryptAllSegments返回NO

- (BOOL)canDecryptAllSegments;
{
    for (CDLoadCommand *loadCommand in _loadCommands) {
        if ([loadCommand isKindOfClass:[CDLCSegment class]] && [(CDLCSegment *)loadCommand canDecrypt] == NO)
            return NO;
    }
    return YES;
}

canDecrypt是通过判断segment的encryptionType是否为定义的三种encryptionType进行返回的

- (BOOL)canDecrypt;
{
    CDSegmentEncryptionType encryptionType = self.encryptionType;

    return (encryptionType == CDSegmentEncryptionType_None)
        || (encryptionType == CDSegmentEncryptionType_AES)
        || (encryptionType == CDSegmentEncryptionType_Blowfish);
}

那segment的encryptionType如何定义?先判断segment的flag字段是否有SG_PROTECTED_VERSION_1位是否为1,如果不是则直接返回CDSegmentEncryptionType_None。进一步的判断直接看代码吧。

- (CDSegmentEncryptionType)encryptionType;
{
    if (self.isProtected) {
        if (self.filesize <= 3 * PAGE_SIZE) {
            // First three pages aren't encrypted, so we can't tell.  Let's pretent it's something we can decrypt.
            return CDSegmentEncryptionType_AES;
        } else {
            const void *src = (uint8_t *)[self.machOFile.data bytes] + self.fileoff + 3 * PAGE_SIZE;

            uint32_t magic = OSReadLittleInt32(src, 0);
            //NSLog(@"%s, magic= 0x%08x", __cmd, magic);
            switch (magic) {
                case CDSegmentProtectedMagic_None:     return CDSegmentEncryptionType_None;
                case CDSegmentProtectedMagic_AES:      return CDSegmentEncryptionType_AES;
                case CDSegmentProtectedMagic_Blowfish: return CDSegmentEncryptionType_Blowfish;
            }

            return CDSegmentEncryptionType_Unknown;
        }
    }

    return CDSegmentEncryptionType_None;
}

回到process方法,当mach-o文件不加密且mach-o文件所有segment都可以解密的情况下才会进一步处理

下一步是加载符号表:

[self.machOFile.symbolTable loadSymbols];
[self.machOFile.dynamicSymbolTable loadSymbols];

先看CDLCSymbolTable(对应LC_SYMTAB指令,symtab_command结构)的loadSymbols

首先找到segment指令中initprot字段设置为VM_PROT_READ|VM_PROT_WRITE的,记录下其vmaddr。(目的?)

struct symtab_command {
	uint32_t	cmd;		/* LC_SYMTAB */
	uint32_t	cmdsize;	/* sizeof(struct symtab_command) */
	uint32_t	symoff;		/* symbol table offset */
	uint32_t	nsyms;		/* number of symbol table entries */
	uint32_t	stroff;		/* string table offset */
	uint32_t	strsize;	/* string table size in bytes */
};

根据symtab_command的symoff字段(CDLCSymbolTable中没有另外存储LC_SYMTAB指令的字段,而是直接持有了一个symtab_command结构),指针cursor找到了符号表起始位置。

根据symtab_command的stroff字段,指针strtab找到了字符串表的起始位置。

接着开始遍历符号表(以32位为例,主要区别是nlist的结构、n_value的读取、CDSymbol的初始化方法)

for (uint32_t index = 0; index < _symtabCommand.nsyms; index++) {
    struct nlist nlist;

    nlist.n_un.n_strx = [cursor readInt32];
    nlist.n_type      = [cursor readByte];
    nlist.n_sect      = [cursor readByte];
    nlist.n_desc      = [cursor readInt16];
    nlist.n_value     = [cursor readInt32];

    const char *ptr = strtab + nlist.n_un.n_strx;
    NSString *str = [[NSString alloc] initWithBytes:ptr length:strlen(ptr) encoding:NSASCIIStringEncoding];

    CDSymbol *symbol = [[CDSymbol alloc] initWithName:str machOFile:self.machOFile nlist32:nlist];
    [symbols addObject:symbol];

    if ([str hasPrefix:ObjCClassSymbolPrefix] && symbol.value != 0) {
        NSString *className = [str substringFromIndex:[ObjCClassSymbolPrefix length]];
        classSymbols[className] = symbol;
    }
}

nlist/nlist_64结构代表了符号表中的一条入口记录

【iOS】class-dump源码学习(二)_第1张图片 nlist在MachOView的Symbol Table表现

读取了nlist之后,通过nlist的n_un.n_strx在指针strtab的基础上找到了在字符串表中对应该符号表的str

取得了nlist和str之后,就可以构建nlist对应的class-dump对象CDSymbol了。CDLCSymbolTable持有了CDSymbol数组symbols

CDSymbol可以理解为就是nlist/nlist_64结构。

完成了CDSymbol的构建之后,看代码的意思是如果str包含_OBJC_CLASS_$_并且nlist的value并不为0,该符号就是一个OC类符号,按<键:类名,值:CDSymbol>的方式存在字典classSymbols中。

至此CDLCSymbolTable的loadSymbols完成。


再看CDLCDynamicSymbolTable(对应LC_DYSYMTAB指令,dysymtab_command结构)的loadSymbols

struct dysymtab_command {
    uint32_t cmd;	/* LC_DYSYMTAB */
    uint32_t cmdsize;	/* sizeof(struct dysymtab_command) */

    uint32_t ilocalsym;	/* index to local symbols */
    uint32_t nlocalsym;	/* number of local symbols */

    uint32_t iextdefsym;/* index to externally defined symbols */
    uint32_t nextdefsym;/* number of externally defined symbols */

    uint32_t iundefsym;	/* index to undefined symbols */
    uint32_t nundefsym;	/* number of undefined symbols */

    uint32_t tocoff;	/* file offset to table of contents */
    uint32_t ntoc;	/* number of entries in table of contents */

    uint32_t modtaboff;	/* file offset to module table */
    uint32_t nmodtab;	/* number of module table entries */

    uint32_t extrefsymoff;	/* offset to referenced symbol table */
    uint32_t nextrefsyms;	/* number of referenced symbol table entries */

    uint32_t indirectsymoff; /* file offset to the indirect symbol table */
    uint32_t nindirectsyms;  /* number of indirect symbol table entries */

    uint32_t extreloff;	/* offset to external relocation entries */
    uint32_t nextrel;	/* number of external relocation entries */

    uint32_t locreloff;	/* offset to local relocation entries */
    uint32_t nlocrel;	/* number of local relocation entries */

};

dysymtab_command结构字段有点多,逐一看看 参考

1.ilocalsym、iextdefsym、iundefsym把符号表分为三个区域,ilocalsym 本地符号仅用于调试,iextdefsym可执行文件定义的符号,iundefsym可执行文件里没有定义的 
2.tocoff,目录偏移,该内容只有在动态分享库中存在。主要作用是把符号和定义它的模块对应起来。

3.modtaboff:为了支持“模块”(整个对象文件)的动态绑定,符号表必须知道文件创建的模块。该内容只有在动态分享库中存在。

4.extrefsymoff:为了支持动态绑定模块,每个模块都有一个引用符号表,符号表里存放着每个模块所引用的符号(定义的和没有定义的)。该表指针动态库中存在。

5.indirectsymoff:如果 section 中有符号指针或者桩(stub),section中的reserved1存放该表的下标。间接符号表,只是存放一些32位小标,这些下标执行符号表。

6.extreloff:每个模块都有一个重定位外部符号表。仅在动态库中存在

7.locreloff:重定位本地符号表,由于只是在调试中用,所以不必增加模块分组。

其实都不是很理解。先记录下来

因为class-dump用到了extreloff和nextrel,了解一下。

extreloff和nextrel声明了重定位外部符号表的位置和大小

重定位外部符号表是section区使用的

回忆一下本文开头,在CDLCSegment32的初始化中,最后涉及到了CDSection32的初始化,当时没有深入解释,现在我们通过section结构看一下CDSection32/CDSection64(对应系统结构section/section_64,是跟在LC_SEGMENT/LC_SEGMENT_64指令后面的section header包含的内容)初始化了些什么:

struct section { /* for 32-bit architectures */
	char		sectname[16];	/* name of this section */
	char		segname[16];	/* segment this section goes in */
	uint32_t	addr;		/* memory address of this section */
	uint32_t	size;		/* size in bytes of this section */
	uint32_t	offset;		/* file offset of this section */
	uint32_t	align;		/* section alignment (power of 2) */
	uint32_t	reloff;		/* file offset of relocation entries */
	uint32_t	nreloc;		/* number of relocation entries */
	uint32_t	flags;		/* flags (section type and attributes)*/
	uint32_t	reserved1;	/* reserved (for offset or index) */
	uint32_t	reserved2;	/* reserved (for count or sizeof) */
};

需要理解的是section结构其实不是section自身,而是一个可以描述并指向section内存的一个section header

在section结构中的reloff(重定位表里的偏移)和nreloc(几个需要重定位的符号)指明了重定位外部符号表中的重定位记录relocation_info,通过relocation_info的r_address和r_length字段可以定位section中的指定地址及长度下的内容,这一部分将被替换成relocation_info的r_symbolnum中的内容,r_symbolnum指向的是符号表里面的index

根据dysymtab_command的extreloff字段(CDLCDynamicSymbolTable中也没有另外存储LC_DYSYMTAB指令的字段,而是直接持有了一个dysymtab_command结构),指针cursor找到了重定位外部符号表起始位置。

接着开始遍历重定位外部符号表。

for (uint32_t index = 0; index < _dysymtab.nextrel; index++) {
    struct relocation_info rinfo;

    rinfo.r_address = [cursor readInt32];
    uint32_t val    = [cursor readInt32];

    rinfo.r_symbolnum = val & 0x00ffffff;
    rinfo.r_pcrel     = (val & 0x01000000) >> 24;
    rinfo.r_length    = (val & 0x06000000) >> 25;
    rinfo.r_extern    = (val & 0x08000000) >> 27;
    rinfo.r_type      = (val & 0xf0000000) >> 28;

    CDRelocationInfo *ri = [[CDRelocationInfo alloc] initWithInfo:rinfo];
    [externalRelocationEntries addObject:ri];
}

构造好了relocation_info结构rinfo,就可以构建relocation_info对应的class-dump对象CDRelocationInfo了。CDLCDynamicSymbolTable持有了CDRelocationInfo数组externalRelocationEntries。

至此CDLCDynamicSymbolTable的loadSymbols完成。


暂停一下,先想一下为什么class-dump需要加载符号表和重定位符号表——》

通过加载符号表和重定位符号表,现在我就可以理解section里面的地址究竟指向哪个符号了


这一篇内容先到这里,下一篇继续:【iOS】class-dump源码学习(三)

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