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)都擁有一段虛擬地址映射到進程的地址空間。
官方給的圖如下:
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概述已經介紹
頭信息的結構可以在 /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的存儲位置,如下圖
編譯后,到編譯目錄里找到該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的頭文件的命令行工具。