iOS-底層原理 32:啟動優化(一)基本概念

iOS 底層原理 文章匯總

虛擬內存 & 物理內存

早期的數據訪問是直接通過物理地址訪問的,這種方式有以下兩個問題:

  • 1、內存不夠用

  • 2、內存數據的安全問題

內存不夠用的方案:虛擬內存

針對問題1,我們在進程和物理內存之間增加一個中間層,這個中間層就是所謂的虛擬內存,主要用于解決當多個進程同時存在時,對物理內存的管理。提高了CPU的利用率,使多個進程可以同時、按需加載。所以虛擬內存其本質就是一張虛擬地址和物理地址對應關系的映射表

  • 每個進程都有一個獨立的虛擬內存,其地址都是從0開始,大小是4G固定的,每個虛擬內存又會劃分為一個一個的(頁的大小在iOS中是16K,其他的是4K),每次加載都是以頁為單位加載的,進程間是無法互相訪問的,保證了進程間數據的安全性。

  • 一個進程中,只有部分功能是活躍的,所以只需要將進程中活躍的部分放入物理內存,避免物理內存的浪費

  • 當CPU需要訪問數據時,首先是訪問虛擬內存,然后通過虛擬內存去尋址,即可以理解為在表中找對應的物理地址,然后對相應的物理地址進行訪問

  • 如果在訪問時,虛擬地址的內容未加載到物理內存,會發生缺頁異常(pagefault),將當前進程阻塞掉,此時需要先將數據載入到物理內存,然后再尋址,進行讀取。這樣就避免了內存浪費

如下圖所示,虛擬內存與物理內存間的關系


虛擬內存與物理內存

內存數據的安全問題:ASLR技術

在上面解釋的虛擬內存中,我們提到了虛擬內存的起始地址與大小都是固定的,這意味著,當我們訪問時,其數據的地址也是固定的,這會導致我們的數據非常容易被破解,為了解決這個問題,所以蘋果為了解決這個問題,在iOS4.3開始引入了ASLR技術。

ASLR的概念:(Address Space Layout Randomization ) 地址空間配置隨機加載,是一種針對緩沖區溢出安全保護技術,通過對堆、棧、共享庫映射等線性區布局的隨機化,通過增加攻擊者預測目的地址的難度,防止攻擊者直接定位攻擊代碼位置,達到阻止溢出攻擊的目的的一種技術。

其目的的通過利用隨機方式配置數據地址空間,使某些敏感數據(例如APP登錄注冊、支付相關代碼)配置到一個惡意程序無法事先獲知的地址,令攻擊者難以進行攻擊。

由于ASLR的存在,導致可執行文件和動態鏈接庫在虛擬內存中的加載地址每次啟動都不固定,所以需要在編譯時來修復鏡像中的資源指針,來指向正確的地址。即正確的內存地址 = ASLR地址 + 偏移值

可執行文件

不同的操作系統,其可執行文件的格式也不同。系統內核將可執行文件讀取到內存,然后根據可執行文件的頭簽名(magic魔數)判斷二進制文件的格式

可執行文件

其中PE、ELF、Mach-O這三種可執行文件格式都是COFF(Command file format)格式的變種,COFF的主要貢獻是目標文件里面引入了“段”的機制,不同的目標文件可以擁有不同數量和不同類型的“段”。

通用二進制文件

因為不同CPU平臺支持的指令不同,比如arm64x86,蘋果中的通用二進制格式就是將多種架構的Mach-O文件打包在一起,然后系統根據自己的CPU平臺,選擇合適的Mach-O,所以通用二進制格式也被稱為胖二進制格式,如下圖所示

胖二進制格式

通用二進制格式的定義在<mach-o/fat.h>中,可以在下載xnu,然后根據 xnu -> EXTERNAL_HEADERS ->mach-o中找到該文件,通用二進制文件開始的Fat Headerfat_header結構體,而Fat Archs是表示通用二進制文件中有多少個Mach-O,單個Mach-O的描述是通過fat_arch結構體。兩個結構體的定義如下:

/*
 - magic:可以讓系統內核讀取該文件時知道是通用二進制文件
 - nfat_arch:表明下面有多個fat_arch結構體,即通用二進制文件包含多少個Mach-O
 */
struct fat_header {
    uint32_t    magic;      /* FAT_MAGIC */
    uint32_t    nfat_arch;  /* number of structs that follow */
};

/*
 fat_arch是描述Mach-O
 - cputype 和 cpusubtype:說明Mach-O適用的平臺
 - offset(偏移)、size(大小)、align(頁對齊)描述了Mach-O二進制位于通用二進制文件的位置
 */
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 */
};

所以,綜上所述,

  • 通用二進制文件是蘋果公司提出的一種新的二進制文件的存儲結構,可以同時存儲多種架構的二進制指令,使CPU在讀取該二進制文件時可以自動檢測并選用合適的架構,以最理想的方式進行讀取

  • 由于通用二進制文件會同時存儲多種架構,所以比單一架構的二進制文件大很多,會占用大量的磁盤空間,但由于系統會自動選擇最合適的,不相關的架構代碼不會占用內存空間,且執行效率高

  • 還可以通過指令來進行Mach-O的合并與拆分

    • 查看當前Mach-O的架構:lipo -info MachO文件

    • 合并:lipo -create MachO1 MachO2 -output 輸出文件路徑

    • 拆分:lipo MachO文件 –thin 架構 –output 輸出文件路徑

Mach-O文件

Mach-O文件是Mach Object文件格式的縮寫,它是用于可執行文件、動態庫、目標代碼的文件格式。作為a.out格式的替代,Mach-O格式提供了更強的擴展性,以及更快的符號表信息訪問速度

熟悉Mach-O文件格式,有助于更好的理解蘋果底層的運行機制,更好的掌握dyld加載Mach-O的步驟。

查看Mach-O文件
如果想要查看具體的Mach-O文件信息,可以通過以下兩種方式,推薦使用第二種方式,更直觀

  • 【方式一】otool終端命令:otool -l Mach-O文件名
    命令

-【方式二】 MachOView工具(推薦):將Mach-O可執行文件拖動到MachOView工具打開

MachOView

Mach-O文件格式

對于OS X 和iOS來說,Mach-O是其可執行文件的格式,主要包括以下幾種文件類型

  • Executable:可執行文件
  • Dylib:動態鏈接庫
  • Bundle:無法被鏈接的動態庫,只能在運行時使用dlopen加載
  • Image:指的是Executable、Dylib和Bundle的一種
  • Framework:包含Dylib、資源文件和頭文件的集合

下面圖示是Mach-O 鏡像文件格式

Mach-O鏡像文件

以上是Mach-O文件的格式,一個完成的Mach-O文件主要分為三大部分:

  • Header Mach-O頭部:主要是Mach-O的cpu架構,文件類型以及加載命令等信息

  • Load Commands 加載命令:描述了文件中數據的具體組織結構,不同的數據類型使用不同的加載命令表示

  • Data 數據:數據中的每個段(segment)的數據都保存在這里,段的概念與ELF文件中段的概念類似。每個段都有一個或多個部分,它們放置了具體的數據與代碼,主要包含代碼,數據,例如符號表,動態符號表等等

Header

Mach-O的Header包含了整個Mach-O文件的關鍵信息,使得CPU能快速知道Mac-O的基本信息,其在Mach.h(路徑同前文的fat.h一致)針對32位和64位架構的cpu,分別使用了mach_headermach_header_64結構體來描述Mach-O頭部mach_header是連接器加載時最先讀取的內容,決定了一些基礎架構、系統類型、指令條數等信息,這里查看64位架構的mach_header_64結構體定義,相比于32位架構的mach_header,只是多了一個reserved保留字段,

/*
 - magic:0xfeedface(32位) 0xfeedfacf(64位),系統內核用來判斷是否是mach-o格式
 - cputype:CPU類型,比如ARM
 - cpusubtype:CPU的具體類型,例如arm64、armv7
 - filetype:由于可執行文件、目標文件、靜態庫和動態庫等都是mach-o格式,所以需要filetype來說明mach-o文件是屬于哪種文件
 - ncmds:sizeofcmds:LoadCommands加載命令的條數(加載命令緊跟header之后)
 - sizeofcmds:LoadCommands加載命令的大小
 - flags:標志位標識二進制文件支持的功能,主要是和系統加載、鏈接有關
 - reserved:保留字段
 */
struct mach_header_64 {
    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 */
    uint32_t    reserved;   /* reserved */
};

其中filetype主要記錄Mach-O的文件類型,常用的有以下幾種

#define MH_OBJECT   0x1     /* 目標文件*/
#define MH_EXECUTE  0x2     /* 可執行文件*/
#define MH_DYLIB    0x6     /* 動態庫*/
#define MH_DYLINKER 0x7     /* 動態鏈接器*/
#define MH_DSYM     0xa     /* 存儲二進制文件符號信息,用于debug分析*/

相對應的,Header在MachOView中的展示如下

Header展示

Load Commands

在Mach-O文件中,Load Commands主要是用于加載指令,其大小和數目在Header中已經被提供,其在Mach.h中的定義如下

/*
 load_command用于加載指令
 - cmd 加載命令的類型
 - cmdsize 加載命令的大小
 */
struct load_command {
    uint32_t cmd;       /* type of load command */
    uint32_t cmdsize;   /* total size of command in bytes */
};

我們在MachOView中查看Load Commands,其中記錄了很多信息,例如動態鏈接器的位置、程序的入口、依賴庫的信息、代碼的位置、符號表的位置等等,如下所示

Load Commands展示

其中LC_SEGMENT_64的類型segment_command_64定義如下

/*
 segment_command 段加載命令
 - cmd:表示加載命令類型,
 - cmdsize:表示加載命令大小(還包括了緊跟其后的nsects個section的大小)
 - segname:16個字節的段名字
 - vmaddr:段的虛擬內存起始地址
 - vmsize:段的虛擬內存大小
 - fileoff:段在文件中的偏移量
 - filesize:段在文件中的大小
 - maxprot:段頁面所需要的最高內存保護(4 = r,2 = w,1 = x)
 - initprot:段頁面初始的內存保護
 - nsects:段中section數量
 - flags:其他雜項標志位
 
 - 從fileoff(偏移)處,取filesize字節的二進制數據,放到內存的vmaddr處的vmsize字節。(fileoff處到filesize字節的二進制數據,就是“段”)
 - 每一個段的權限相同(或者說,編譯時候,編譯器把相同權限的數據放在一起,成為段),其權限根據initprot初始化。initprot指定了如何通過讀/寫/執行位初始化頁面的保護級別
 - 段的保護設置可以動態改變,但是不能超過maxprot中指定的值(在iOS中,+x和+w是互斥的)
 */
struct segment_command_64 { /* for 64-bit architectures */
    uint32_t    cmd;        /* LC_SEGMENT_64 */
    uint32_t    cmdsize;    /* includes sizeof section_64 structs */
    char        segname[16];    /* segment name */
    uint64_t    vmaddr;     /* memory address of this segment */
    uint64_t    vmsize;     /* memory size of this segment */
    uint64_t    fileoff;    /* file offset of this segment */
    uint64_t    filesize;   /* amount to map from the file */
    vm_prot_t   maxprot;    /* maximum VM protection */
    vm_prot_t   initprot;   /* initial VM protection */
    uint32_t    nsects;     /* number of sections in segment */
    uint32_t    flags;      /* flags */
};

Data

Load Commands后就是Data區域,這個區域存儲了具體的只讀、可讀寫代碼,例如方法、符號表、字符表、代碼數據、連接器所需的數據(重定向、符號綁定等)。主要是存儲具體的數據。其中大多數的Mach-O文件均包含以下三個段:

  • __TEXT 代碼段:只讀,包括函數,和只讀的字符串
  • __DATA 數據段:讀寫,包括可讀寫的全局變量等
  • __LINKEDIT: __LINKEDIT包含了方法和變量的元數據(位置,偏移量),以及代碼簽名等信息。

Data區中,Section占了很大的比例,SectionMach.h中是以結構體section_64(在arm64架構下)表示,其定義如下

/*
 Section節在MachO中集中體現在TEXT和DATA兩段里.
 - sectname:當前section的名稱
 - segname:section所在的segment名稱
 - addr:內存中起始位置
 - size:section大小
 - offset:section的文件偏移
 - align:字節大小對齊
 - reloff:重定位入口的文件偏移
 - nreloc:重定位入口數量
 - flags:標志,section的類型和屬性
 - reserved1:保留(用于偏移量或索引)
 - reserved2:保留(用于count或sizeof)
 - reserved3:保留
 */

struct section_64 { /* for 64-bit architectures */
    char        sectname[16];   /* name of this section */
    char        segname[16];    /* segment this section goes in */
    uint64_t    addr;       /* memory address of this section */
    uint64_t    size;       /* size in bytes of this section */
    uint32_t    offset;     /* file offset of this section */
    uint32_t    align;      /* section alignment (power of 2) */
    uint32_t    reloff;     /* file offset of relocation entries */
    uint32_t    nreloc;     /* number of relocation entries */
    uint32_t    flags;      /* flags (section type and attributes)*/
    uint32_t    reserved1;  /* reserved (for offset or index) */
    uint32_t    reserved2;  /* reserved (for count or sizeof) */
    uint32_t    reserved3;  /* reserved */
};

SectionMachOView中可以看出,主要集中體現在TEXTDATA兩段里,如下所示

Section展示

其中常見的section,主要有以下一些

section - __TEXT 說明
__TEXT.__text 主程序代碼
__TEXT.__cstring C語言字符串
__TEXT.__const const 關鍵字修飾的常量
__TEXT.__stubs 用于 Stub 的占位代碼,很多地方稱之為樁代碼
__TEXT.__stubs_helper 當 Stub 無法找到真正的符號地址后的最終指向
__TEXT.__objc_methname Objective-C 方法名稱
__TEXT.__objc_methtype Objective-C 方法類型
__TEXT.__objc_classname Objective-C 類名稱
section - __DATA 說明
__DATA.__data 初始化過的可變數據
__DATA.__la_symbol_ptr lazy binding 的指針表,表中的指針一開始都指向 __stub_helper
__DATA.nl_symbol_ptr 非 lazy binding 的指針表,每個表項中的指針都指向一個在裝載過程中,被動態鏈機器搜索完成的符號
__DATA.__const 沒有初始化過的常量
__DATA.__cfstring 程序中使用的 Core Foundation 字符串(CFStringRefs)
__DATA.__bss BSS,存放為初始化的全局變量,即常說的靜態內存分配
__DATA.__common 沒有初始化過的符號聲明
__DATA.__objc_classlist Objective-C 類列表
__DATA.__objc_protolist Objective-C 原型
__DATA.__objc_imginfo Objective-C 鏡像信息
__DATA.__objc_selfrefs Objective-C self 引用
__DATA.__objc_protorefs Objective-C 原型引用
__DATA.__objc_superrefs Objective-C 超類引用

所以,綜上所述,Mach-O的格式圖示,如下所示


Mach-O的格式圖示
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容