iOS底層學習 - 從編譯到啓動的奇幻旅程(二)

上一章咱們通過編譯的旅程,咱們的App已經成功編譯完成,生成了對應的Mach-O可執行文件,那麼咱們以後要進行啓動的相關操做了,啓動的時候,咱們是如何加載的動態庫,若是執行相似objc_init這些代碼的呢git

編譯過程傳送門☞iOS底層學習 - 從編譯到啓動的奇幻旅程(一)程序員

在運行的時候,咱們通常都已main函數爲起點,來進行代碼編寫,可是咱們發現main函數以前咱們也進行了許多的操做,好比dyld的一系列操做,本章就來詳細探究github

裝載與動態連接

首先安利一本書《程序員的自我修養--連接、裝載與庫》,看完神清氣爽。bootstrap

一個App從可執行文件到真正啓動運行代碼,基本須要通過裝載和動態庫連接兩個步驟緩存

裝載

可執行文件(程序)是一個靜態的概念,在運行以前它只是硬盤上的一個文件;而進程是一個動態的概念,它是程序運行時的一個過程,咱們知道每一個程序被運行起來後,它會擁有本身獨立的虛擬地址空間,這個地址空間大小的上限是由計算機的硬件(CPU的位數)決定的。bash

進程的虛擬空間都在操做系統的掌握之中,且在操做系統中會同時運行着多個進程,它們彼此之間的虛擬地址空間是隔離的,若是進程訪問了操做系統分配給該進程之外的地址空間,會被系統當作非法操做而強制結束進程。網絡

裝載就是將硬盤上的可執行文件映射到虛擬內存中的過程,但內存是昂貴且稀有的,因此將程序執行時所需的指令和數據所有裝載到內存中顯然是行不通的,因而人們研究發現了程序運行時是有局部性原理的,能夠只將最經常使用的部分駐留在內存中,而不太經常使用的數據存放在磁盤裏,這也是動態裝載的基本原理架構

裝載的過程也能夠理解爲進程創建的過程,操做系統只須要作如下三件事情:app

  • 建立一個獨立的虛擬地址空間
  • 讀取可執行文件頭,而且創建虛擬空間與可執行文件的映射關係
  • 將CPU的指令寄存器設置成可執行文件的入口地址,啓動運行

動態庫連接

概念

連接的共用庫分爲靜態庫和動態庫:靜態庫是編譯時連接的庫,須要連接進你的 Mach-O 文件裏,若是須要更新就要從新編譯一次,沒法動態加載和更新;而動態庫是運行時連接的庫,使用 dyld 就能夠實現動態加載。框架

在真實的 iOS 開發中,你會發現不少功能都是現成可用的,不光你可以用,其餘 App 也在用,好比 GUI 框架、I/O、網絡等。連接這些共享庫到你的Mach-O文件,也是經過連接器來完成的。

iOS 中用到的全部系統framework(UIKit,Foundation等)都是動態連接的,類比成插頭和插排,靜態連接的代碼在編譯後的靜態連接過程就將插頭和插排一個個插好,運行時直接執行二進制文件;而動態連接須要在程序啓動時去完成「插插銷」的過程,因此在咱們寫的代碼執行前,動態鏈接器須要完成準備工做。

共享緩存

爲了節約空間 , 蘋果將這些系統庫放在了一個地方 : 動態庫共享緩存區 (dyld shared cache)

Mach-O 文件是編譯後的產物,而動態庫在運行時纔會被連接,並沒參與 Mach-O 文件的編譯和連接,所以Mach-O文件中並無包含動態庫裏的符號定義

也就是說,這些符號會顯示爲未定義,但它們的名字和對應的庫的路徑會被記錄下來。運行時經過 dlopendlsym 導入動態庫時,先根據記錄的庫路徑找到對應的庫,再經過記錄的名字符號找到綁定的地址。

dlopen會把共享庫載入運行進程的地址空間,載入的共享庫也會有未定義的符號,這樣會觸發更多的共享庫被載入。dlopen也能夠選擇是馬上解析全部引用仍是滯後去作。dlopen 打開動態庫後返回的是引用的指針,dlsym的做用就是經過 dlopen 返回的動態庫指針和函數符號,獲得函數的地址而後使用。

優勢

系統使用動態庫連接的好處以下:

  • 代碼共用:不少程序都動態連接了這些 lib,但它們在內存和磁盤中中只有一份
  • 易於維護:因爲被依賴的 lib 是程序執行時才 link 的,因此這些 lib 很容易作更新,好比libSystem.dylib 是 libSystem.B.dylib 的替身,哪天想升級直接換成 libSystem.C.dylib 而後再替換替身就好了
  • 減小可執行文件體積:相比靜態連接,動態連接在編譯時不須要打進去,因此可執行文件的體積要小不少

從dyld看程序啓動

簡介

dyld(the dynamic link editor)是蘋果的動態連接器,是蘋果操做系統的一個重要組成部分,在應用被編譯打包成可執行文件格式的 Mach-O 文件以後,交由 dyld 負責連接,加載程序 。

dyld的相關代碼是開源的☞源碼地址

啓動流程

建立一個空工程,咱們知道load函數是優於main函數來調用的,因此將斷點打在load方法裏,看一下函數的調用堆棧。

咱們能夠看到load方法錢,幾乎全是dyld動態連接器的調用,從 _dyld_start開始

dyldbootstrap::start

dyldbootstrap::start 就是指 dyldbootstrap 這個命名空間做用域裏的 start 函數 。來到源碼中,搜索 dyldbootstrap,而後找到 start 函數。

//
//  This is code to bootstrap dyld.  This work in normally done for a program by dyld and crt.
//  In dyld we have to do this manually.
//
uintptr_t start(const struct macho_header* appsMachHeader, int argc, const char* argv[], 
				intptr_t slide, const struct macho_header* dyldsMachHeader,
				uintptr_t* startGlue)
{
	// if kernel had to slide dyld, we need to fix up load sensitive locations
	// we have to do this before using any global variables
    slide = slideOfMainExecutable(dyldsMachHeader);
    bool shouldRebase = slide != 0;
#if __has_feature(ptrauth_calls)
    shouldRebase = true;
#endif
    if ( shouldRebase ) {
        rebaseDyld(dyldsMachHeader, slide);
    }

	// allow dyld to use mach messaging
	mach_init();

	// kernel sets up env pointer to be just past end of agv array
	const char** envp = &argv[argc+1];
	
	// kernel sets up apple pointer to be just past end of envp array
	const char** apple = envp;
	while(*apple != NULL) { ++apple; }
	++apple;

	// set up random value for stack canary
	__guard_setup(apple);

#if DYLD_INITIALIZER_SUPPORT
	// run all C++ initializers inside dyld
	runDyldInitializers(dyldsMachHeader, slide, argc, argv, envp, apple);
#endif

	// now that we are done bootstrapping dyld, call dylds main
	uintptr_t appsSlide = slideOfMainExecutable(appsMachHeader);
	return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}

複製代碼

start函數主要的調用流程爲:

1.首先進行bootstrap自舉操做,由於dyld自己也是一個動態庫,可是因爲它須要連接其餘動態庫,因此它不依賴其餘庫,且自己所須要的全局和靜態變量的重定位工做由它自己完成,這樣就防止了「蛋生雞,雞生蛋」的問題

  • const struct macho_header這個指Mach-O文件裏的header
  • intptr_t slide這個其實就是 ALSR , 說白了就是經過一個隨機值 ( 也就是咱們這裏的 slide ) 來實現地址空間配置隨機加載 ,防止被攻擊
  • rebaseDyld是dyld的重定向

2.開放函數消息使用:mach_init()

3.設置堆棧保護:__guard_setup

4.開始連接共享對象:dyld::_main

dyld::_main

uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, 
		int argc, const char* argv[], const char* envp[], const char* apple[], 
		uintptr_t* startGlue)
{
    ...這是dyld連接的主要函數,代碼太長,逐步分析...
}
複製代碼

1.配置環境變量等

1.1 從環境變量中主要可執行文件的cdHash。其中環境變量是系統定義的,能夠再Xcode中進行配置

1.2 設置上下文信息 setContext

1.3 檢測線程是否受限,並作相關處理 configureProcessRestrictions

1.4 檢查環境變量 checkEnvironmentVariables

1.5 獲取程序架構 getHostInfo

2.加載共享緩存

2.1 驗證共享緩存路徑:checkSharedRegionDisable

2.2 加載共享緩存: mapSharedCache

3. 添加dyld到UUID列表

將dyld自己添加到UUID列表addDyldImageToUUIDList

4.reloadAllImages

4.1 實例化主程序instantiateFromLoadedImage

sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
複製代碼

內核會映射到主可執行文件中。咱們須要已經映射到主可執行文件中的文件建立一個ImageLoader

// The kernel maps in main executable before dyld gets control.  We need to 
// make an ImageLoader* for the already mapped in main executable.
static ImageLoaderMachO* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path)
{
	// try mach-o loader
	if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
		ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
		addImage(image);
		return (ImageLoaderMachO*)image;
	}
	
	throw "main executable not a known format";
}
複製代碼

經過instantiateMainExecutable中的sniffLoadCommands加載主程序其實就是對MachO文件中LoadCommons段的一些列加載

  • 最大的segment數量爲256個!
  • 最大的動態庫(包括系統的個自定義的)個數爲4096個!
void ImageLoaderMachO::sniffLoadCommands(const macho_header* mh, const char* path, bool inCache, bool* compressed,
											unsigned int* segCount, unsigned int* libCount, const LinkContext& context,
											const linkedit_data_command** codeSigCmd,
											const encryption_info_command** encryptCmd)
{
    ...
    for (uint32_t i = 0; i < cmd_count; ++i) {
    ...
}
複製代碼

生成鏡像文件後,添加到sAllImages全局鏡像中,主程序永遠是sAllImages的第一個對象

static void addImage(ImageLoader* image)
{
	// add to master list
    allImagesLock();
        sAllImages.push_back(image);
    allImagesUnlock();
    ...
}
複製代碼

4.2 加載插入動態庫loadInsertedDylib

// load any inserted libraries
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
    for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) 
	loadInsertedDylib(*lib);
}
複製代碼

4.3 連接主程序link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);

連接主程序中各動態庫,進行符號綁定

// link main executable
		gLinkContext.linkingMainExecutable = true;
#if SUPPORT_ACCELERATE_TABLES
		if ( mainExcutableAlreadyRebased ) {
			// previous link() on main executable has already adjusted its internal pointers for ASLR
			// work around that by rebasing by inverse amount
			sMainExecutable->rebase(gLinkContext, -mainExecutableSlide);
		}
#endif
		link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
		sMainExecutable->setNeverUnloadRecursive();
		if ( sMainExecutable->forceFlat() ) {
			gLinkContext.bindFlat = true;
			gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
		}
複製代碼

至此 , 配置環境變量 -> 加載共享緩存 -> 實例化主程序 -> 加載動態庫 -> 連接動態庫 就已經完成了 .

5.運行全部初始化程序

函數調用爲initializeMainExecutable();。爲主要可執行文件及其帶來的一切運行初始化程序

5.1 runInitializers->processInitializers初始化準備

void ImageLoader::runInitializers(const LinkContext& context, InitializerTimingList& timingInfo)
{
	uint64_t t1 = mach_absolute_time();
	mach_port_t thisThread = mach_thread_self();
	ImageLoader::UninitedUpwards up;
	up.count = 1;
	up.images[0] = this;
	processInitializers(context, thisThread, timingInfo, up);
	context.notifyBatch(dyld_image_state_initialized, false);
	mach_port_deallocate(mach_task_self(), thisThread);
	uint64_t t2 = mach_absolute_time();
	fgTotalInitTime += (t2 - t1);
}
複製代碼

5.2 遍歷image.count,遞歸開始初始化鏡像,

void ImageLoader::processInitializers(const LinkContext& context, mach_port_t thisThread,
									 InitializerTimingList& timingInfo, ImageLoader::UninitedUpwards& images)
{
	uint32_t maxImageCount = context.imageCount()+2;
	ImageLoader::UninitedUpwards upsBuffer[maxImageCount];
	ImageLoader::UninitedUpwards& ups = upsBuffer[0];
	ups.count = 0;
	// Calling recursive init on all images in images list, building a new list of
	// uninitialized upward dependencies.
	for (uintptr_t i=0; i < images.count; ++i) {
		images.images[i]->recursiveInitialization(context, thisThread, images.images[i]->getPath(), timingInfo, ups);
	}
	// If any upward dependencies remain, init them.
	if ( ups.count > 0 )
		processInitializers(context, thisThread, timingInfo, ups);
}
複製代碼

5.3 recursiveInitialization獲取到鏡像的初始化

void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize,
										  InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
    ...
    uint64_t t1 = mach_absolute_time();
	fState = dyld_image_state_dependents_initialized;
	oldState = fState;
	context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
	// initialize this image
	bool hasInitializers = this->doInitialization(context);

	// let anyone know we finished initializing this image
	fState = dyld_image_state_initialized;
	oldState = fState;
	context.notifySingle(dyld_image_state_initialized, this, NULL);
    ...
}
複製代碼

5.3.1 notifySingle獲取到鏡像的回調

static void notifySingle(dyld_image_states state, const ImageLoader* image, ImageLoader::InitializerTimingList* timingInfo)
{ ... }
複製代碼

重頭戲來了 . 根據函數調用棧咱們發現 , 下一步是調用load_images , 但是這個 notifySingle 裏並無找到 load_images,其實這是一個回調函數的調用

5.3.2 sNotifyObjCInit的賦值在registerObjCNotifiers函數中

void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
{
	// record functions to call
	sNotifyObjCMapped	= mapped;
	sNotifyObjCInit		= init;
	sNotifyObjCUnmapped = unmapped;

	// call 'mapped' function with all images mapped so far
	try {
		notifyBatchPartial(dyld_image_state_bound, true, NULL, false, true);
	}
	catch (const char* msg) {
		// ignore request to abort during registration
	}

	// <rdar://problem/32209809> call 'init' function on all images already init'ed (below libSystem) for (std::vector<ImageLoader*>::iterator it=sAllImages.begin(); it != sAllImages.end(); it++) { ImageLoader* image = *it; if ( (image->getState() == dyld_image_state_initialized) && image->notifyObjC() ) { dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0); (*sNotifyObjCInit)(image->getRealPath(), image->machHeader()); } } } 複製代碼

5.3.3 registerObjCNotifiers的調用在_dyld_objc_notify_register函數中

這個函數是用來給外部共享動態庫調用的,好比runtime中須要加載的objc

void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,
                                _dyld_objc_notify_init      init,
                                _dyld_objc_notify_unmapped  unmapped)
{
	dyld::registerObjCNotifiers(mapped, init, unmapped);
}

複製代碼

咱們能夠看到源碼中在_objc_init調用了_dyld_objc_notify_register

3個參數的含義以下:

  • map_images : dyld 將 image 加載進內存時 , 會觸發該函數.
  • load_images : dyld 初始化 image 會觸發該方法. ( 咱們所熟知的 load 方法也是在此處調用 ) .
  • unmap_image : dyld 將 image 移除時 , 會觸發該函數 .
void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    environ_init();
    tls_init();
    static_init();
    lock_init();
    exception_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
複製代碼

5.4 doInitialization這是一個系統特定的C++構造函數的調用方法。

bool ImageLoaderMachO::doInitialization(const LinkContext& context)
{
	CRSetCrashLogMessage2(this->getPath());

	// mach-o has -init and static initializers
	doImageInit(context);
	doModInitFunctions(context);
	
	CRSetCrashLogMessage2(NULL);
	
	return (fHasDashInit || fHasInitializers);
}
複製代碼

這種C++構造函數有特定的寫法,在MachO文件中找到對應的方法,以下

__attribute__((constructor)) void CPFunc(){
    printf("C++Func1");
}

複製代碼

6.notifyMonitoringDyldMain監聽dyld的main

7.找到main函數的調用

找到真正 main 函數入口 並返回.

// find entry point for main executable
result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();
複製代碼

小結

至此,整個啓動流程結束了

大致runtime的加載流程以下

  • dyld 開始將程序二進制文件初始化
  • 交由 ImageLoader 讀取 image,其中包含了咱們的類、方法等各類符號
  • 因爲 runtime 向 dyld 綁定了回調,當 image 加載到內存後,dyld 會通知 runtime 進行處理
  • runtime 接手後調用 map_images 作解析和處理,接下來 load_images 中調用call_load_methods方法,遍歷全部加載進來的 Class,按繼承層級依次調用 Class 的 +load 方法和其 Category 的 +load 方法

總結

流程簡圖

dyld調用順序

1.從 kernel 留下的原始調用棧引導和啓動本身

2.將程序依賴的動態連接庫遞歸加載進內存,固然這裏有緩存機制

3.non-lazy 符號當即 link 到可執行文件,lazy 的存表裏

4.Runs static initializers for the executable

5.找到可執行文件的 main 函數,準備參數並調用

6.程序執行中負責綁定 lazy 符號、提供 runtime dynamic loading services、提供調試器接口

7.程序main函數 return 後執行 static terminator

8.某些場景下 main 函數結束後調 libSystem 的 _exit 函數

層級順序圖

參考

相關文章
相關標籤/搜索