想要了解應(yīng)用程序加載,我們需要了解下面幾個問題:
- 我們寫的代碼是如何加載到內(nèi)存的牙言?
- 我們使用的動靜態(tài)庫是如何加載到內(nèi)存的酸钦?
- objc是如何啟動的?
我們程序執(zhí)行都會依賴很多庫咱枉,比如UIKit
卑硫、CoreFoundation
黔寇、libsystem
等堕担,這些庫其實就是可執(zhí)行的二進(jìn)制文件,能夠被操作系統(tǒng)加載到內(nèi)存嚣州。庫分為兩種:動態(tài)庫和靜態(tài)庫亿乳。
整個編譯過程如下圖:
首先我們的代碼會經(jīng)歷預(yù)編譯階段硝拧,進(jìn)行詞法和語法樹的分析,然后交給編譯器編譯葛假,會生成相應(yīng)的匯編文件障陶,把這些內(nèi)容鏈接裝載到內(nèi)存,生成我們的可執(zhí)行文件聊训。
動態(tài)庫和靜態(tài)庫區(qū)別一個是動態(tài)鏈接抱究,一個是靜態(tài)鏈接。
靜態(tài)鏈接:一個一個裝載進(jìn)內(nèi)存带斑,會有重復(fù)的問題鼓寺,浪費相應(yīng)性能酿雪。
動態(tài)鏈接:并不是直接加載,通過映射加載到內(nèi)存侄刽,內(nèi)存是共享的只會有一份指黎,大部分蘋果的庫都為動態(tài)庫。
靜態(tài)庫和動態(tài)庫是如何加載到內(nèi)存的呢州丹?需要連接器dyld
醋安,dyld
作用如下圖:
下面我們開始研究dyld
,先在main
函數(shù)位置打一個斷點墓毒,執(zhí)行程序:
看到函數(shù)調(diào)用棧在main
函數(shù)之前會執(zhí)行start
方法吓揪,點擊可以看到start
就是執(zhí)行dyld
中的start
方法:
我們再添加一個名字為start
的符號斷點,再執(zhí)行程序所计,發(fā)現(xiàn)斷點并沒有斷到start
位置
為什么沒有停在start
柠辞?證明在下層真正調(diào)用不是start
而是其他方法。我們知道load
方法是在程序執(zhí)行之前就跑主胧,我們添加個load
方法并打一個斷點叭首,
看函數(shù)調(diào)用棧。點擊最下面的_dyld_start
dyld
源碼可以在官網(wǎng)下載踪栋。
打開源碼工程焙格,首先全局搜索_dyld_start
,發(fā)現(xiàn)是匯編實現(xiàn)的夷都,根據(jù)架構(gòu)不同眷唉,代碼不同,我們直接看真機arm64
在這里我們分析主要代碼邏輯即可囤官,配合注釋找到start
方法調(diào)用:
全局搜索dyldbootstrap
命名空間冬阳,找到start
方法
uintptr_t start(const dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[],
const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue)
{
// Emit kdebug tracepoint to indicate dyld bootstrap has started <rdar://46878536>
dyld3::kdebug_trace_dyld_marker(DBG_DYLD_TIMING_BOOTSTRAP_START, 0, 0, 0, 0);
// if kernel had to slide dyld, we need to fix up load sensitive locations
// we have to do this before using any global variables
rebaseDyld(dyldsMachHeader);
// 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(argc, argv, envp, apple);
#endif
_subsystem_init(apple);
// now that we are done bootstrapping dyld, call dyld's main
uintptr_t appsSlide = appsMachHeader->getSlide();
return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}
這里面最重要的就是最后一行,main
函數(shù)党饮,點擊進(jìn)去肝陪,發(fā)現(xiàn)main
函數(shù)代碼非常多,有將近1000行代碼劫谅,看這種代碼需要點技巧见坑,我們知道這個函數(shù)是有返回值的,那就先看下返回值是什么捏检,有什么對應(yīng)操作荞驴。
返回值是result
,賦值的地方有:
result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();
result = (uintptr_t)sMainExecutable->getEntryFromLC_UNIXTHREAD();
result = (uintptr_t)&fake_main;
上面三個地方有對result
賦值贯城,可以看到最主要就是sMainExecutable
相關(guān)處理熊楼,找一下sMainExecutable
初始化:
/// instantiate ImageLoader for main executable
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
看到注釋寫的也很明白,初始化鏡像文件。點擊進(jìn)去:
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";
}
點擊進(jìn)入instantiateMainExecutable
鲫骗,看到sniffLoadCommands(mh, path, false, &compressed, &segCount, &libCount, context, &codeSigCmd, &encryptCmd);
這個代碼就是按照machO
格式進(jìn)行加載文件犬耻。
共享緩存相關(guān)處理:
// load shared cache
checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
加載插入的動態(tài)庫:
// load any inserted libraries
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}
鏈接主程序:
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
鏈接插入的動態(tài)庫:
link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
弱引用綁定主程序:
// <rdar://problem/12186933> do weak binding only after all inserted images linked
sMainExecutable->weakBind(gLinkContext);
初始化主程序:
// run all initializers
initializeMainExecutable();
回調(diào)函數(shù):
// notify any montoring proccesses that this process is about to enter main()
notifyMonitoringDyldMain();
initializeMainExecutable
void initializeMainExecutable()
{
// record that we've reached this step
gLinkContext.startedInitializingMainExecutable = true;
// run initialzers for any inserted dylibs
ImageLoader::InitializerTimingList initializerTimes[allImagesCount()];
initializerTimes[0].count = 0;
const size_t rootCount = sImageRoots.size();
if ( rootCount > 1 ) {
for(size_t i=1; i < rootCount; ++i) {
sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
}
}
// run initializers for main executable and everything it brings up
sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);
// register cxa_atexit() handler to run static terminators in all loaded images when this process exits
if ( gLibSystemHelpers != NULL )
(*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL);
// dump info if requested
if ( sEnv.DYLD_PRINT_STATISTICS )
ImageLoader::printStatistics((unsigned int)allImagesCount(), initializerTimes[0]);
if ( sEnv.DYLD_PRINT_STATISTICS_DETAILS )
ImageLoaderMachO::printStatisticsDetails((unsigned int)allImagesCount(), initializerTimes[0]);
}
首先拿到所有鏡像文件個數(shù),循環(huán)跑起來执泰。點擊進(jìn)入runInitializers
看下如何初始化
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.imagesAndPaths[0] = { this, this->getPath() };
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);
}
這里最重要的是processInitializers
函數(shù)枕磁,點進(jìn)去
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.imagesAndPaths[i].first->recursiveInitialization(context, thisThread, images.imagesAndPaths[i].second, timingInfo, ups);
}
// If any upward dependencies remain, init them.
if ( ups.count > 0 )
processInitializers(context, thisThread, timingInfo, ups);
}
還是循環(huán)調(diào)用recursiveInitialization
,找到這個方法的實現(xiàn)void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize, InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
术吝,
里面最重要的代碼如圖:
先對依賴文件進(jìn)行初始化计济,然后再初始化自己。notifySingle
全局搜索找到這個方法實現(xiàn)排苍,觀察里面最重要代碼:(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
沦寂。
全局搜索sNotifyObjCInit
,發(fā)現(xiàn)在registerObjCNotifiers
方法中有賦值淘衙,再搜索看哪調(diào)用這個方法:_dyld_objc_notify_register
传藏,這個方法和objc
源碼中objc_init
里面的方法名字是一模一樣的。但其實從dyld
加載到objc
這條線到現(xiàn)在已經(jīng)斷了彤守,需要再找下別的方法毯侦。
打開objc
源碼工程,在源碼中_objc_init
打一個斷點遗增,查看函數(shù)調(diào)用棧:
從下往上看執(zhí)行流程:
- _dyld_start
- dyldbootstrap::start
- dyld::_main
- dyld::initializeMainExecutable()
- runInitializers
- processInitializers
- recursiveInitialization
- doInitialization
- doModInitFunctions
到這是dyld
處理的流程叫惊,在往上看就不知道是什么流程款青,我們現(xiàn)在從上往下看做修,先走libdispatch
的_os_object_init
方法,我們下載libdispatch
源碼抡草,全局搜索_os_object_init
void
_os_object_init(void)
{
_objc_init();
Block_callbacks_RR callbacks = {
sizeof(Block_callbacks_RR),
(void (*)(const void *))&objc_retain,
(void (*)(const void *))&objc_release,
(void (*)(const void *))&_os_objc_destructInstance
};
_Block_use_RR2(&callbacks);
#if DISPATCH_COCOA_COMPAT
const char *v = getenv("OBJC_DEBUG_MISSING_POOLS");
if (v) _os_object_debug_missing_pools = _dispatch_parse_bool(v);
v = getenv("DISPATCH_DEBUG_MISSING_POOLS");
if (v) _os_object_debug_missing_pools = _dispatch_parse_bool(v);
v = getenv("LIBDISPATCH_DEBUG_MISSING_POOLS");
if (v) _os_object_debug_missing_pools = _dispatch_parse_bool(v);
#endif
}
可以看到_os_object_init
確實調(diào)用了_objc_init
饰及,再全局搜索_os_object_init
,發(fā)現(xiàn)是在libdispatch_init
有調(diào)用康震,再全局搜索libdispatch_init
發(fā)現(xiàn)沒有調(diào)用地方燎含,看上面截圖函數(shù)調(diào)用棧,libSystem
調(diào)用過來的腿短,我們下載libSystem
源碼屏箍,打開libSystem
源碼工程全局搜索libdispatch_init
,發(fā)現(xiàn)是libSystem_initializer
方法進(jìn)行調(diào)用橘忱,而libSystem_initializer
這個方法沒有調(diào)用赴魁,看函數(shù)調(diào)用棧回到dyld
源碼進(jìn)行搜索钝诚,沒找到相關(guān)信息颖御,全局搜索doModInitFunctions
看這個方法實現(xiàn),可以看到框住就是調(diào)用libSystem_initializer
相關(guān)的方法:
doModInitFunctions
是由doInitialization
方法調(diào)用凝颇,最后找到位置:
到此潘拱,整個項目從dyld
如何加載到objc_init
的流程已經(jīng)分析完了疹鳄。
我們可以看到_dyld_objc_notify_register(&map_images, load_images, unmap_image);
方法調(diào)用,map_images
和load_image
都是參數(shù)芦岂,那么他們什么時候調(diào)用的呢瘪弓,時機肯定在別的地方,繼續(xù)探索禽最。
之前分析notifySingle
調(diào)用sNotifyObjCInit
杠茬,找到sNotifyObjCInit
賦值的地方,
_dyld_objc_notify_register
調(diào)用的registerObjCNotifiers
弛随,也就是說_dyld_objc_notify_register
會對三個參數(shù)進(jìn)行初始化瓢喉,也就是說notifySingle
會對將要執(zhí)行的方法進(jìn)行初始化賦值,但是具體調(diào)用還不知道舀透。
之前我們看到有_dyld_objc_notify_register
方法:
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);
}
這個方法三個參數(shù)對應(yīng)著objc
源碼中同名方法的map_images
栓票、load_images
、umap_image
愕够,進(jìn)去看賦值:
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());
}
}
}
也就是我們現(xiàn)在需要找sNotifyObjCMapped()
走贪、sNotifyObjCInit()
、sNotifyObjCUnmapped()
惑芭,這三個函數(shù)調(diào)用才會執(zhí)行我們最關(guān)心的map_images
坠狡、load_images
、umap_image
遂跟。
- 全局搜索
sNotifyObjCMapped
找調(diào)用的地方逃沿,發(fā)現(xiàn)_dyld_objc_notify_register
->registerObjCNotifiers
->notifyBatchPartial
->sNotifyObjCMapped
這個流程會調(diào)用,也就是函數(shù)執(zhí)行時候幻锁,就會執(zhí)行第一個參數(shù)對應(yīng)的方法凯亮。 - 全局搜索
sNotifyObjCInit
,有兩個調(diào)用地方哄尔,a(dyld_image_state_initialized
):_dyld_objc_notify_register
->registerObjCNotifiers
->sNotifyObjCInit()
假消,加載自己的庫的時候會直接調(diào)用load_images
。b(dyld_image_state_dependents_initialized
):notifySingle
->sNotifyObjCInit()
岭接,加載自己依賴庫的時候富拗,依賴庫是通過notifySingle
來調(diào)用load_images
。 - 全局搜索
sNotifyObjCUnmapped
鸣戴,發(fā)現(xiàn)removeImage
才會調(diào)用啃沪,移除鏡像文件時候,暫時不深入分析葵擎。
總結(jié)下從dyld
開始怎么加載到objc
以及map_images
和load_images
調(diào)用的流程圖:
load C++ main 執(zhí)行順序
接下來我們再看一個有意思的現(xiàn)象谅阿,在main
函數(shù)中輸出Hello, World!
,并且在main
函數(shù)中添加一個C++方法:
int main(int argc, const char * argv[]) {
@autoreleasepool {
SJLog(@"Hello, World!");
}
return 0;
}
__attribute__ ((constructor)) void sjFunc() {
printf("C++ 來了 %s \n", __func__);
}
添加SJPerson
類,并實現(xiàn)load
方法:
@implementation SJPerson
+ (void)load
{
SJLog(@"%s", __func__);
}
@end
運行程序签餐,可以看到打印結(jié)果如下:
首先執(zhí)行load
方法寓涨,然后執(zhí)行C++方法,最后才會走到main
函數(shù)里面氯檐。
研究方法執(zhí)行順序戒良,首先我們看下load
方法是怎么調(diào)用:
void
load_images(const char *path __unused, const struct mach_header *mh)
{
if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
didInitialAttachCategories = true;
loadAllCategories();
}
// Return without taking locks if there are no +load methods here.
if (!hasLoadMethods((const headerType *)mh)) return;
recursive_mutex_locker_t lock(loadMethodLock);
// Discover load methods
{
mutex_locker_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
}
// Call +load methods (without runtimeLock - re-entrant)
call_load_methods();
}
首先會prepare_load_methods
準(zhǔn)備所有的load
方法,
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;
runtimeLock.assertLocked();
classref_t const *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
schedule_class_load(remapClass(classlist[i]));
}
category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
for (i = 0; i < count; i++) {
category_t *cat = categorylist[i];
Class cls = remapClass(cat->cls);
if (!cls) continue; // category for ignored weak-linked class
if (cls->isSwiftStable()) {
_objc_fatal("Swift class extensions and categories on Swift "
"classes are not allowed to have +load methods");
}
realizeClassWithoutSwift(cls, nil);
ASSERT(cls->ISA()->isRealized());
add_category_to_loadable_list(cat);
}
}
這里實現(xiàn)準(zhǔn)備和調(diào)用類冠摄、分類的load
方法糯崎。
下一步C++方法為什么會調(diào)用呢?在C++方法打個斷點河泳,調(diào)試打印函數(shù)調(diào)用棧:
到dyld
源碼中查看doInitialization
函數(shù)調(diào)用:
這個位置我們很熟悉了吧沃呢,上面已經(jīng)解釋過了,也就是doInitialization
會調(diào)用我們寫的C++方法拆挥。
在搞一個騷操作薄霜,我們在objc-os.mm
文件中,添加一個C++方法纸兔,再執(zhí)行程序:
__attribute__ ((constructor)) void sjFunc() {
printf("objc C++ 來了 %s \n", __func__);
}
看到執(zhí)行的結(jié)果:
也就是說惰瓜,在同一個鏡像文件中,才會先執(zhí)行load
方法汉矿,再執(zhí)行C++方法崎坊。
執(zhí)行完dyld
然后是如何走到我們工程里面的main
函數(shù)呢?
在C++方法打斷點洲拇,進(jìn)入斷點后顯示匯編:
跳出這個方法:
讀取寄存器:
匯編最后執(zhí)行jump rax
奈揍,rax
寄存器存的就是我們工程的main
函數(shù)。