最近我意識到,對可執行檔案如何與作業系統互動所知甚少。我寫一些C程式碼,它被編譯,彙編,並靜態連結,然後一些魔法發生,我寫的東西不知道怎麼就被載入並執行了。這篇文章是關於魔法背後的一些玄機,特別是解剖 OS X 的 Mach-O ABI 機制。
我開始了這個探索的過程,通過寫一個簡單的”Hello World”,我推測它可能會生成一個易於解釋的輸出檔案。當然,我可以通過大量閱讀了解這一切,但那不好玩。我倒是更喜歡親自去探索,看看它能帶我到哪,要是被卡住就深入研究下。程式碼如下:
1 2 3 4 5 6 7 |
#include <stdio.h>; int main() { fwrite("Hello, world!\n", 1, 15, stdout); return 0; } |
接下來在 2013 款 Macbook Yosemite 系統執行 gcc hello-world.c -o hello.out (如果我沒有記錯的話實際上是Clang在OS X上的偽裝),完成後用十六進位制編輯器開啟結果並開始分析。說實話,我沒想到僅兩行C程式碼消耗了我這麼多時間。我不想解釋在這裡詳細地解釋每個8548輸出位元組—-這將耗時頗靡,且讀之乏味。相反,我會試圖給出一個比較簡短的概括。放輕鬆,如果你有著跟我類似的環境,一個人在家同你自個兒的二進位制檔案玩去吧。
生成檔案的前四個位元組是cf fa ed fe – 毫無疑問,某種標準檔案頭。谷歌一下發現,這確實是一個小端64位的Mach-O二進位制檔案的標頭檔案。這是個好的開始!事實證明,Mach-O格式,是標準的OS X(和iOS)的程式和庫檔案儲存格式 – 這兒甚至還有個官方參考檔案,應該對你很有用。
那麼有一個更好的想法,當然也是我們正要處理的,讓我們後退到一秒前,來獲取佈局檔案的概述。下面是生成的二進位制(通過一個我寫的應用程式生成)的Cortesi的風格的視覺化。如果你不熟悉它,各個位元組繪製在一個塊填充曲線和並根據其權值染色,從而具有相似位置的位元組顯示在視覺上相關的區域,並染以相關的顏色。
從這我們可以看到,似乎是伴隨著很多黑色塊(零值位元組)明顯分開的區域。來看一下檔案格式參考,蘋果提供了以下圖表來描述的Mach-O格式的佈局:
瞭解格式的小知識之後,你就可以開始在上述兩圖之間畫等號。該檔案開頭的資料是Mach-O的檔案頭和載入指令,然後我們有一些“段”裡的資料(通常在段內的section),從0×00位元組,一直填充段到頁邊界(在本例子是4096位元組)。
特別是,上述的黃色區域是檔案頭,紅色中包含的載入指令,以及綠色,藍色,紫色區域是(部分的)段。讓我們來談談這每一個的細節。
MACH-O檔案頭
Mach-O檔案頭相對易於理解—-它包括了檔案的前32個位元組,可以通過檢視“mach_header_64” 結構逐位元組理解 。otool工具自動為我們做好了,幹得好。執行“otool -h hello.out ”揭示了檔案頭的所有重要資訊:
1 2 3 4 5 |
$ otool -h hello.out hello.out: Mach header magic cputype cpusubtype caps filetype ncmds sizeofcmds flags 0xfeedfacf 16777223 3 0x80 2 16 1376 0x00200085 |
蘋果的開原始碼(包括otool的程式碼)提供了內部的大量細節,整個過程中這對我很有用,除了總結輸出:
- 0xfeedfacf (從這個檔案的小端表示重新排序了)是個64位檔案頭的魔數常量,(MH_MAGIC_64/MH_CIGAM_64 ,在loader.h)。
- “cputype”是x86_64 CPU 型別值(CPU_TYPE_X86_64, 在machine.h)。
- “cpusubtype”是所有x86_64 處理器的值(CPU_SUBTYPE_X86_64_ALL),附加64位庫相容需要的“capability bits”(CPU_SUBTYPE_LIB64 ,在machine.h)。
- “filetype”是一種按需分頁的可執行檔案(loader.h裡的MH_EXECUTE )。
- “ncmds”和“sizeofcmds”域表明16個載入指令緊隨其後,總大小1376位元組。
- “flags”域設定了一堆我們檔案的標誌:我們的檔案無未定義引用 (MH_NOUNDEFS),是為動態連結器(MH_DYLDLINK),使用two-level名Cheng繫結(MH_TWOLEVEL)且應被載入到隨機地址 (MH_PIE).
載入指令
根據檔案格式參考,載入指令“指定虛擬記憶體中的檔案邏輯結構和檔案佈局”。 他們在 Mach-O檔案格式的中央,而我們的檔案頭說我們有16個指令緊隨其後,讓我們來看一下。
你可以再一次按照Mach-O檔案格式參考逐位元組閱讀(這次看一下“load_command’”結構和它的密友們),但otool讓事情變簡單了。 執行“otool -l hello.out ”提供檔案中所有載入指令的詳細資訊—- 我不想在本貼分析過一下細節,無論如何。Mach-O格式參考總結了一部分載入指令格式而非所有,所以我會自己來做個格式綜覽。
- LC_SEGMENT_64:定義一個(64位)段, 當檔案載入後它將被對映到地址空間。包括段內節(section)的定義。
- LC_SYMTAB:為該檔案定義符號表(‘stabs’ 風格)和字串表。 他們在連結檔案時被連結器使用,同時也用於偵錯程式對映符號到原始檔。具體來說,符號表定義本地符號僅用於除錯,而已定義和未定義external 符號被連結器使用。
- LC_DYSYMTAB:提供符號表中給出符號的額外符號資訊給動態連結器,以便其處理。 包括專門為此而設的一個間接符號表的定義。
- LC_DYLD_INFO_ONLY:定義一個附加 壓縮的動態連結器資訊節,它包含在其他事項中用到的 動態繫結符號和操作碼的後設資料。stub 繫結器(“dyld_stub_binder”),它涉及的動態間接連結利用了這點。 “_ONLY” 字尾t表明這個載入指令是程式執行必須的,, 這樣那些舊到不能理解這個載入指令的連結器就在這裡停下。
- LC_LOAD_DYLINKER: 載入一個動態連結器。在OS X上,通常是“/usr/lib/dyld”。LC_LOAD_DYLIB: 載入一個動態連結共享庫。舉例來說,“/usr/lib/libSystem.B.dylib”,這是C標準庫的實現再加上一堆其他的事務(系統呼叫和核心服務,其他系統庫等)。每個庫由動態連結器載入幷包含一個符號表,符號連結名稱是查詢匹配的符號地址。
- LC_MAIN:指明程式的入口點。在本案例,是函式themain()的地址。
- LC_UUID:提供一個唯一的隨機UUID,通常由靜態連結器生成。
- LC_VERSION_MIN_MACOSX:程式可執行的最低OS X版本要求
- LC_SOURCE_VERSION::構建二進位制檔案的原始碼版本號。
- LC_FUNCTION_STARTS:定義一個函式起始地址表,使偵錯程式和其他程式易於看到一個地址是否在函式內。
- LC_DATA_IN_CODE:定義在程式碼段內的非指令的表。
- LC_DYLIB_CODE_SIGN_DRS: 為已連結的動態庫定義程式碼簽名 指定要求。
哇,這麼快就理解了!我們甚至還沒有看到這個可執行檔案載入指令的更深處,只是看到載入指令的型別!如果你現在還沒有得到所有的理論,不要擔心。本質上,載入指令只是提供了一堆各種各樣的資訊或是有關檔案空閒處的資料(定義/引用中發生的資料塊),或者直接有關的可執行檔案。這個資訊大大鞏固了我們檔案空閒部分的全部內容。
節和段(SECTION & SEGMENTS)
更深入地來看載入指令,某一段/節結構通過“LC_SEGMENT_64”指令定義,且被許多其他載入指令引用。該檔案的其餘基本上是用有意義的資料填充這個結構。所有在我們檔案中定義的段描述如下:
- __PAGEZERO:一個全用0填充的段,用於抓取空指標引用。這通常不會佔用磁碟空間 (或記憶體空間),因為它執行時對映為一群0啊。順便說一句,這個段是隱藏惡意程式碼的好地方。
- __TEXT:本段只有可執行程式碼和其他只讀資料。
- __text:本段是可執行機器碼。
- __stubs:間接符號存根。這些跳轉到非延遲載入 (“隨執行載入”) 和延遲載入(“初次使用時載入”) 間接引用的(可寫)位置的值 (例如條目“__la_symbol_ptr”,我們很快就會看到) 。對於延遲載入引用,其地址跳轉講首先指向一個解析過程,但初始化解析後會指向一個確定的地址。 對於非延遲載入引用,其地址跳轉會始終指向一個確定的地址,因為動態連結器在載入可執行檔案時就修正好了。
- __stub_helper:提供助手來解決延遲載入符號。如上所述,延遲載入的間接符號指標將指到這裡面,直到得到確定地址。
- __cstring: constant (只讀) C風格字串(如”Hello, world!n”)的節。連結器在生成最終產品時會清除重複語句。
- __unwind_info:一個緊湊格式,為了儲存堆疊展開資訊供處理異常。此節由連結器生成,通過“__eh_frame”裡供OS X異常處理的資訊。
- __eh_frame: 一個標準的節,用於異常處理,它提供堆疊展開資訊,以DWARF格式。
- __DATA:用於讀取和寫入資料的一個段。
- __nl_symbol_ptr:非延遲匯入符號指標表。
- __la_symbol_ptr:延遲匯入符號指標表。本節開始時,指標們指向解析助手,如前所討述。
- __got:全域性偏移表 –— (非延遲)匯入全域性指標表。
- __LINKEDIT:包含給連結器(“連結編輯器‘)的原始資料的段,在本案例中,包括符號和字串表,壓縮動態連結資訊,程式碼簽名存託憑證,以及間接符號表 – 所有這一切的佔區都被載入指令指定了。
全域性思考
隨著對載入指令,段,節的知識以及所有他們的用途,這樣應該不會太難去看出當二進位制被執行時發生了什麼的全域性思考。我們已經討論了許多發生在動態連結和執行二進位制檔案時候的過程。從本質上講:
- 我們編譯和靜態連結產生的Mach-O輸出成為動態連結器的輸入,它使用在我們檔案中被載入指令指明的資料,以各種方式連結依賴。
- 可執行檔案的段被對映到記憶體中,按載入指令中指明的。
- 執行開始於“LC_MAIN”指定的點,在本案例是“__TEXT.__”文字開頭。
進入稍微更深的細節(攜此文件的幫助下),這裡是專門為我們的“Hello World”二進位制檔案的整個過程的大致輪廓:
- 使用者表明他們希望要執行這個二進位制檔案。
- 它確定該檔案是一個有效的Mach-O檔案,所以核心為程式(for)建立一個程式並開始程式執行過程(execve)。
- 核心檢查的Mach-O檔案頭並載入程式,配合以指定的動態聯結器(“/ usr / lib/ dyld的”),進入載入指令指定分配的地址空間。段的虛擬記憶體保護標誌也按指示新增上(例如__TEXT是隻讀)。
- 核心執行動態連結器,它載入所有引用的庫 —- 在本案例是“/usr/lib/libSystem.B.dylib” —- 並執行啟動程式必須的符號繫結(即非延遲引用),搜尋載入庫以匹配符號。
- 假設符號已經被正確解析,動態聯結器把結果地址置入section,即那些它在間接符號表(由“LC_DYSYMTAB”定義)中掌控主導(如它們的載入指令條目指定)的相應條目的部分。在本案例,確定地址被置於“_nl_symbol_ptr”和“__got”。
- 一些初始化程式碼執行設定執行時狀態,在所指定的入口點“LC_MAIN”之後!
- 當一個延遲繫結的應用第一次使用(通過“__stubs”),“__la_symbol_ptr”條目應該指向一個在“__stub_helpers”裡的解析例程(由於構建過程靜態連結器的準備),它呼叫“dyld_stub_binder”(這是當我們的程式被載入時動態連結的),以執行解析和更新“__la_symbol_ptr”裡的地址。
當然,如果你想進一步瞭解的話還有很多更具體的細節,如果你想要看。對於Mach-O的探索,我建議使用otool和MachOView,以及一個合適量的開原始碼,說明書和模糊的線上資源。有一堆的Mach-O部分和動態連結我們甚至還沒有觸及到。例如,弱繫結,是一個不同型別的符號繫結,它僅當連結符號是在系統上可用時才連結。
如果你有興趣看“Hello World”程式的__TEXT.__文字的彙編程式碼,objdump使反編譯工作更容易 –—- 雖然,當然我們也可以從編譯器第一手得到這樣的輸出:“gcc -S hello-world.c -o hello.s”。目前已經有一些資源,仔細檢查了C“Hello World”的彙編,所以我沒興趣涵蓋這一部分。無論如何,本文還是側重於介紹現代系統二進位制執行過程中常被遺忘的一些細節。
打賞支援我翻譯更多好文章,謝謝!
打賞譯者
打賞支援我翻譯更多好文章,謝謝!
任選一種支付方式