ios深入-MACHO文件解析

ios深入-MACHO文件解析

發(fā)表于 2017-10-26 | 分類于 優(yōu)化

導讀
在分析linkMap文件的時候,遇到一個有趣的問題:獲取類名可以用_objc_classname, 獲取方法名可以用_objc_methname〕喑矗可是怎么將方法名稱和對象名稱對應起來广料,程序是如何對應這兩部分數(shù)據(jù)的稻薇。帶著這個疑問研究了下macho的文件結(jié)構(gòu)。

MACHO文件說明

macho文件是mac os或ios系統(tǒng)可執(zhí)行文件的格式创坞,系統(tǒng)通過加載這個格式來執(zhí)行代碼平酿。

相關結(jié)構(gòu)如圖:

img

注:來源于:(http://www.reibang.com/p/f1a61b53398f

具體每部分的含義可以參考這個定義:

mach-0 loader.h

這里簡單講幾個我比較關注的:

注:下面都是以64位做演示說明凤优,cpu結(jié)構(gòu)為arm64。

MachO Header的結(jié)構(gòu)

img

數(shù)據(jù)結(jié)構(gòu)為:

/*
 * 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. 第一個四字節(jié)數(shù)叫做magic number,可以得到使用的是64位還是32位系統(tǒng)
  2. 第二個字節(jié)和第三個字節(jié)是CPU類型
  3. 第四個字節(jié)是文件類型蜈彼。MH_EXECUTE表示可執(zhí)行文件
  4. 第五個字節(jié)和第六個字節(jié)表示load commands的個數(shù)和長度
  5. 第7個字節(jié)是加載的flag信息筑辨。具體參考loader.h中的文件

MachO load command

程序檢索完Header之后就開始加載和解析Load Commands了。

相關代碼在mach_loader.c,通過遞歸調(diào)用加載命令柳刮。

img

img

load_comand的數(shù)據(jù)結(jié)構(gòu)為:

/*
 * 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程序保留區(qū),用于處理NULL異常,__TEXT保存程序代碼和字符哪替,DATA保存程序使用的二進制數(shù)據(jù)栋荸,__LINKEDIT保存動態(tài)庫需要原始數(shù)據(jù)如符號、字符串凭舶、重定位條目等晌块。也保留了起始地址信息,后續(xù)的LC_SYMTABLC_DYSYMTAB也是基于起始地址來算出相關偏移的值
  2. LC_LOAD_DYLINKER,用來讀取動態(tài)加載庫路徑帅霜,通常在usr/lib/dyld匆背,然后使用這個命令加載后面的動態(tài)庫(最終還是遞歸調(diào)用parse_machfile)。
  3. LC_MAIN身冀,用來讀取程序入口
  4. LC_CODE_SIGNATURE 用來驗證程序簽名
  5. LC_DYSYMTAB加載Dynamic Symbol Table,保存了C Function相關的鏈接信息钝尸,通過數(shù)據(jù)偏移,可以查詢LC_SYMTAB保存的C Function相關的信息搂根,比如方法名和實現(xiàn)等珍促。fishhook,利用這個機制可以找到C對應的方法實現(xiàn),并動態(tài)替換成要hook的函數(shù)剩愧,具體參考我的fishHooker源碼解析猪叙。

經(jīng)過LoadCommand,程序正式被加載到內(nèi)存中仁卷,最終運行起來穴翩。

MACHO Section

下面的主要是相關的節(jié)數(shù)據(jù),主要有:

__TEXT段節(jié)名含義

1. __text: 代碼節(jié)五督,存放機器編譯后的代碼
2. __stubs: 用于輔助做動態(tài)鏈接代碼(dyld).
3. __stub_helper:用于輔助做動態(tài)鏈接(dyld).
4. __objc_methname:objc的方法名稱
5. __cstring:代碼運行中包含的字符串常量,比如代碼中定義`#define kGeTuiPushAESKey        @"DWE2#@e2!"`,那DWE2#@e2!會存在這個區(qū)里藏否。
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段節(jié)名含義

1. __got:存儲引用符號的實際地址,類似于動態(tài)符號表充包,存儲了`__nl_symbol_ptr`相關函數(shù)指針。
2. __la_symbol_ptr:lazy symbol pointers遥椿。懶加載的函數(shù)指針地址(C代碼實現(xiàn)的函數(shù)對應實現(xiàn)的地址)基矮。和__stubs和stub_helper配合使用。具體原理暫留冠场。
3. __mod_init_func:模塊初始化的方法家浇。
4. __const:存儲constant常量的數(shù)據(jù)。比如使用extern導出的const修飾的常量碴裙。
5. __cfstring:使用Core Foundation字符串
6. __objc_classlist:objc類列表,保存類信息钢悲,映射了__objc_data的地址
7. __objc_nlclslist:Objective-C 的 +load 函數(shù)列表点额,比 __mod_init_func 更早執(zhí)行。
8. __objc_catlist: categories
9. __objc_nlcatlist:Objective-C 的categories的 +load函數(shù)列表莺琳。
10. __objc_protolist:objc協(xié)議列表
11. __objc_imageinfo:objc鏡像信息
12. __objc_const:objc常量还棱。保存objc_classdata結(jié)構(gòu)體數(shù)據(jù)。用于映射類相關數(shù)據(jù)的地址惭等,比如類名珍手,方法名等。
13. __objc_selrefs:引用到的objc方法
14. __objc_protorefs:引用到的objc協(xié)議
15. __objc_classrefs:引用到的objc類
16. __objc_superrefs:objc超類引用
17. __objc_ivar:objc ivar指針,存儲屬性辞做。
18. __objc_data:objc的數(shù)據(jù)琳要。用于保存類需要的數(shù)據(jù)。最主要的內(nèi)容是映射__objc_const地址秤茅,用于找到類的相關數(shù)據(jù)稚补。
19. __data:暫時沒理解,從日志看存放了協(xié)議和一些固定了地址(已經(jīng)初始化)的靜態(tài)量框喳。
20. __bss:存儲未初始化的靜態(tài)量孔厉。比如:`static NSThread *_networkRequestThread = nil;`其中這里面的size表示應用運行占用的內(nèi)存,不是實際的占用空間帖努。所以計算大小的時候應該去掉這部分數(shù)據(jù)撰豺。
21. __common:存儲導出的全局的數(shù)據(jù)。類似于static拼余,但是沒有用static修飾污桦。比如KSCrash里面`NSDictionary* g_registerOrders;`, g_registerOrders就存儲在__common里面

這部分數(shù)據(jù)會在上一步LoadCommand命令時,加載到內(nèi)存里匙监。

解析__objc_classlist

在看linkMap的時候凡橱,很奇怪的是,獲取類名可以用_objc_classname, 獲取方法名可以用_objc_methname亭姥,但是兩個數(shù)據(jù)怎么匹配起來的稼钩,根據(jù)查相關資料,是通過__objc_classlist來映射的达罗。

在解析的時候需要兩個工具:MachOViewHopper坝撑,

加載可執(zhí)行文件

選用真機編譯,編譯選項選擇Build Active Architecture Only,這樣只生成一個CPU類型的文件,方便后續(xù)分析粮揉,然后在工程的DerivedData/**/Build/Products/**-iphonesos/**.app中顯示包內(nèi)容巡李,把和工程同名的文件copy到自己的目錄下。

打開``MachOview`,打開剛才的可執(zhí)行文件扶认。

img

解析__objc_class結(jié)構(gòu)

直接看__objc_classlist節(jié)侨拦,

img

然后看下__objc_classlist數(shù)據(jù)結(jié)構(gòu),這個是個內(nèi)存地址占用64位,
經(jīng)過分析辐宾,__objc_classlist,保存的地址狱从,映射的是__objc_data的地址膨蛮,在MachOView中,對應的數(shù)據(jù)為:

img

使用Hopper打開可執(zhí)行文件季研,按G敞葛,在搜索框里輸入這個地址,比如輸入0000000100009278

img

之后顯示了一個數(shù)據(jù)結(jié)構(gòu)训貌。

img

這個數(shù)據(jù)對應的數(shù)據(jù)結(jié)構(gòu)為:

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中搜索這個地址豺鼻,得到的數(shù)據(jù)為:

    img

  2. 第二個指向父類的指針,對應地址為0000000000000000

  3. 第5個指向data,對應的地址為:00000001000082C8, 這個數(shù)據(jù)保存在__objc_const節(jié),對應的數(shù)據(jù)結(jié)構(gòu)為__objc_data
    ,在Hopper中搜索這個地址款慨,得到的數(shù)據(jù)為:

    img

    對應的具體數(shù)據(jù)為:
    img

``

解析__objc_data

對應的數(shù)據(jù)結(jié)構(gòu)為:

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

主要的幾個數(shù)據(jù)結(jié)構(gòu):

  1. name 保存的類名稱儒飒。這個地址為:00000001000076B6,對應的數(shù)據(jù)在__objc_classname段里,用Hopper查看這個地址,對應的名稱為ViewController

    img

  2. baseMethod,保存了類所有方法檩奠,這個地址為:0000000100008278 , 對應數(shù)據(jù)在__objc_const,可以在這里找到對應的數(shù)據(jù)桩了。

    img

    對應數(shù)據(jù)結(jié)構(gòu)為__objc_method_list,在Hopper,查看:
    img

解析__objc_method_list

對應的數(shù)據(jù)結(jié)構(gòu)為:

typedef struct objc_method_list{
    uint32_t flags;
    uint32_t count;
}

使用到的數(shù)據(jù)主要是count,對應數(shù)據(jù)為00000003,對應10進制數(shù)為3埠戳,說明有3個方法井誉。具體方法對應的數(shù)據(jù)結(jié)構(gòu)為:

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

這個數(shù)據(jù)結(jié)構(gòu)占用24(8*3)字節(jié)。objc_method_list結(jié)構(gòu)體占用8字節(jié)整胃,所以從0000000100008278開始颗圣,偏移8個字節(jié),到0000000100008280就是第一個方法的起始位置屁使,再偏移24個字節(jié)到0000000100008298,就是第二個方法起始地址位置舌菜,以此類推炭庙,最后一個方法占用地址為00000001000082b0 ~ 00000001000082c7腌闯。

先看第一個方法存儲的數(shù)據(jù)為:

img

然后分別解析這些地址:

  1. 0000000100006924,在__objc_methname段里驮吱,對應方法名稱。

    img

  2. 000000010000770F,在__objc_methtype段里酬蹋,對應方法簽名及老,這里的值為v16@0:8,代表含義可以參考這里關于type encodings的理解–runtime programming guide

    img

  3. 0000000100004A20,在__text節(jié)里,對應的數(shù)據(jù)為:

    img

最終類需要的數(shù)據(jù)完全解析完成除嘹。

ps:想要知道數(shù)據(jù)結(jié)構(gòu)是什么写半,可以在Hopper的右側(cè)導航欄下,點擊Manager type查看尉咕。

img

參考

  1. iOS安全–從Mach-o文件結(jié)構(gòu)分析類名和方法名
  2. 從macho中解析類名
  3. 深入理解Macho文件(二)- 消失的OBJC段與新生的DATA段
  4. mach-o文件格式分析
  5. Macho kern
  6. main.m 方法之前的優(yōu)化
  7. OSX內(nèi)核加載mach-o流程分析
  8. iOS程序啟動->dyld加載->runtime初始化(初識)
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市璃岳,隨后出現(xiàn)的幾起案子年缎,更是在濱河造成了極大的恐慌悔捶,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件单芜,死亡現(xiàn)場離奇詭異蜕该,居然都是意外死亡,警方通過查閱死者的電腦和手機洲鸠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門堂淡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人扒腕,你說我怎么就攤上這事绢淀。” “怎么了瘾腰?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵皆的,是天一觀的道長。 經(jīng)常有香客問我蹋盆,道長费薄,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任栖雾,我火速辦了婚禮楞抡,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘析藕。我一直安慰自己召廷,他們只是感情好,可當我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布噪径。 她就那樣靜靜地躺著柱恤,像睡著了一般。 火紅的嫁衣襯著肌膚如雪找爱。 梳的紋絲不亂的頭發(fā)上梗顺,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天,我揣著相機與錄音车摄,去河邊找鬼寺谤。 笑死,一個胖子當著我的面吹牛吮播,可吹牛的內(nèi)容都是我干的变屁。 我是一名探鬼主播,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼意狠,長吁一口氣:“原來是場噩夢啊……” “哼粟关!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起环戈,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤闷板,失蹤者是張志新(化名)和其女友劉穎澎灸,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體遮晚,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡性昭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了县遣。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片糜颠。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖萧求,靈堂內(nèi)的尸體忽然破棺而出其兴,到底是詐尸還是另有隱情,我是刑警寧澤饭聚,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站秒梳,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏酪碘。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一徙赢、第九天 我趴在偏房一處隱蔽的房頂上張望探越。 院中可真熱鬧,春花似錦钦幔、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽卷玉。三九已至哨颂,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間相种,已是汗流浹背威恼。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人沃测。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓缭黔,卻偏偏與公主長得像食茎,于是被迫代替她去往敵國和親蒂破。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容