上一篇:【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:没有文件的处理操作,略过。
- (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];
首先找到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结构代表了符号表中的一条入口记录
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完成。
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源码学习(三)