ios深入-MACHO文件解析

ios深入-MACHO文件解析

发表于 2017-10-26 | 分类于 优化

导读
在分析linkMap文件的时候,遇到一个有趣的问题:获取类名可以用_objc_classname, 获取方法名可以用_objc_methname。可是怎么将方法名称和对象名称对应起来,程序是如何对应这两部分数据的。带着这个疑问研究了下macho的文件结构。

MACHO文件说明

macho文件是mac os或ios系统可执行文件的格式,系统通过加载这个格式来执行代码。

相关结构如图:

img

注:来源于:(http://www.jianshu.com/p/f1a61b53398f

具体每部分的含义可以参考这个定义:

mach-0 loader.h

这里简单讲几个我比较关注的:

注:下面都是以64位做演示说明,cpu结构为arm64。

MachO Header的结构

img

数据结构为:

/*
 * 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 */
};
  1. 第一个四字节数叫做magic number,可以得到使用的是64位还是32位系统
  2. 第二个字节和第三个字节是CPU类型
  3. 第四个字节是文件类型。MH_EXECUTE表示可执行文件
  4. 第五个字节和第六个字节表示load commands的个数和长度
  5. 第7个字节是加载的flag信息。具体参考loader.h中的文件

MachO load command

程序检索完Header之后就开始加载和解析Load Commands了。

相关代码在mach_loader.c,通过递归调用加载命令。

img

img

load_comand的数据结构为:

/*
 * The load commands directly follow the mach_header.  The total size of all
 * of the commands is given by the sizeofcmds field in the mach_header.  All
 * load commands must have as their first two fields cmd and cmdsize.  The cmd
 * field is filled in with a constant for that command type.  Each command type
 * has a structure specifically for it.  The cmdsize field is the size in bytes
 * of the particular load command structure plus anything that follows it that
 * is a part of the load command (i.e. section structures, strings, etc.).  To
 * advance to the next load command the cmdsize can be added to the offset or
 * pointer of the current load command.  The cmdsize for 32-bit architectures
 * MUST be a multiple of 4 bytes and for 64-bit architectures MUST be a multiple
 * of 8 bytes (these are forever the maximum alignment of any load commands).
 * The padded bytes must be zero.  All tables in the object file must also
 * follow these rules so the file can be memory mapped.  Otherwise the pointers
 * to these tables will not work well or at all on some machines.  With all
 * padding zeroed like objects will compare byte for byte.
 */
struct load_command {
    uint32_t cmd;       /* type of load command */
    uint32_t cmdsize;   /* total size of command in bytes */
};

每一个command都需要包含

  1. cmd:加载类型
  2. cmdsize:加载的大小

相关的最主要的解析源码在mach_loader.c里的parse_machfile方法里.最主要的代码如下:

            /*
             * Act on struct load_command's for which kernel
             * intervention is required.
             */
            switch(lcp->cmd) {
            case LC_SEGMENT:
                if (pass != 2)
                    break;

                if (abi64) {
                    /*
                     * Having an LC_SEGMENT command for the
                     * wrong ABI is invalid <rdar://problem/11021230>
                     */
                    ret = LOAD_BADMACHO;
                    break;
                }

                ret = load_segment(lcp,
                                   header->filetype,
                                   control,
                                   file_offset,
                                   macho_size,
                                   vp,
                                   map,
                                   slide,
                                   result);
                break;
            case LC_SEGMENT_64:
                if (pass != 2)
                    break;

                if (!abi64) {
                    /*
                     * Having an LC_SEGMENT_64 command for the
                     * wrong ABI is invalid <rdar://problem/11021230>
                     */
                    ret = LOAD_BADMACHO;
                    break;
                }

                ret = load_segment(lcp,
                                   header->filetype,
                                   control,
                                   file_offset,
                                   macho_size,
                                   vp,
                                   map,
                                   slide,
                                   result);
                break;
            case LC_UNIXTHREAD:
                if (pass != 1)
                    break;
                ret = load_unixthread(
                         (struct thread_command *) lcp,
                         thread,
                         slide,
                         result);
                break;
            case LC_MAIN:
                if (pass != 1)
                    break;
                if (depth != 1)
                    break;
                ret = load_main(
                         (struct entry_point_command *) lcp,
                         thread,
                         slide,
                         result);
                break;
            case LC_LOAD_DYLINKER:
                if (pass != 3)
                    break;
                if ((depth == 1) && (dlp == 0)) {
                    dlp = (struct dylinker_command *)lcp;
                    dlarchbits = (header->cputype & CPU_ARCH_MASK);
                } else {
                    ret = LOAD_FAILURE;
                }
                break;
            case LC_UUID:
                if (pass == 1 && depth == 1) {
                    ret = load_uuid((struct uuid_command *) lcp,
                            (char *)addr + mach_header_sz + header->sizeofcmds,
                            result);
                }
                break;
            case LC_CODE_SIGNATURE:
                /* CODE SIGNING */
                if (pass != 1)
                    break;
                /* pager -> uip ->
                   load signatures & store in uip
                   set VM object "signed_pages"
                */
                ret = load_code_signature(
                    (struct linkedit_data_command *) lcp,
                    vp,
                    file_offset,
                    macho_size,
                    header->cputype,
                    result);
                if (ret != LOAD_SUCCESS) {
                    printf("proc %d: load code signature error %d "
                           "for file \"%s\"\n",
                           p->p_pid, ret, vp->v_name);
                    ret = LOAD_SUCCESS; /* ignore error */
                } else {
                    got_code_signatures = TRUE;
                }
                break;
#if CONFIG_CODE_DECRYPTION
            case LC_ENCRYPTION_INFO:
            case LC_ENCRYPTION_INFO_64:
                if (pass != 3)
                    break;
                ret = set_code_unprotect(
                    (struct encryption_info_command *) lcp,
                    addr, map, slide, vp,
                    header->cputype, header->cpusubtype);
                if (ret != LOAD_SUCCESS) {
                    printf("proc %d: set_code_unprotect() error %d "
                           "for file \"%s\"\n",
                           p->p_pid, ret, vp->v_name);
                    /* 
                     * Don't let the app run if it's 
                     * encrypted but we failed to set up the
                     * decrypter. If the keys are missing it will
                     * return LOAD_DECRYPTFAIL.
                     */
                     if (ret == LOAD_DECRYPTFAIL) {
                        /* failed to load due to missing FP keys */
                        proc_lock(p);
                        p->p_lflag |= P_LTERM_DECRYPTFAIL;
                        proc_unlock(p);
                    }
                     psignal(p, SIGKILL);
                }
                break;
#endif
            default:
                /* Other commands are ignored by the kernel */
                ret = LOAD_SUCCESS;
                break;
            }

其中几个比较重要的加载命令:

  1. LC_SEGMENT(LC_SEGMENT_64),用于加载段(segment)的命令,有下面段用下面加载:__PAGEZERO__TEXTDATA__LINKEDIT。其中__PAGEZERO程序保留区,用于处理NULL异常,__TEXT保存程序代码和字符,DATA保存程序使用的二进制数据,__LINKEDIT保存动态库需要原始数据如符号、字符串、重定位条目等。也保留了起始地址信息,后续的LC_SYMTABLC_DYSYMTAB也是基于起始地址来算出相关偏移的值
  2. LC_LOAD_DYLINKER,用来读取动态加载库路径,通常在usr/lib/dyld,然后使用这个命令加载后面的动态库(最终还是递归调用parse_machfile)。
  3. LC_MAIN,用来读取程序入口
  4. LC_CODE_SIGNATURE 用来验证程序签名
  5. LC_DYSYMTAB加载Dynamic Symbol Table,保存了C Function相关的链接信息,通过数据偏移,可以查询LC_SYMTAB保存的C Function相关的信息,比如方法名和实现等。fishhook,利用这个机制可以找到C对应的方法实现,并动态替换成要hook的函数,具体参考我的fishHooker源码解析。

经过LoadCommand,程序正式被加载到内存中,最终运行起来。

MACHO Section

下面的主要是相关的节数据,主要有:

__TEXT段节名含义

1. __text: 代码节,存放机器编译后的代码
2. __stubs: 用于辅助做动态链接代码(dyld).
3. __stub_helper:用于辅助做动态链接(dyld).
4. __objc_methname:objc的方法名称
5. __cstring:代码运行中包含的字符串常量,比如代码中定义`#define kGeTuiPushAESKey        @"DWE2#@e2!"`,那DWE2#@e2!会存在这个区里。
6. __objc_classname:objc类名
7. __objc_methtype:objc方法类型
8. __ustring:
9. __gcc_except_tab:
10. __const:存储const修饰的常量
11. __dof_RACSignal:
12. __dof_RACCompou:
13. __unwind_info:

__DATA段节名含义

1. __got:存储引用符号的实际地址,类似于动态符号表,存储了`__nl_symbol_ptr`相关函数指针。
2. __la_symbol_ptr:lazy symbol pointers。懒加载的函数指针地址(C代码实现的函数对应实现的地址)。和__stubs和stub_helper配合使用。具体原理暂留。
3. __mod_init_func:模块初始化的方法。
4. __const:存储constant常量的数据。比如使用extern导出的const修饰的常量。
5. __cfstring:使用Core Foundation字符串
6. __objc_classlist:objc类列表,保存类信息,映射了__objc_data的地址
7. __objc_nlclslist:Objective-C 的 +load 函数列表,比 __mod_init_func 更早执行。
8. __objc_catlist: categories
9. __objc_nlcatlist:Objective-C 的categories的 +load函数列表。
10. __objc_protolist:objc协议列表
11. __objc_imageinfo:objc镜像信息
12. __objc_const:objc常量。保存objc_classdata结构体数据。用于映射类相关数据的地址,比如类名,方法名等。
13. __objc_selrefs:引用到的objc方法
14. __objc_protorefs:引用到的objc协议
15. __objc_classrefs:引用到的objc类
16. __objc_superrefs:objc超类引用
17. __objc_ivar:objc ivar指针,存储属性。
18. __objc_data:objc的数据。用于保存类需要的数据。最主要的内容是映射__objc_const地址,用于找到类的相关数据。
19. __data:暂时没理解,从日志看存放了协议和一些固定了地址(已经初始化)的静态量。
20. __bss:存储未初始化的静态量。比如:`static NSThread *_networkRequestThread = nil;`其中这里面的size表示应用运行占用的内存,不是实际的占用空间。所以计算大小的时候应该去掉这部分数据。
21. __common:存储导出的全局的数据。类似于static,但是没有用static修饰。比如KSCrash里面`NSDictionary* g_registerOrders;`, g_registerOrders就存储在__common里面

这部分数据会在上一步LoadCommand命令时,加载到内存里。

解析__objc_classlist

在看linkMap的时候,很奇怪的是,获取类名可以用_objc_classname, 获取方法名可以用_objc_methname,但是两个数据怎么匹配起来的,根据查相关资料,是通过__objc_classlist来映射的。

在解析的时候需要两个工具:MachOViewHopper

加载可执行文件

选用真机编译,编译选项选择Build Active Architecture Only,这样只生成一个CPU类型的文件,方便后续分析,然后在工程的DerivedData/**/Build/Products/**-iphonesos/**.app中显示包内容,把和工程同名的文件copy到自己的目录下。

打开``MachOview`,打开刚才的可执行文件。

img

解析__objc_class结构

直接看__objc_classlist节,

img

然后看下__objc_classlist数据结构,这个是个内存地址占用64位,
经过分析,__objc_classlist,保存的地址,映射的是__objc_data的地址,在MachOView中,对应的数据为:

img

使用Hopper打开可执行文件,按G,在搜索框里输入这个地址,比如输入0000000100009278

img

之后显示了一个数据结构。

img

这个数据对应的数据结构为:

typedef struct objc_class{
        struct __objc_class* isa;
        struct __objc_class* wuperclass;
        struct __objc_cache* cache;
        struct __objc_vtable* vtable;
        struct __objc_ data* data;
}objc_class;
  1. 第一个是64位指针,保存isa指针,指向了MetaClass指针,对应的地址为00000001000092A0,在Hopper中搜索这个地址,得到的数据为:

    img

  2. 第二个指向父类的指针,对应地址为0000000000000000

  3. 第5个指向data,对应的地址为:00000001000082C8, 这个数据保存在__objc_const节,对应的数据结构为__objc_data
    ,在Hopper中搜索这个地址,得到的数据为:

    img

    对应的具体数据为:
    img

``

解析__objc_data

对应的数据结构为:

typedef struct objc_data{
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
    uint32_t reserved;
    void* ivarlayout;
    char* name;
    struct __objc_method_list* baseMethod;
    struct __objc_protos* baseProtocol;
    struct __objc_ivars* ivars;
    struct __objc_ivars weakIvarLayout;
    struct __objc_ivars baseProperties;
}

主要的几个数据结构:

  1. name 保存的类名称。这个地址为:00000001000076B6,对应的数据在__objc_classname段里,用Hopper查看这个地址,对应的名称为ViewController

    img

  2. baseMethod,保存了类所有方法,这个地址为:0000000100008278 , 对应数据在__objc_const,可以在这里找到对应的数据。

    img

    对应数据结构为__objc_method_list,在Hopper,查看:
    img

解析__objc_method_list

对应的数据结构为:

typedef struct objc_method_list{
    uint32_t flags;
    uint32_t count;
}

使用到的数据主要是count,对应数据为00000003,对应10进制数为3,说明有3个方法。具体方法对应的数据结构为:

typedef struct objc_method{
    char* name;
    char* signature;
    void* implementation;
}

这个数据结构占用24(8*3)字节。objc_method_list结构体占用8字节,所以从0000000100008278开始,偏移8个字节,到0000000100008280就是第一个方法的起始位置,再偏移24个字节到0000000100008298,就是第二个方法起始地址位置,以此类推,最后一个方法占用地址为00000001000082b0 ~ 00000001000082c7

先看第一个方法存储的数据为:

img

然后分别解析这些地址:

  1. 0000000100006924,在__objc_methname段里,对应方法名称。

    img

  2. 000000010000770F,在__objc_methtype段里,对应方法签名,这里的值为v16@0:8,代表含义可以参考这里关于type encodings的理解–runtime programming guide

    img

  3. 0000000100004A20,在__text节里,对应的数据为:

    img

最终类需要的数据完全解析完成。

ps:想要知道数据结构是什么,可以在Hopper的右侧导航栏下,点击Manager type查看。

img

参考

  1. iOS安全–从Mach-o文件结构分析类名和方法名
  2. 从macho中解析类名
  3. 深入理解Macho文件(二)- 消失的OBJC段与新生的DATA段
  4. mach-o文件格式分析
  5. Macho kern
  6. main.m 方法之前的优化
  7. OSX内核加载mach-o流程分析
  8. iOS程序启动->dyld加载->runtime初始化(初识)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容