瞭解iOS上的可執行文件和Mach-O格式

很多朋友都知道,在Windows上exe是可直接執行的文件擴展名,而在Linux(以及很多版本的Unix)系統上ELF是可直接執行的文件格式,那麼在蘋果的操作系統上又是怎樣的呢?在iOS(和Mac OS X)上,主要的可執行文件格式是Mach-O格式。本文就關於iOS上的可執行文件和Mach-O格式做一個簡要整理。

Mach-O格式是iOS系統上應用程序運行的基礎,瞭解Mach-O的格式,對於調試、自動化測試、安全都有意義。在瞭解二進制文件的數據結構以後,一切就都顯得沒有祕密。

0. Mach與Mach-O

這裏先提醒大家一下,Mach不是Mac,Mac是蘋果電腦Macintosh的簡稱,而Mach則是一種操作系統內核。Mach內核被NeXT公司的NeXTSTEP操作系統使用。在Mach上,一種可執行的文件格是就是Mach-O(Mach Object file format)。1996年,喬布斯將NeXTSTEP帶回蘋果,成爲了OS X的內核基礎。所以雖然Mac OS X是Unix的“後代”,但所主要支持的可執行文件格式是Mach-O。

iOS是從OS X演變而來,所以同樣是支持Mach-O格式的可執行文件。

1. iOS可執行文件初探

作爲iOS客戶端開發者,我們比較熟悉的一種文件是ipa包(iPhone Application)。但實際上這只是一個變相的zip壓縮包,我們可以把一個ipa文件直接通過unzip命令解壓。

解壓之後,會有一個Payload目錄,而Payload裏則是一個.app文件,而這個實際上又是一個目錄,或者說是一個完整的App Bundle。

在這個目錄中,裏面體積最大的文件通常就是和ipa包同名的一個二進制文件。找到它,我們用file命令來看一下這個文件的類型:

1
2
3
XXX: Mach-O universal binary with 2 architectures
XXX (for architecture armv7): Mach-O executable arm
XXX (for architecture armv7s): Mach-O executable arm

由此看來,這是一個支持armv7和armv7s兩種處理器架構的通用程序包,裏面包含的兩部分都是Mach-O格式。

對於一個二進制文件來講,每個類型都可以在文件最初幾個字節來標識出來,即“魔數”。比如PNG圖片的最初幾個字節是\211 P N G \r \n \032 \n (89 50 4E 47 0D 0A 1A 0A)。我們再來看下這個Mach-O universal binary的:

0000000 ca fe ba be 00 00 00 02 00 00 00 0c 00 00 00 09

沒錯,開始的4個字節是cafe babe,即“Cafe baby”。瞭解Java或者說class文件格式的同學可能會很熟悉,這也是.class文件開頭的“魔數”,但貌似是Mach-O在更早的時候就是用了它。在OS X上,可執行文件的標識有這樣幾個魔數(也就是文件格式):

  • cafebabe
  • feedface
  • feadfacf
  • 還有一個格式,就是以#!開頭的腳本

cafebabe就是跨處理器架構的通用格式,feedface和feedfacf則分別是某一處理器架構下的Mach-O格式,腳本的就很常見了,比如#!/bin/bash開頭的shell腳本。

這裏注意一點是,feedface和cafebabe的字節順序不同,我們可以用lipo把上面cafebabe的文件拆出armv7架構的,看一下開頭的幾個字節:

0000000 ce fa ed fe 0c 00 00 00 09 00 00 00 02 00 00 00

2. Mach-O格式

接下來我們再來看看這個Mach-O格式到底是什麼樣的格式。我們可以通過二進制查看工具查看這個文件的數據,結果發現,不是所有數據都是相連的,而是被分成了幾個段落。

在一位叫做JOE SAVAGE的老兄發佈的圖片上來看,Mach-O的文件數據顯現出來是這個樣子的:

圖形化的Mach-O文件數據

大家可以對數據的分佈感受下。

雖然被五顏六色的標記出來,可能這還不是特別直接。再來引用蘋果官方文檔的示意圖:

Mach-O文件格式基本結構

Mach-O文件格式基本結構

從這張圖上來看,Mach-O文件的數據主體可分爲三大部分,分別是頭部(Header)、加載命令(Load commands)、和最終的數據(Data)。

回過頭來,我們再看上面那張圖,也許就都明白了。黃色部分是頭部、紅色是加載命令、而其它部分則是被分割成Segments的數據。

3. Mach-O頭部

這裏,我們用otool來看下Mach-O的頭部信息,得到:

1
2
     magic cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
0xfeedface      12          9  0x00          2    45       4788 0x00218085

更詳細的,我們可以通過otool的V參數得到翻譯版:

1
2
3
Mach header
      magic cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
   MH_MAGIC     ARM         V7  0x00     EXECUTE    45       4788   NOUNDEFS DYLDLINK TWOLEVEL WEAK_DEFINES BINDS_TO_WEAK PIE

前面幾個字段的意義,上下對比就能看懂,我這裏主要說下這樣幾個字段:

  • filetype,這個可以有很多類型,靜態庫(.a)、單個目標文件(.o)都可以通過這個類型標識來區分。
  • ncmds和sizeofcmds,這個cmd就是加載命令,ncmds就是加載命令的個數,而sizeofcmds就是所佔的大小。
  • flags裏包含的標記很多,比如TWOLEVEL是指符號都是兩級格式的,符號自身+加上自己所在的單元,PIE標識是位置無關的。

4. 加載命令

上面頭部中的數據已經說明了整個Mach-O文件的基本信息,但整個Mach-O中最重要的還要數加載命令。它說明了操作系統應當如何加載文件中的數據,對系統內核加載器和動態鏈接器起指導作用。一來它描述了文件中數據的具體組織結構,二來它也說明了進程啓動後,對應的內存空間結構是如何組織的。

我們可以用otool -l xxx來看一個Mach-O文件的加載命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Load command 0
      cmd LC_SEGMENT
  cmdsize 56
  segname __PAGEZERO
   vmaddr 0x00000000
   vmsize 0x00004000
  fileoff 0
 filesize 0
  maxprot ---
 initprot ---
   nsects 0
    flags (none)
Load command 1
      cmd LC_SEGMENT
  cmdsize 736
  segname __TEXT
   vmaddr 0x00004000
   vmsize 0x00390000
  fileoff 0
 filesize 3735552
  maxprot r-x
 initprot r-x
   nsects 10
    flags (none)
Section
  sectname __text
   segname __TEXT
      addr 0x0000b0d0
      size 0x0030e7f4

上面這段是執行結果的一部分,是加載PAGE_ZERO和TEXT兩個segment的load command。PAGE_ZERO是一段“空白”數據區,這段數據沒有任何讀寫運行權限,方便捕捉總線錯誤(SIGBUS)。TEXT則是主體代碼段,我們注意到其中的r-x,不包含w寫權限,這是爲了避免代碼邏輯被肆意篡改。

我再提一個加載命令,LC_MAIN。這個加載指令,會聲明整個程序的入口地址,保證進程啓動後能夠正常的開始整個應用程序的運行。

除此之外,Mach-O裏還有LC_SYMTAB、LC_LOAD_DYLIB、LC_CODE_SIGNATURE等加載命令,大家可以去官方文檔查找其含義。

至於Data部分,在瞭解了頭部和加載命令後,就沒什麼特別可說的了。Data是最原始的編譯數據,裏面包含了Objective-C的類信息、常量等。

本文是對Mach-O文件格式的一個理解小結,希望能夠拋磚引玉,幫助各位朋友把握可執行文件的主題脈絡,進而解決各類問題。

參考:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章