趣探 Mach-O:載入過程
這是Mach-O系列的第二篇,趣探 Mach-O:檔案格式分析是本文的一個基礎
我們都知道 Mach-O
是 OS X
系統的可執行檔案,說到可執行檔案肯定離不開程序。在 Linux
中,我們會通過 Fork()
來新建立子程序,然後執行映象通過exec()
來替換為另一個可執行程式,至於為什麼這麼做,解釋如下
原因闡述:這是基於作業系統方面的分析。程序可以通過
fork()
系統呼叫來建立子程序,子程序得到與父程序地址空間相同的(但是獨立的)一份拷貝,包括文字、資料和bss
段、堆以及使用者棧等,但是新執行緒只會複製呼叫fork
的執行緒。所有父程序中別的執行緒,到了子程序中都是突然“蒸發”掉的。我們線上程問題中經常會提到鎖,每個鎖都有一個持有者(最後一次
lock
它的執行緒)。為了效能,鎖物件會因為fork
複製到子程序中,但是子程序只複製呼叫fork
的執行緒,很可能並不擁有鎖持有者執行緒,那麼就沒有辦法解開鎖,導致死鎖問題、記憶體洩漏避免死鎖的方法:在子執行緒中馬上呼叫
exec
函式,一個程序一旦呼叫exec
類函式,它本身就”死亡”了,系統把程式碼段替換成新的程式的程式碼,廢棄原有的資料段和堆疊段,併為新程式分配新的資料段與堆疊段,唯一留下的,就是程序號,也就是說,對系統而言,還是同一個程序,不過已經是另一個程式了。
綜上所述,我們在使用者態會通過exec*
系列函式來載入一個可執行檔案,同時exec*
都只是對系統呼叫execve
的封裝,那我們載入Mach-O
execve
說起。Mach-O
有多種檔案型別,比如MH_DYLIB
檔案、MH_BUNDLE
檔案、MH_EXECUTE
檔案(這些需要dyld
動態載入),MH_OBJECT
(核心載入)等。所以一個程序往往不是隻需要核心載入器就可以完成載入的,需要dyld
來進行動態載入配合。考慮核心載入和dyld
載入兩種情況,就會有如下流程圖
execve
這個函式只是直接呼叫 __mac_execve()
,對於原始碼內部實現細節,可以看XNU的原始碼
__mac_execve()
原始碼可以參考:
bsd/kern/kern_exec.c
主要是為載入映象進行資料的初始化,以及資源相關的操作,在其內部會執行exec_activate_image()
123456789101112 | int__mac_execve(proc_tp,struct__mac_execve_args *uap,int32_t *retval){structimage_params *imgp;// 初始化imgp資料.......exec_activate_image(imgp);} |
exec_activate_image
原始碼可以參考:
bsd/kern/kern_exec.c
主要是拷貝可執行檔案到記憶體中,並根據不同的可執行檔案型別選擇不同的載入函式,所有的映象的載入要麼終止在一個錯誤上,要麼最終完成載入映象。在OS X中專門處理可執行檔案格式的程式叫execsw
映象載入器
OS X有三種可執行檔案,mach-o
由exec_mach_imgact
處理,fat binary
由exec_fat_imgact
處理,interpreter
(直譯器)由exec_shell_imgact
處理
exec_mach_imgact
原始碼可以參考:
bsd/kern/kern_exec.c
主要是用來對Mach-O
做檢測,會檢測Mach-O
頭部,解析其架構、檢查imgp
等內容,並拒絕接受Dylib
和Bundle
這樣的檔案,這些檔案會由dyld
負責載入
然後把Mach-O
對映到記憶體中去,呼叫load_machfile()
load_machfile
原始碼可以參考:
bsd/kern/mach_loader.c
load_machfile
會載入Mach-O
中的各種load monmand
命令。在其內部會禁止資料段執行,防止溢位漏洞攻擊,還會設定地址空間佈局隨機化(ASLR),還有一些對映的調整。
真正負責對載入命令解析的是parse_machfile()
parse_machfile
原始碼可以參考:
bsd/kern/mach_loader.c
parse_machfile
會根據load_command
的種類選擇不同的函式來載入,內部是一個Switch
語句來實現的
常見的命令有LC_SEGMENT_64
、LC_LOAD_DYLINKER
、LC_CODE_SIGNATURE
、LC_UUID
等,更多命令可以檢視Mach-O
檔案格式,也可以查詢這篇文章-趣探 Mach-O:檔案格式分析
對於命令的載入會進行多次掃描,當掃描三次之後,並且存在dylinker_command
命令時,會執行 load_dylinker()
,啟動動態連結器 (dyld)
動態連結過程
動態連結也有區分,一種是載入主程式(很多部落格裡這麼寫),是由load commands
指定的dylib
,以靜態的方式存放在二進位制檔案裡,一種是由DYLD_INSERT_LIBRARIES
動態指定。下面這種就是提前指定在二進位制檔案中的動態庫,下面的闡述主要是站在前者的角度,對於動態指定的後期再研究
你可以在 load
方法處打一個斷點看一下,通過檢視呼叫棧可以發現:
123456 | 0+[XXObject load]1call_class_loads()2call_load_methods3load_images4dyld::notifySingle(dyld_image_states,ImageLoader const*)11_dyld_start |
_dyld_start
甚是耀眼,覺得是dyld
的入口,然後就去看dyld
的原始碼,全域性搜了一下_dyld_start
,就發現註釋,於是順著註釋往下閱讀
原始碼可以參考:
dyld/src/dyld.cpp
核心會載入dyld
並呼叫dyld_start
方法,隨後dyld_start
會呼叫_main()
,在_main函式中對資料進行一通初始化之後,就會呼叫instantiateFromLoadedImage
函式初始化ImageLoader
例項
12 | // instantiate ImageLoader for main executablesMainExecutable=instantiateFromLoadedImage(mainExecutableMH,mainExecutableSlide,sExecPath); |
instantiateFromLoadedImage
12345678910111213 | // The kernel maps in main executable before dyld gets control. We need to // make an ImageLoader* for the already mapped in main executable.staticImageLoader*instantiateFromLoadedImage(constmacho_header*mh,uintptr_t slide,constchar*path){// try mach-o loaderif(isCompatibleMachO((constuint8_t*)mh,path)){ImageLoader*image=ImageLoaderMachO::instantiateMainExecutable(mh,slide,path,gLinkContext);addImage(image);returnimage;}throw"main executable not a known format";} |
instantiateFromLoadedImage
這個函式內的程式碼比較容易理解,檢測Mach-O
是否合法,合法的話就初始化ImageLoader
例項,然後將其加入到一個全域性的管理ImageLoader
的陣列中去
isCompatibleMachO
會對Mach-O
頭部的一些資訊與當前平臺進行比較,判斷其合法性。
ImageLoader
12345678910111213 | //// ImageLoader is an abstract base class. To support loading a particular executable// file format, you make a concrete subclass of ImageLoader.//// For each executable file (dynamic shared object) in use, an ImageLoader is instantiated.//// The ImageLoader base class does the work of linking together images, but it knows nothing// about any particular file format.////classImageLoader{} |
註釋可得,ImageLoader
是一個抽象基類,每一個動態載入的可執行檔案都會初始化一個ImageLoader
例項
instantiateMainExecutable
原始碼可以參考:
dyld/src/ImageLoaderMachO.cpp
1234567891011121314151617181920 | // create image for main executableImageLoader*ImageLoaderMachO::instantiateMainExecutable(constmacho_header*mh,uintptr_t slide,constchar*path,constLinkContext&context){boolcompressed;unsignedintsegCount;unsignedintlibCount;constlinkedit_data_command*codeSigCmd;constencryption_info_command*encryptCmd;sniffLoadCommands(mh,path,false,&compressed,&segCount,&libCount,context,&codeSigCmd,&encryptCmd);// instantiate concrete class based on content of load commandsif(compressed)returnImageLoaderMachOCompressed::instantiateMainExecutable(mh,slide,path,segCount,libCount,context);else#if SUPPORT_CLASSIC_MACHOreturnImageLoaderMachOClassic::instantiateMainExecutable(mh,slide,path,segCount,libCount,context);#elsethrow"missing LC_DYLD_INFO load command";#endif} |
可以這裡會有一個Bool
型別的compressed
作為判斷,然後返回不同的例項。
這兩種例項都是做什麼?
ImageLoaderMachOCompressed
與ImageLoaderMachOClassic
均繼承於ImageLoaderMachO
,ImageLoaderMachO
繼承於ImageLoader
sniffLoadCommands
會對Mach-O
是classic
還是compressed
的做一個判斷
instantiateMainExecutable
是對ImageLoaderMachOCompressed
或ImageLoaderMachOClassic
做初始化,並載入load comond
命令,之中呼叫過程也比較簡單,可以參考網路的這個圖片,比較清晰,具體內容可以看原始碼
至此,主二進位制檔案的載入就結束,然後會連結各種DYLD_INSERT_LIBRARIES
動態庫,以上就是非常粗略的瞭解了下載入Mach-O
的過程,後期有需要再深入研究
寫的比較倉促,難免有錯誤的地方,望包涵並告知