Mach-O可執行文件

Mach-O 概述 和 部分命令介紹

我們知道Windows下的文件都是PE文件,同樣在OS X和iOS中可執行文件是Mach-o格式的。
Mach-O通常有三部分組成
*頭部 (Header): Mach-O文件的架構 比如Mac的 PPC, PPC64, IA-32, x86-64,ios的arm系列.
*加載命令(Load commands): .
*原始段數據(Raw segment data):可以擁有多個段(segment),每個段可以擁有零個或多個區域(section)。每一個段(segment)都擁有一段虛擬地址映射到進程的地址空間。
官方給的圖如下:

mach_o_segments

Xcode本身包含了command-line tools,命令行工具本身包含了分析和編譯Mach-O,相關如下:

  • lipo /usr/bin/lipo 能夠分析二進制文件的架構,可以拆分和合并二進制文件。比如查看一個靜態庫的架構,可以使用
lipo -info lib1.a
  • otool /usr/bin/otool 列出Mach-O文件的sections和segments信息,具體使用可以參考otool --help
  • pagestuff /usr/bin/pagestuff 展示每一個組成反射(image)的每個邏輯頁面的內容,其中包含了sections的名字和每個page里的符號。這個工具不能在有多個架構的包含映射的二進制文件中運行。
  • symbol table的展示工具,/usr/bin/nm,允許你查看對象文件符號表的內容

Mach-O 分析

通常一個iOS App應用會安裝在/var/mobile/Applications,系統的原生App會安裝在/Applications目錄下,大部分情況下,xxx.app/xxx文件并不是Mach-O格式文件,由于現在需要支持不同CPU架構的iOS設備,所以我們編譯打包出來的執行文件是一個Universal Binary格式文件(通用二進制文件,也稱胖二進制文件),實際上Universal Binary只不過將支持不同架構的Mach-O打包在一起,再在文件起始位置加上Fat Header來說明所包含的Mach-O文件支持的架構和偏移地址信息。
例如:

file CTRIP_WIRELESS
CTRIP_WIRELESS: Mach-O universal binary with 2 architectures
CTRIP_WIRELESS (for architecture i386): Mach-O executable i386
CTRIP_WIRELESS (for architecture x86_64):   Mach-O 64-bit executable x86_64

上面顯示程序支持i386和x86_64架構,胖二進制文件定義在 /usr/include/mach-o/fat.h,我們查看一下源碼

#include <stdint.h>
#include <mach/machine.h>
#include <architecture/byte_order.h>

#define FAT_MAGIC   0xcafebabe
#define FAT_CIGAM   0xbebafeca  /* NXSwapLong(FAT_MAGIC) */

struct fat_header {
    uint32_t    magic;      /* FAT_MAGIC */
    uint32_t    nfat_arch;  /* number of structs that follow */
};

struct fat_arch {
    cpu_type_t  cputype;    /* cpu specifier (int) */
    cpu_subtype_t   cpusubtype; /* machine specifier (int) */
    uint32_t    offset;     /* file offset to this object file */
    uint32_t    size;       /* size of this object file */
    uint32_t    align;      /* alignment as a power of 2 */
};

結構體struct fat_header:

  • magic字段就是我們常說的魔數(例如通常判斷png文件格式,還有快速求平方根的0x5f3759df),加載器通過這個魔數值來判斷這是什么樣的文件,胖二進制文件的魔數值是0xcafebabe;
  • nfat_arch字段是指當前的胖二進制文件包含了多少個不同架構的Mach-O文件;
    fat_header后會跟著fat_arch,有多少個不同架構的Mach-O文件,就有多少個fat_arch,用于說明對應Mach-O文件大小、支持的CPU架構、偏移地址等;

(1).頭部Header

我們可以使用 otool(1) 來觀察可執行文件的頭部 -- 規定了這個文件是什么,以及文件是如何被加載的。通過 -h 可以打印出頭信息:
例如使用otool命令可以查看Mach-O文件的頭信息,頭信息就是Mach-O文件的第一部分,我們在第一部分介紹Mach-o概述已經介紹


mach_o_header

頭信息的結構可以在 /usr/include/mach-o/loader.h

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

我們依次介紹這個頭信息

1.magic,可以看到文件中的內容最開始部分,是以 cafe babe開頭的
對于一個 二進制文件 來講,每個類型都可以在文件最初幾個字節來標識出來,即“魔數”。不同類型的 二進制文件,都有自己獨特的"魔數"。
OS X上,可執行文件的標識有這樣幾個魔數(不同的魔數代表不同的可執行文件類型)
是mach-o文件的魔數,0xfeedface代表的是32位,0xfeedfacf代表64位,cafebabe是跨處理器架構的通用格式,#!代表的是腳本文件。
2.cputype和cupsubtype代表的是cpu的類型和其子類型,圖上的例子是模擬器程序,cpu結構是x86_64,如果直接查看ipa,可以看到cpu是arm,subtype是armv7,arm64等
3.接著是filetype,2,代表可執行的文件 #define MH_EXECUTE 0×2
4.ncmds 指的是加載命令(load commands)的數量,例子中一共65個,編號0-64
5.sizeofcmds 表示23個load commands的總字節大小, load commands區域是緊接著header區域的。
6.最后個flags,例子中是0×00200085,可以按文檔分析之。
也可以借助UE程序MachOView,MachOView是Mac上查看Mach-O結構的工具,如下圖

mach_o_h_view

(2).加載命令(Load commands)

load commmand直接跟在 header 部分的后面,結構定義如下

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

這些加載命令在Mach-O文件加載解析時,被內核加載器或者動態鏈接器調用,指導如何設置加載對應的二進制數據段,加載命令的種類有很多種,在<mach-o/loader.h>頭文件有簡單的注釋。
具體可以使用命令

otool -v -l CTRIP_WIRELESS | open -f

查看。

(3)段數據(Segments)

Segments包含了很多segment,每一個segment定義了一些Mach-O文件的數據、地址和內存保護屬性,這些數據在動態鏈接器加載程序時被映射到了虛擬內存中。每個段都有不同的功能,一般包括:

  • 1). __PAGEZERO: 空指針陷阱段,映射到虛擬內存空間的第一頁,用于捕捉對NULL指針的引用;
  • 2). __TEXT: 包含了執行代碼以及其他只讀數據。 為了讓內核將它 直接從可執行文件映射到共享內存, 靜態連接器設置該段的虛擬內存權限為不允許寫。當這個段被映射到內存后,可以被所有進程共享。(這主要用在frameworks, bundles和共享庫等程序中,也可以為同一個可執行文件的多個進程拷貝使用)
  • 3). __DATA: 包含了程序數據,該段可寫;
  • 4). __OBJC: Objective-C運行時支持庫;
  • 5). __LINKEDIT: 含有為動態鏈接庫使用的原始數據,比如符號,字符串,重定位表條目等等。

一般的段又會按不同的功能劃分為幾個區(section),即段所有字母大小,加兩個下橫線作為前綴,而區則為小寫,同樣加兩個下橫線作為前綴

下面列出段中可能包含的section:

__TEXT段:
__text, __cstring, __picsymbol_stub, __symbol_stub, __const, __litera14, __litera18;

__DATA段:
__data, __la_symbol_ptr, __nl_symbol_ptr, __dyld, __const, __mod_init_func, __mod_term_func, __bss, __commom;

__IMPORT段
__jump_table, __pointers;

其中__TEXT段中的__text是實際上的代碼部分;__DATA段的__data是實際的初始數據,更加詳細的說明見這里

可以通過otool –s查看某segment的某個section。

otool -s __TEXT __text a.out 
a.out:
(__TEXT,__text) section
0000000100000e80 55 48 89 e5 48 83 ec 20 c7 45 fc 00 00 00 00 89 
0000000100000e90 7d f8 48 89 75 f0 e8 a7 00 00 00 48 8b 35 8e 02 
0000000100000ea0 00 00 48 8b 0d 6f 02 00 00 48 89 f7 48 89 ce 48 
0000000100000eb0 89 45 e0 e8 90 00 00 00 48 8b 35 61 02 00 00 48 
0000000100000ec0 89 c7 e8 81 00 00 00 48 89 45 e8 48 8b 45 e8 48 
0000000100000ed0 8b 35 52 02 00 00 48 89 c7 e8 6a 00 00 00 c7 45 
0000000100000ee0 fc 00 00 00 00 48 8b 7d e0 e8 4e 00 00 00 8b 45 
0000000100000ef0 fc 48 83 c4 20 5d c3 90 90 90 90 90 90 90 90 90 
0000000100000f00 55 48 89 e5 48 83 ec 10 48 89 7d f8 48 89 75 f0 
0000000100000f10 e8 1b 00 00 00 48 8d 35 1c 01 00 00 48 89 f7 48 
0000000100000f20 89 c6 b0 00 e8 0d 00 00 00 48 83 c4 10 5d c3 

由于 -s __TEXT __text 很常見,otool 對其設置了一個縮寫 -t 。我們還可以通過添加 -v 來查看反匯編代碼:

otool -v -t a.out
a.out:
(__TEXT,__text) section
_main:
0000000100000e80    pushq   %rbp
0000000100000e81    movq    %rsp, %rbp
0000000100000e84    subq    $0x20, %rsp
0000000100000e88    movl    $0x0, -0x4(%rbp)
0000000100000e8f    movl    %edi, -0x8(%rbp)
0000000100000e92    movq    %rsi, -0x10(%rbp)
0000000100000e96    callq   0x100000f42
0000000100000e9b    movq    0x28e(%rip), %rsi
0000000100000ea2    movq    0x26f(%rip), %rcx
0000000100000ea9    movq    %rsi, %rdi
0000000100000eac    movq    %rcx, %rsi
...

了解Mach-O的作用

(1)Xcode中配置LinkMap

LinkMap文件是Xcode產生可執行文件(Mach-O)的同時生成的鏈接信息,用來描述可執行文件的構造成分,包括代碼段(__TEXT)和數據段(__DATA)的分布情況。XCode -> Project -> Build Settings -> 搜map -> 把Write Link Map File選項設為yes,并指定好linkMap的存儲位置,如下圖


linkmap

編譯后,到編譯目錄里找到該txt文件,文件名和路徑就是上述的Path to Link Map File
位于~/Library/Developer/Xcode/DerivedData/XXX-eumsvrzbvgfofvbfsoqokmjprvuh/Build/Intermediates/XXX.build/Debug-iphoneos/XXX.build/

LinkMap里展示了整個可執行文件的全貌,分為三段,分別是:

  • # Object files:為分割標志,列出所有.o目標文件的信息(包括靜態鏈接庫.a里的),
  • # Sections:為分割標志,描述各個段在最后編譯成的可執行文件中的偏移位置及大小,包括了代碼段(__TEXT,保存程序代碼段編譯后的機器碼)和數據段(__DATA,保存變量值),字段的含義在Mach-o中已詳細介紹。
  • # Symbols:為分割標志,列出具體的按每個文件列出每個對應字段的位置和占用空間,例如:
# Symbols:
# Address   Size        File  Name
0x100002070 0x000000B0  [  1] -[PackageManager init]
0x100002120 0x00000080  [  1] +[PackageManager share]
0x1000021A0 0x00000050  [  1] ___23+[PackageManager share]_block_invoke
0x1000021F0 0x00000080  [  1] +[PackageManager getPackageType]
0x100002270 0x000000E0  [  1] +[PackageManager isProductionEnv]
0x100002350 0x000000E0  [  1] +[PackageManager isSpecialPackageForTest]
0x100002430 0x000000E0  [  1] +[PackageManager isDevEnv]
0x100002510 0x00000020  [  1] +[PackageManager isAUTOMATIC_TEST_ENV]
0x100002530 0x00000020  [  1] +[PackageManager isPRO_PACKAGE]
0x100002550 0x00000020  [  1] +[PackageManager ApplicationVersion]
0x100002570 0x00000020  [  1] -[PackageManager packageType]
0x100002590 0x00000040  [  1] -[PackageManager setPackageType:]
0x1000025D0 0x00000033  [  1] -[PackageManager .cxx_destruct]
0x100002610 0x0000005B  [  2] _main
0x10000266B 0x00000255  [  3] -[CTMyCtripOROrderAction isEqual:]

同樣首列是數據在文件的偏移地址,第二列是占用大小,第三列是所屬文件序號,對應上述Object files列表,最后是名字。
例如第二行代表了文件序號為2(反查上面就是PackageManage.o)的share方法占用了8*16=128byte大小。

根據上面符號文件的分析,我們可以寫一個腳本統計我們程序的每一個靜態庫和framwork以及每一個實現文件的大小,有便于我們分析程序文件大小,為代碼優化,減少二進制包大小提供了優化方向。我寫了一個腳本,代碼如下:

#!usr/bin/python
## -*- coding: UTF-8 -*-
#
#使用簡介:python linkmap.py XXX-LinkMap-normal-xxxarch.txt 或者 python linkmap.py XXX-LinkMap-normal-xxxarch.txt -g
#使用參數-g會統計每個模塊.o的統計大小
#
__author__ = "zmjios"
__date__ = "2016-07-27"

import os
import re
import shutil
import sys

class SymbolModel:
    file = ""
    size = 0

def verify_linkmapfile(args):
    if len(sys.argv) < 2:
        print("請輸入linkMap文件")
        return False
    
    path = args[1]

    if not os.path.isfile(path):
        print("請輸入文件")
        return False

    file = open(path)
    content = file.read()
    file.close()

    #查找是否存在# Object files:
    if content.find("# Object files:") == -1:
        print("輸入linkmap文件非法")
        return False
    #查找是否存在# Sections:
    if content.find("# Sections:") == -1:
        print("輸入linkmap文件非法")
        return False
    #查找是否存在# Symbols:
    if content.find("# Symbols:") == -1:
        print("輸入linkmap文件非法")
        return False

    return True 

def symbolMapFromContent():
    symbolMap = {}
    reachFiles = False
    reachSections = False
    reachSymblos = False
    file = open(sys.argv[1])
    for line in file.readlines():
        if line.startswith("#"):
            if line.startswith("# Object files:"):
                reachFiles = True
            if line.startswith("# Sections:"):
                reachSections = True
            if line.startswith("# Symbols:"):
                reachSymblos = True
        else:
            if reachFiles == True and reachSections == False and reachSymblos == False:
                #查找 files 列表,找到所有.o文件
                location = line.find("]")
                if location != -1:
                    key = line[:location+1]
                    if  symbolMap.get(key) is not None:
                        continue
                    symbol = SymbolModel()
                    symbol.file = line[location + 1:]
                    symbolMap[key] = symbol
            elif reachFiles == True and reachSections == True and reachSymblos == True:
                #'\t'分割成三部分,分別對應的是Address,Size和 File  Name
                symbolsArray = line.split('\t')
                if len(symbolsArray) == 3:
                    fileKeyAndName = symbolsArray[2]
                    #16進制轉10進制
                    size = int(symbolsArray[1],16)
                    location = fileKeyAndName.find(']')
                    if location != -1:
                        key = fileKeyAndName[:location + 1]
                        symbol = symbolMap.get(key)
                        if symbol is not None:
                            symbol.size = symbol.size + size
    file.close()
                            
    return symbolMap
    
def sortSymbol(symbolList):
     return sorted(symbolList, key=lambda s: s.size,reverse = True)

def buildResultWithSymbols(symbols):
    results = ["文件大小\t文件名稱\r\n"]
    totalSize = 0
    for symbol in symbols:
        results.append(calSymbol(symbol))
        totalSize += symbol.size
    results.append("總大小: %.2fM" % (totalSize/1024.0/1024.0))
    return results

def buildCombinationResultWithSymbols(symbols):
    #統計不同模塊大小
    results = ["庫大小\t庫名稱\r\n"]
    totalSize = 0
    combinationMap = {}
    
    for symbol in symbols:
        names = symbol.file.split('/')
        name = names[len(names) - 1].strip('\n')
        location = name.find("(")
        if name.endswith(")") and location != -1:
            component = name[:location]
            combinationSymbol = combinationMap.get(component)
            if combinationSymbol is None:
                combinationSymbol = SymbolModel()
                combinationMap[component] = combinationSymbol

            combinationSymbol.file = component
            combinationSymbol.size = combinationSymbol.size + symbol.size
        else:
            #symbol可能來自app本身的目標文件或者系統的動態庫
            combinationMap[symbol.file] = symbol
    sortedSymbols = sortSymbol(combinationMap.values())

    for symbol in sortedSymbols:
        results.append(calSymbol(symbol))
        totalSize += symbol.size
    results.append("總大小: %.2fM" % (totalSize/1024.0/1024.0))

    return results

def calSymbol(symbol):
    size = ""
    if symbol.size / 1024.0 / 1024.0 > 1:
        size = "%.2fM" % (symbol.size / 1024.0 / 1024.0)
    else:
        size = "%.2fK" % (symbol.size / 1024.0)
    names = symbol.file.split('/')
    if len(names) > 0:
        size = "%s\t%s" % (size,names[len(names) - 1])
    return size

def analyzeLinkMap():
    if verify_linkmapfile(sys.argv) == True:
        print("**********正在開始解析*********")
        symbolDic = symbolMapFromContent()
        symbolList = sortSymbol(symbolDic.values())
        if len(sys.argv) >= 3 and sys.argv[2] == "-g":
            results = buildCombinationResultWithSymbols(symbolList)
        else:
            results = buildResultWithSymbols(symbolList)
        for result in results:
            print(result)
        print("***********解析結束***********")


if __name__ == "__main__":
    analyzeLinkMap()

(2).查找無用selector和無用class

WeMobileDev公眾號之前介紹了iOS微信安裝包瘦身也做了相關介紹。無論是Mach-O或者是linkMap文件,都能做相關操作。具體原理是過正則表達式([+|-][.+\s(.+)]),我們可以提取當前可執行文件里所有objc類方法和實例方法(SelectorsAll)。再使用otool命令otool -v -s __DATA __objc_selrefs逆向__DATA.__objc_selrefs段,提取可執行文件里引用到的方法名(UsedSelectorsAll),我們可以大致分析出SelectorsAll里哪些方法是沒有被引用的(SelectorsAll-UsedSelectorsAll)。注意,系統API的Protocol可能被列入無用方法名單里,如UITableViewDelegate的方法,我們只需要對這些Protocol里的方法加入白名單過濾即可。
萬能的github(https://github.com/nst/objc_cover)已經有人寫了相關腳本,有需要可以參考。

(3)class-dump和越獄相關

class-dump正式利用Mach-O文件導出出Mac或者iOS app的頭文件的命令行工具。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,494評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,283評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,714評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,410評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,940評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,776評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,210評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,654評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容

  • 熟悉Linux和windows開發的同學都知道,ELF是Linux下可執行文件的格式,PE32/PE32+是win...
    Klaus_J閱讀 3,992評論 1 10
  • 可執行文件是怎么來的?(以C語言為例) C代碼(.c) - 經過編譯器預處理,編譯成匯編代碼(.asm) - 匯編...
    那只大象閱讀 8,065評論 2 9
  • 原文地址 寫在之前 之前工作中對Mach-O文件有一定的接觸, 原本早就想寫一篇文章分享一下,但是奈何只是不夠深入...
    南梔傾寒閱讀 4,827評論 3 22
  • 13. Hook原理介紹 13.1 Objective-C消息傳遞(Messaging) 對于C/C++這類靜態語...
    Flonger閱讀 1,428評論 0 3
  • 一個在異地星巴克萍水相逢的久違的女孩發了 你是不是換號了 看你也沒更新動態 也沒評論 因為一些原因已經回來...
    WilsonW閱讀 207評論 1 0