上一章咱們通過編譯的旅程,咱們的App已經成功編譯完成,生成了對應的Mach-O可執行文件,那麼咱們以後要進行啓動的相關操做了,啓動的時候,咱們是如何加載的動態庫,若是執行相似objc_init這些代碼的呢git
編譯過程傳送門☞iOS底層學習 - 從編譯到啓動的奇幻旅程(一)程序員
在運行的時候,咱們通常都已main
函數爲起點,來進行代碼編寫,可是咱們發現main
函數以前咱們也進行了許多的操做,好比dyld
的一系列操做,本章就來詳細探究github
首先安利一本書《程序員的自我修養--連接、裝載與庫》,看完神清氣爽。bootstrap
一個App從可執行文件到真正啓動運行代碼,基本須要通過裝載和動態庫連接兩個步驟緩存
可執行文件(程序)是一個靜態的概念,在運行以前它只是硬盤上的一個文件;而進程是一個動態的概念,它是程序運行時的一個過程,咱們知道每一個程序被運行起來後,它會擁有本身獨立的虛擬地址空間,這個地址空間大小的上限是由計算機的硬件(CPU的位數)決定的。bash
進程的虛擬空間都在操做系統的掌握之中,且在操做系統中會同時運行着多個進程,它們彼此之間的虛擬地址空間是隔離的,若是進程訪問了操做系統分配給該進程之外的地址空間,會被系統當作非法操做而強制結束進程。網絡
裝載就是將硬盤上的可執行文件映射到虛擬內存中的過程,但內存是昂貴且稀有的,因此將程序執行時所需的指令和數據所有裝載到內存中顯然是行不通的,因而人們研究發現了程序運行時是有局部性原理的,能夠只將最經常使用的部分駐留在內存中,而不太經常使用的數據存放在磁盤裏,這也是動態裝載的基本原理架構
裝載的過程也能夠理解爲進程創建的過程,操做系統只須要作如下三件事情:app
連接的共用庫分爲靜態庫和動態庫:靜態庫是編譯時連接的庫,須要連接進你的 Mach-O 文件裏,若是須要更新就要從新編譯一次,沒法動態加載和更新;而動態庫是運行時連接的庫,使用 dyld 就能夠實現動態加載。框架
在真實的 iOS 開發中,你會發現不少功能都是現成可用的,不光你可以用,其餘 App 也在用,好比 GUI 框架、I/O、網絡等。連接這些共享庫到你的Mach-O文件,也是經過連接器來完成的。
iOS 中用到的全部系統framework
(UIKit,Foundation等)都是動態連接的,類比成插頭和插排,靜態連接的代碼在編譯後的靜態連接過程就將插頭和插排一個個插好,運行時直接執行二進制文件;而動態連接須要在程序啓動時去完成「插插銷」的過程,因此在咱們寫的代碼執行前,動態鏈接器須要完成準備工做。
爲了節約空間 , 蘋果將這些系統庫放在了一個地方 : 動態庫共享緩存區 (dyld shared cache)
Mach-O 文件是編譯後的產物,而動態庫在運行時纔會被連接,並沒參與 Mach-O 文件的編譯和連接,所以Mach-O文件中並無包含動態庫裏的符號定義。
也就是說,這些符號會顯示爲未定義,但它們的名字和對應的庫的路徑會被記錄下來。運行時經過 dlopen
和 dlsym
導入動態庫時,先根據記錄的庫路徑找到對應的庫,再經過記錄的名字符號找到綁定的地址。
dlopen
會把共享庫載入運行進程的地址空間,載入的共享庫也會有未定義的符號,這樣會觸發更多的共享庫被載入。dlopen
也能夠選擇是馬上解析全部引用仍是滯後去作。dlopen
打開動態庫後返回的是引用的指針,dlsym
的做用就是經過 dlopen
返回的動態庫指針和函數符號,獲得函數的地址而後使用。
系統使用動態庫連接的好處以下:
dyld(the dynamic link editor)是蘋果的動態連接器,是蘋果操做系統的一個重要組成部分,在應用被編譯打包成可執行文件格式的 Mach-O 文件以後,交由 dyld 負責連接,加載程序 。
dyld
的相關代碼是開源的☞源碼地址
建立一個空工程,咱們知道load函數
是優於main函數
來調用的,因此將斷點打在load方法裏,看一下函數的調用堆棧。
_dyld_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
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 從環境變量中主要可執行文件的cdHash
。其中環境變量是系統定義的,能夠再Xcode中進行配置
setContext
configureProcessRestrictions
checkEnvironmentVariables
getHostInfo
2.1 驗證共享緩存路徑:checkSharedRegionDisable
mapSharedCache
將dyld自己添加到UUID列表addDyldImageToUUIDList
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段的一些列加載
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;
}
複製代碼
至此 , 配置環境變量 -> 加載共享緩存 -> 實例化主程序 -> 加載動態庫 -> 連接動態庫 就已經完成了 .
函數調用爲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");
}
複製代碼
notifyMonitoringDyldMain
監聽dyld的main找到真正 main
函數入口 並返回.
// find entry point for main executable
result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();
複製代碼
至此,整個啓動流程結束了
大致runtime的加載流程以下
map_images
作解析和處理,接下來 load_images
中調用call_load_methods
方法,遍歷全部加載進來的 Class,按繼承層級依次調用 Class 的 +load 方法和其 Category 的 +load 方法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 函數