iOS开发之深入理解runtimeRuntime源码iOS

iOS开发之runtime(14):markgc.cpp源码分析

2019-01-12  本文已影响19人  kyson老师

本系列博客是本人的源码阅读笔记,如果有iOS开发者在看runtime的,欢迎大家多多交流。为了方便讨论,本人新建了一个微信群(iOS技术讨论群),想要加入的,请添加本人微信:zhujinhui207407,【加我前请备注:ios 】,本人博客http://www.kyson.cn 也在不停的更新中,欢迎一起讨论

runtime logo

本文完整版详见笔者小专栏:https://xiaozhuanlan.com/runtime

背景

上一篇文章中,我们分析了markgc.cpp在runtime编译过程中的作用:将mod_init_func的区名改为了__objc_init_func。但是我们没有具体分析这个文件的替换原理。今天,我们就来分析一下markgc.cpp:

markgc.cpp

通过本文,您将知道:

分析

之前的文章中我们已经说过了,section其实是在segment中的,我们再通过machview看一下runtime库的文件构成:


runtime文件

因此如果我们想要替换mod_init_func的section名改为__objc_init_func,思路应该是先遍历并找到mod_init_func,然后通过方法sectionname将section名字进行更改。
按照此思路,我们从头到尾开始分析一下markgc.cpp:
首先是main方法:

int main(int argc, const char *argv[]) {
    for (int i = 1; i < argc; ++i) {
        if (!processFile(argv[i])) return 1;
    }
    return 0;
}

由上一篇文章我们可知,传入的参数是库文件的文件名(连同路径)。
拿到文件后,调用了方法processFile,看名字可知这是要处理这个文件了。

bool processFile(const char *filename)
{
    if (debug) printf("file %s\n", filename);
//打开文件
    int fd = open(filename, O_RDWR);
    if (fd < 0) {
        printf("open %s: %s\n", filename, strerror(errno));
        return false;
    }
    
    struct stat st;
//获取文件状态
    if (fstat(fd, &st) < 0) {
        printf("fstat %s: %s\n", filename, strerror(errno));
        return false;
    }
//将文件映射进内存进行处理
    void *buffer = mmap(NULL, (size_t)st.st_size, PROT_READ|PROT_WRITE, 
                        MAP_FILE|MAP_SHARED, fd, 0);
    if (buffer == MAP_FAILED) {
        printf("mmap %s: %s\n", filename, strerror(errno));
        return false;
    }
//开始处理进入内存后的文件
    bool result = parse_fat((uint8_t *)buffer, (size_t)st.st_size);
//解除映射关系
    munmap(buffer, (size_t)st.st_size);
    close(fd);
    return result;
}

上面代码笔者已经进行了部分注释,大概意思就是先将编译好的runtime库映射进内存进行处理,处理完后解除映射。至于这里为什么要读入内存,笔者知道的一个原因是mmap()对该内存区域的存取即是直接对该文件内容的读写。因此这样一来就省去了读取后再重新保存文件的做法。
在以上方法中有个函数调用是用来处理该文件的:

    bool result = parse_fat((uint8_t *)buffer, (size_t)st.st_size);

parse_fat,从文件名可以看出,是用来解析fat文件的。关于fat之前已经说过,fat文件是“胖文件”的意思,胖文件就是已经适配了各种架构的处理器(主要是i386和arm)。我们继续进入该方法进行分析:

bool parse_fat(uint8_t *buffer, size_t size)
{
    uint32_t magic;

    if (size < sizeof(magic)) {
        printf("file is too small\n");
        return false;
    }

    magic = *(uint32_t *)buffer;
    if (magic != FAT_MAGIC && magic != FAT_CIGAM) {
        /* Not a fat file */
        return parse_macho(buffer);
    } else {
        struct fat_header *fh;
        uint32_t fat_magic, fat_nfat_arch;
        struct fat_arch *archs;
        
        if (size < sizeof(struct fat_header)) {
            printf("file is too small\n");
            return false;
        }

        fh = (struct fat_header *)buffer;
        fat_magic = OSSwapBigToHostInt32(fh->magic);
        fat_nfat_arch = OSSwapBigToHostInt32(fh->nfat_arch);

        if (size < (sizeof(struct fat_header) + fat_nfat_arch * sizeof(struct fat_arch))) {
            printf("file is too small\n");
            return false;
        }

        archs = (struct fat_arch *)(buffer + sizeof(struct fat_header));

        /* Special case hidden CPU_TYPE_ARM64 */
        if (size >= (sizeof(struct fat_header) + (fat_nfat_arch + 1) * sizeof(struct fat_arch))) {
            if (fat_nfat_arch > 0
                && OSSwapBigToHostInt32(archs[fat_nfat_arch].cputype) == CPU_TYPE_ARM64) {
                fat_nfat_arch++;
            }
        }
        /* End special case hidden CPU_TYPE_ARM64 */

        if (debug) printf("%d fat architectures\n", 
                          fat_nfat_arch);

        for (uint32_t i = 0; i < fat_nfat_arch; i++) {
            uint32_t arch_cputype = OSSwapBigToHostInt32(archs[i].cputype);
            uint32_t arch_cpusubtype = OSSwapBigToHostInt32(archs[i].cpusubtype);
            uint32_t arch_offset = OSSwapBigToHostInt32(archs[i].offset);
            uint32_t arch_size = OSSwapBigToHostInt32(archs[i].size);

            if (debug) printf("cputype %d cpusubtype %d\n", 
                              arch_cputype, arch_cpusubtype);

            /* Check that slice data is after all fat headers and archs */
            if (arch_offset < (sizeof(struct fat_header) + fat_nfat_arch * sizeof(struct fat_arch))) {
                printf("file is badly formed\n");
                return false;
            }

            /* Check that the slice ends before the file does */
            if (arch_offset > size) {
                printf("file is badly formed\n");
                return false;
            }

            if (arch_size > size) {
                printf("file is badly formed\n");
                return false;
            }

            if (arch_offset > (size - arch_size)) {
                printf("file is badly formed\n");
                return false;
            }

            bool ok = parse_macho(buffer + arch_offset);
            if (!ok) return false;
        }
        return true;
    }
}

以上代码大部分都是为了去校验合法性,真正起作用的其实是方法调用:

bool ok = parse_macho(buffer + arch_offset);

可以看出,刚刚是解析胖文件,现在开始解析我们的mach-o文件了:

template<typename P>
bool parse_macho(uint8_t *buffer)
{
    macho_header<P>* mh = (macho_header<P>*)buffer;
    uint8_t *cmds = (uint8_t *)(mh + 1);
    for (uint32_t c = 0; c < mh->ncmds(); c++) {
        macho_load_command<P>* cmd = (macho_load_command<P>*)cmds;
        cmds += cmd->cmdsize();
        if (cmd->cmd() == LC_SEGMENT  ||  cmd->cmd() == LC_SEGMENT_64) {
            doseg(buffer, (macho_segment_command<P>*)cmd);
        }
    }

    return true;
}

由上一篇文章我们知道,通过结构体macho_header我们知道了命令的数目,通过结构体的ncmds的属性。我们在machview中也可以看到对应的展示:

mach_header信息展示
所以以上代码不难看出其实是开始遍历每个命令,并调用
doseg(buffer, (macho_segment_command<P>*)cmd);

方法:

template <typename P>
void doseg(uint8_t *start, macho_segment_command<P> *seg)
{
    if (debug) printf("segment name: %.16s, nsects %u\n",
                      seg->segname(), seg->nsects());
    macho_section<P> *sect = (macho_section<P> *)(seg + 1);
    for (uint32_t i = 0; i < seg->nsects(); ++i) {
        dosect(start, &sect[i]);
    }
}

仍然是遍历所有的sections,然后调用方法:

dosect(start, &sect[i]);

这里终于到了我们熟悉的替换sectionname的方法了:

template <typename P>
void dosect(uint8_t *start, macho_section<P> *sect)
{
    if (debug) printf("section %.16s from segment %.16s\n",
                      sect->sectname(), sect->segname());

    // Strip S_MOD_INIT/TERM_FUNC_POINTERS. We don't want dyld to call 
    // our init funcs because it is too late, and we don't want anyone to 
    // call our term funcs ever.
    if (segnameStartsWith(sect->segname(), "__DATA")  &&  
        sectnameEquals(sect->sectname(), "__mod_init_func"))
    {
        // section type 0 is S_REGULAR
        sect->set_flags(sect->flags() & ~SECTION_TYPE);
        sect->set_sectname("__objc_init_func");
        if (debug) printf("disabled __mod_init_func section\n");
    }
    if (segnameStartsWith(sect->segname(), "__DATA")  &&  
        sectnameEquals(sect->sectname(), "__mod_term_func"))
    {
        // section type 0 is S_REGULAR
        sect->set_flags(sect->flags() & ~SECTION_TYPE);
        sect->set_sectname("__objc_term_func");
        if (debug) printf("disabled __mod_term_func section\n");
    }
}

至此分析终于结束了。

总结

其实思路很简单,就是遍历segments,然后遍历sections,找到section __mod_init_func并将其名字改掉。画个图总结一下吧:

markgc流程图
上一篇下一篇

猜你喜欢

热点阅读