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

iOS开发之runtime(12):关于__objc_init_

2019-01-08  本文已影响14人  kyson老师

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

runtime logo

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

背景

上一篇文章中我们说道,static_init方法使用到了区__objc_init_func区的数据,那么

本文将给出详细分析。

分析

接着上一篇文章的Mach-O的文件构成的概念,我们继续深入讲解Mach-O的构成,首先还是那张图:


mach-o

与上一篇文章中的图不一样的地方在于,后面给了一个实例,那这个示例是怎么样显示出来的呢,这里不得不提到一个优秀的查看Mach-O文件内部构成的软件:MachView

MachView
MachOView工具可Mac平台中可查看MachO文件格式信息,IOS系统中可执行程序属于Mach-O文件格式,有必要介绍如何利用工具快速查看Mach-O文件格式。MachOView工具属于免费开源项目。
git 地址:
https://git.code.sf.net/p/machoview/code

具体的使用流程这里不赘述了,文章末尾给出了几个参考地址,可以浏览一下。

Header
Header 中记录了 Mach-O 文件的属性信息,其数据结构定义在 loader.h中,分为32位以及64位:

32位:

/*
* The 32-bit mach header appears at the very beginning of the object file for
* 32-bit architectures.
*/
struct mach_header {
uint32_t    magic;  /* mach magic number identifier */
cpu_type_t  cputype;    /* cpu specifier */
cpu_subtype_t   cpusubtype; /* machine specifier */
uint32_t    filetype;   /* type of file */
uint32_t    ncmds;  /* number of load commands */
uint32_t    sizeofcmds; /* the size of all the load commands */
uint32_t    flags;  /* flags */
};
/* Constant for the magic field of the mach_header (32-bit architectures) */
#define MH_MAGIC    0xfeedface  /* the mach magic number */
#define MH_CIGAM    0xcefaedfe  /* NXSwapInt(MH_MAGIC) */

64位:

/*
* The 64-bit mach header appears at the very beginning of object files for
* 64-bit architectures.
*/
struct mach_header_64 {
uint32_t    magic;  /* mach magic number identifier */
cpu_type_t  cputype;    /* cpu specifier */
cpu_subtype_t   cpusubtype; /* machine specifier */
uint32_t    filetype;   /* type of file */
uint32_t    ncmds;  /* number of load commands */
uint32_t    sizeofcmds; /* the size of all the load commands */
uint32_t    flags;  /* flags */
uint32_t    reserved;   /* reserved */
};
/* Constant for the magic field of the mach_header_64 (64-bit architectures) */
#define MH_MAGIC_64 0xfeedfacf /* the 64-bit mach magic number */
#define MH_CIGAM_64 0xcffaedfe /* NXSwapInt(MH_MAGIC_64) */

这里每个字段的意思如下:


字段介绍

因此简单总结一下就是 Headers 能帮助校验 Mach-O 合法性和定位文件的运行环境。

Load commands
Load commands 紧随在 Header 后,它包含了一系列的加载命令,目的是向操作系统描述如何处理 Mach-O 文件。
Load Commands 是跟在 Header 后面的加载命令区,所有 commands 的大小总和即为 Header->sizeofcmds 字段,共有 Header->ncmds 条加载命令。

struct load_command {
    uint32_t cmd;        /* type of load command */
    uint32_t cmdsize;    /* total size of command in bytes */
};

Segment
Mach-O 文件有多个段(Segment),每个段有不同的功能。然后每个段又分为很多小的 Section。 LC_SEGMENT 意味着这部分文件需要映射到进程的地址空间去。一般有以下段名:
__PAGEZERO: 空指针陷阱段,映射到虚拟内存空间的第一页,用于捕捉对 NULL 指针的引用。
__TEXT: 包含了执行代码以及其他只读数据。该段数据可以 VM_PROT_READ(读)、VM_PROT_EXECUTE(执行),不能被修改。
__DATA: 程序数据,该段可写 VM_PROT_WRITE/READ/EXECUTE。
__LINKEDIT: 链接器使用的符号以及其他表。

struct segment_command { /* for 32-bit architectures */
    uint32_t    cmd;        /* LC_SEGMENT */
    uint32_t    cmdsize;    /* includes sizeof section structs */
    char        segname[16];    /* segment name */
    uint32_t    vmaddr;     /* memory address of this segment 段的虚拟内存地址*/
    uint32_t    vmsize;     /* memory size of this segment  段的虚拟内存大小*/
    uint32_t    fileoff;    /* file offset of this segment  段在文件中的偏移量*/
    uint32_t    filesize;   /* amount to map from the file  段在文件中的大小*/
    vm_prot_t   maxprot;    /* maximum VM protection */
    vm_prot_t   initprot;   /* initial VM protection */
    uint32_t    nsects;     /* number of sections in segment */
    uint32_t    flags;      /* flags */
};
struct segment_command_64 { /* for 64-bit architectures */
    uint32_t    cmd;        /* LC_SEGMENT_64 */
    uint32_t    cmdsize;    /* includes sizeof section_64 structs */
    char        segname[16];    /* segment name */
    uint64_t    vmaddr;     /* memory address of this segment */
    uint64_t    vmsize;     /* memory size of this segment */
    uint64_t    fileoff;    /* file offset of this segment */
    uint64_t    filesize;   /* amount to map from the file */
    vm_prot_t   maxprot;    /* maximum VM protection */
    vm_prot_t   initprot;   /* initial VM protection */
    uint32_t    nsects;     /* number of sections in segment */
    uint32_t    flags;      /* flags */
};

其中 nsects 字段就是表明该段中有多少个 section。文件映射的起始位置是由 fileoff 给出,映射到地址空间的 vmaddr 处。

Section
Section 是具体有用的数据存放的地方。它的结构体跟随在 LC_SEGMENT 结构体之后,LC_SEGMENT 又在 Load Commands 中,但是 segment 的数据内容是跟在 Load Commands 之后的。它的结构体为:

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) */
};
struct section_64 { /* for 64-bit architectures */
    char        sectname[16];   /* name of this section */
    char        segname[16];    /* segment this section goes in */
    uint64_t    addr;       /* memory address of this section */
    uint64_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) */
    uint32_t    reserved3;  /* reserved */
};

其中 flag 字段分为两个部分,一个是区域类型(section type),一个是区域属性(section attributes)。其中 type 是互斥的,即只能有一个类型,而 attributes 不是互斥的,可以有多个属性。如果段(segment)中的任何一个 section 拥有属性 S_ATTR_DEBUG,那么该段所有的 section 都必须拥有这个属性。具体的flag字段内容以及意义请参考 /usr/include/mach-o/loader.h。

段名为大写,节名为小写。各节的作用主要有:

__text: 主程序代码
__stub_helper: 用于动态链接的存根
__symbolstub1: 用于动态链接的存根
__objc_methname: Objective-C 的方法名
__objc_classname: Objective-C 的类名
__cstring: 硬编码的字符串

__lazy_symbol: 懒加载,延迟加载节,通过 dyld_stub_binder 辅助链接
_got: 存储引用符号的实际地址,类似于动态符号表
__nl_symbol_ptr: 非延迟加载节
__mod_init_func: 初始化的全局函数地址,在 main 之前被调用
__mod_term_func: 结束函数地址
__cfstring: Core Foundation 用到的字符串(OC字符串)

__objc_clsslist: Objective-C 的类列表
__objc_nlclslist: Objective-C 的 +load 函数列表,比 __mod_init_func 更早执行
__objc_const: Objective-C 的常量
__data: 初始化的可变的变量
__bss: 未初始化的静态变量

这里我们并没有看到__objc_init_func,而是只有类似的__mod_init_func区。那么__objc_init_func是哪里来的呢?

全局搜索一下__objc_init_func,可以发现在markgc.cpp中有如下代码:

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");
    }
}

这段方法凭借我们读者的慧眼肯定能轻易看出,作用是将区__mod_init_func替换成__objc_init_func
瞬间豁然开朗。

那么有会有朋友问,该markgc.cpp文件是何时被调用,并被写进runtime库的,这个又是一段故事了,请听笔者下回分解。

最后我们写个Demo 来设置区数据,并获取区数据:

#import <Foundation/Foundation.h>
#import <dlfcn.h>
#include <mach-o/loader.h>
#include <mach-o/getsect.h>

#ifndef __LP64__
#define mach_header mach_header
#else
#define mach_header mach_header_64
#endif

const struct mach_header *machHeader = NULL;
static NSString *configuration = @"";
//设置"__DATA,__customSection"的数据为kyson
char *kString __attribute__((section("__DATA,__customSection"))) = (char *)"kyson";

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //设置machheader信息
        if (machHeader == NULL)
        {
            Dl_info info;
            dladdr((__bridge const void *)(configuration), &info);
            machHeader = (struct mach_header_64*)info.dli_fbase;
        }

        unsigned long byteCount = 0;
        uintptr_t* data = (uintptr_t *) getsectiondata(machHeader, "__DATA", "__customSection", &byteCount);
        NSUInteger counter = byteCount/sizeof(void*);
        for(NSUInteger idx = 0; idx < counter; ++idx)
        {
            char *string = (char*)data[idx];
            NSString *str = [NSString stringWithUTF8String:string];
            NSLog(@"%@",str);
        }

    }
    return 0;
}

参考

Macho文件浏览器---MachOView
探秘 Mach-O 文件
解读 Mach-O 文件格式

上一篇下一篇

猜你喜欢

热点阅读