初始mach-o文件及在項目中應用

2024年2月6日 20点热度 0人点赞

01

認識mach-o的必要性

了解mach-o的結構可以幫助認識系統加載二進制文件的動態鏈接和靜態鏈接。應用層面,使用initializec 函數計算啟動時間耗時也需要以mach-o的結構知識為鋪墊。還可以用在使用clang自註冊啟動任務上。後續會一一展開說明。

02

mach-o的定義

mach-o是mach object的縮寫,是存儲程序或庫的標準格式。app的mach-o又稱為可執行文件,靜態庫的.a文件也為mach-o文件,還有諸如此類的一些文件。

  • .o目標文件:MH_OBJECT
  • 靜態庫文件.a : MH_OBJECT
  • 可執行文件:MH_EXECUTE
  • 動態庫:MH_DYLIB
  • dyld:MH_DYLINKER
  • 符號信息:MH_DSYM。可在loader.h的源碼中看到全部的mach-o文件。

想要深入了解mach-o,可以自己創建一個,在xocde的build setting --> mach-o type 下選擇類型,依據xcode的提示步驟可以創建出。如果已經有了文件,想知道是否為mach-o,也可以使用xcode打開文件,在build setting --> mach-o type下查看屬於哪種類型。xcode

的查看示意見下圖:

03

mach-o的結構

查看mach-o的內部結構需要借助於工具mach-o View下載地址。目前下載下來之後需要在mac上運行使用。舉例,使用mach-o View查看app的可執行文件,首先需要編譯項目,這樣會生成.app文件,然後項目中搜索.app:

右鍵show in finder,就會找到app包。如下圖:

查找可執行文件,文件以項目名稱命名的。下圖中第一個就是:

通過打開mach-o View可以看到,mach-o分為三大部分,無論是什麼類型的mach-o文件,都分為3大部分。

  • mach-o header 描述了Mach-o的cpu框架以及加載命令等信息;
  • load Commands 記錄虛擬內存中的佈局例如有哪些段,段從哪開始,段占用多大空間;
  • data 記錄段的具體數據。如下圖,三大部分在mach-o中分佈:

1、第一部分:mach-o Header 詳解

mach-o header 的結構如下圖中紅框展示:

  • magic number :系統加載器通過該字段判斷文件適用於32位還是64位;
  • cpu type:cpu類型,該字段確保系統可以將合適的二進制文件在當下架構下進行,為x86,arm64等;
  • file type :說明文件類型(可執行文件、庫文件、核心轉儲文件、內核擴展文件、DYSM文件、動態庫等)mach-o為MH-EXECUTE.;
  • number of load command 說明加載命令的條數;
  • size of load commands 表示加載命令的大小;

如上所述,header介紹了文件的基礎信息。

2、第二部分:mach-o內容

mach-o的內容部分分為load commands和 data。

  • load commands 如圖所示:

每一個命令的含義下表:

命令名稱命令含義LC_SEGMENT_64將文件中的段映射到進程地址空間LC_DYLD_INFO_ONLYdyld相關信息LC_SYMTAB加載全局符號表信息LC_DYSYMTAB動態鏈接符號表信息LC_DYLD_INFO_ONLYdyld相關信息LC_LOAD_DYLINKER加載一個動態鏈接器,也就是加載dyldLC_UUIDapp的uuidLC_VERSION_MIN_IPHONEOS支持最低系統版本LC_MAIN設置程序主線程的入口地址LC_LOAD_DYLIB(動態庫名稱)加載相應的動態庫LC_FUNCTION_STARTS函數啟示地址表LC_CODE_SIGNATURE代碼簽名

loader.h文件中可查看命令的官方註釋。

  • data部分的內容如圖所示:

如圖所示,data有2種段數據,一種為__TEXT段,一種為__DATA段。__text段是Mach-O文件中存儲代碼的一個特定段,它包含了程序的實際可執行代碼。在__text段中,存儲了程序的實際指令和函數定義。當程序被加載到內存中並執行時,操作系統會將__text段中的代碼加載到內存中,並按照指令逐條執行,從而實現程序的功能。 __text段通常是以隻讀方式存儲在Mach-O文件中,以確保代碼的完整性和安全性。這意味著在程序運行時,__text段中的代碼是不可被修改的,這有助於防止惡意軟件對程序代碼進行篡改。__data段是用來存儲程序的靜態數據的一個特定段。__data段包含了程序中的靜態全局變量、靜態局部變量和其他靜態數據,這些數據在程序運行時需要被初始化和使用。與代碼段__text不同,__data段存儲的是程序運行時需要進行讀寫操作的數據。

__text各個段的含義:

名稱作用TEXT.text隻有可執行的機器碼TEXT.cstring去重後的C字符串TEXT.const初始化過的常量TEXT.stubs符號樁。本質上是一小段會直接跳入lazybinding的表對應項指針指向的地址的代碼。TEXT.stub_helper輔助函數。上述提到的lazybinding的表中對應項的指針在沒有找到真正的符號地址的時候,都指向這。TEXT.unwind_info用於存儲處理異常情況信息TEXT.eh_frame調試輔助信息_objc_classname類名稱objc_methlist方法列表

__text段在mach-o中的釋義:

__data 各個段的含義:

名稱

作用

DATA.data

初始化過的可變的數據

DATA.nl_symbol_ptr

非lazy-binding的指針表,dyld 加載會立即綁定

DATA.la_symbol_ptr

lazy-binding的指針表,每個表項中的指針一開始指向stub_helper

DATA.const

沒有初始化過的常量

DATA.mod_init_func

初始化函數,在main之前調用

DATA.mod_term_func

終止函數,在main返回之後調用

DATA.bss

沒有初始化的靜態變量

DATA.common

沒有初始化過的符號聲明

DATA.__objc_nlclslist

實現了 load 方法的類

__data 在mach-o中的展示:

04

mach-o的應用

認識了mach-o,可以將其運用在統計啟動時期c static initializer 階段耗時。c static initializer 階段系統做了什麼,一個是c 的構造函數屬性函數,一個是非基礎類型的c

靜態全局變量的創建(通常是類或結構體)。在構造函數上打斷點,可以得到如圖:

從dyld的源碼中可以看到doModeInitFunction()

的具體執行。如下圖所示:

從dyld的源碼中可以看出,取出mod_init_func section 中的元素並執行。可以看出,mod_init_func section中存儲的是函數地址,類型為initialize.了解這些之後,自己寫一個帶有計時的start和end函數,並在中間調用源函數地址。然後hook mod_init_func 中的所有地址,並替換執行自己的函數。步驟如下:

  • hook mod_init_func中的所有地址 。因為__mod_init_func section 位於__DATAsegment.__DATA segment 是數據段,是可以在運行時被修改的。並且, load方法的執行是在dyld讀取這些initializer之前。所以hook mod_init_func中的所有地址是可行的;
  • 修改mod_init_func數據。利用getsectiondata獲取到segment的每一個數據,將自己寫的方法替換表中的方法;
  • 調用原來的initializer。自己的Initializer中逐個獲取每一個原函數地址,調用並計算耗時獲取。

通過以上步驟,我們可以得出這一項的耗時,從而做出優化。

認識mach-o,是註冊啟動任務的必備知識。做註冊啟動任務的必要性有兩點。

  1. 啟動代碼集中在AppDelegate中,代碼逐漸臃腫,易讀性降低,且代碼之間耦合度高;
  2. 各個業務方加啟動任務,都需要啟動業務配合。

我們利用mach-o結構的__DATA可讀寫性。所以可以通過clang的section函數在編譯階段寫入macho文件中一個__DATA段。__DATA段存儲函數指針的指針。具體的使用步驟為:

  1. 編寫註冊方法的宏,提供給外部使用;
  2. 業務方註冊任務,註冊的每個時機都會在編譯期間新增一個__DATA類型section,存儲任務函數;
  3. App運行,在註冊的時機函數中使用getsectbynamefromheader_64遍歷取出相應Section中的函數,並依次執行。

代碼如下:

static void Launch_Func_(void);\
__attribute__((used, section("__DATA, "#period""))) static const void * __Func__= Launch_Func_;\
static void Launch_Func_(void)
#define RegisterLaunchTaskOnWillFinishLaunchPeriod\
        RegisterLaunchTask(willFinishLaunch)
#define RegisterLaunchTaskOnDidFinishLaunchPeriod\
        RegisterLaunchTask(didFinishLaunch)
#define RegisterLaunchTaskOnDidFinishADPeriod\
        RegisterLaunchTask(didFinishAD)
#define RegisterLaunchTaskOnDidFinishHomepagePeriod\
        RegisterLaunchTask(didFinishHome)        

各個業務的使用代碼如下:

RegisterLaunchTaskOnDidFinishHomepagePeriod{
    /*do sth*/
}

參考資料:

1.https://everettjf.github.io/2017/02/06/a-method-of-hook-static-2.initializers/# https://github.com/fangshufeng/MachOView

作者:李贊

來源:微信公眾號:搜狐技術產品

出處
:https://mp.weixin.qq.com/s/c3bgebQUcXXetiFAdiXjsQ