iOS的dyld加载流程

2,042 阅读14分钟

前言

笔者整理了一系列有关OC的底层文章,希望可以帮助到你。

1.iOS的OC对象创建的alloc原理

2.iOS的OC对象的内存对齐

3.iOS的OC的isa的底层原理

4.iOS的OC源码分析之类的结构分析

5.iOS的OC的方法缓存的源码分析

6.iOS的OC的方法的查找原理

7.iOS的OC的方法的决议与消息转发原理

在App的加载过程中会依赖很多底层的库,但是库是什么呢?库就是可执行代码的二进制,可以被操作系统识别写入到内存中的。在底层库中有分别有静态库和动态库。

1.静态库和动态库

在程序的编译过程中是有一个流程的,这个流程如下

  • 预编译:主要是宏替换,导入的头文件替换成头文件里面的代码,将#开头的预编译指令展开,比如#define,#include,#import。
  • 编译:对预编译处理过的文件进行词法分析、语法分析和语义分析,并进行源代码优化,然后生成汇编代码;
  • 汇编:通过汇编器将汇编代码转换为机器可以执行的指令,并生成目标文件.o文件
  • 链接:将目标文件和用到的静态库动态库链接成可执行文件。

静态库:在链接阶段,会将汇编生成的目标与引用的库一起链接打包到可执行文件当中。

动态库:程序编译并不会链接到目标代码中,而是在程序运行时才被载入。

在这个过程中,很明显动态库是比静态库有优势的,这样的话动态库就可以减少打包后的App的大小,可以共享内容节约资源,可以通过更新动态库达到更新App程序的目的。在iOS系统,我们的用到的系统库一般都是动态库,例如:UIKit,libdispatch,libobjc.dyld等。

2.dyld

dyld全名The dynamic link editor 苹果的动态链接器,是苹果操作系统一个重要组成部分 ,在应用被编译打包成可执行文件格式Mach-O文件之后,交由dyld负责链接和加载程序。并且苹果也开源了这部分的源码,如果有需要的可以去苹果官方下载源码,这篇文章对dyld介绍是在dyld-635.2源码的基础上的。为了方便对接下来的内容介绍,创建了一个demo项目,并且在ViewController中加上load类方法,并且在load方法打个断点。这是在真机调试下得到了。

这是在模拟器下调试得到的,还是有点不同的。 接下来的流程介绍我们是通过真机的模式下的,也可以在lldb中用指令bt来查看

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
  * frame #0: 0x0000000100942578 dyldDemo`+[ViewController load](self=ViewController, _cmd="load") at ViewController.m:19:5
    frame #1: 0x00000001b5b31e78 libobjc.A.dylib`load_images + 908
    frame #2: 0x00000001009a20d4 dyld`dyld::notifySingle(dyld_image_states, ImageLoader const*, ImageLoader::InitializerTimingList*) + 448
    frame #3: 0x00000001009b15b8 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 524
    frame #4: 0x00000001009b0334 dyld`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 184
    frame #5: 0x00000001009b03fc dyld`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 92
    frame #6: 0x00000001009a2420 dyld`dyld::initializeMainExecutable() + 216
    frame #7: 0x00000001009a6db4 dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 4616
    frame #8: 0x00000001009a1208 dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) + 396
    frame #9: 0x00000001009a1038 dyld`_dyld_start + 56

(lldb) 

从程序的调用栈可以看出,dyld的入口是_dyld_start

2.1 _dyld_start

可以在lldb中通过up指令或者在程序调用栈中直接点击,可以去到_dyld_start的汇编源码中 从中可以看到调用的是一个c++的方法,是在dyldbootstrap这个作用域中调用start这个函数,通过dyld的源码中可以先搜索dyldbootstrap作用域然后再搜索start就可以找到,通过cmd + shift + j可以定位到是在dyldInitialization.cpp这个文件中。

//
//  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 dyld's main
	uintptr_t appsSlide = slideOfMainExecutable(appsMachHeader);
	return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}

appsMachHeader这个参数就是Mach-Oheaderslide是一个ASLR随机值,当每一个MachO加载到内存的会随机加一个变量保证在不定的内存分布,也是为了缓存溢出的手段。那么start函数的主要的作用是

  • 1.通过主程序内存算出ASLR偏移值。
  • 2.开始通过rebaseDyld开始重定向dyld和通过mach_init()来初始化。
  • 3.通过__guard_setup来做栈溢出的保护。

接着就是执行dyld::_main这个函数。

2.2 dyld::_main函数

dyld::_main这个函数是在dyld.cpp文件中的。

//
// Entry point for dyld.  The kernel loads dyld and jumps to __dyld_start which
// sets up some registers and call this function.
//
// Returns address of main() in target program which __dyld_start jumps to
//
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, 
		int argc, const char* argv[], const char* envp[], const char* apple[], 
		uintptr_t* startGlue){
    //代码太多了就不贴出来了
	}

这个函数主要是用来加载MachO,并且dyld加载的关键函数也是在dyld::_main这个函数里面的。接下来介绍就只分段地截取部分代码来分析。

这部分的内容主要是用来配置环境变量的。

 // Grab the cdHash of the main executable from the environment
uint8_t mainExecutableCDHashBuffer[20];
const uint8_t* mainExecutableCDHash = nullptr;
if ( hexToBytes(_simple_getenv(apple, "executable_cdhash"), 40, mainExecutableCDHashBuffer) )
		mainExecutableCDHash = mainExecutableCDHashBuffer;

// Trace dyld's load
notifyKernelAboutImage((macho_header*)&__dso_handle, _simple_getenv(apple, "dyld_file"));

这部分的内容是sMainExecutableMachHeader用来获取主程序的MachO的头,sMainExecutableSlide用来获取主程序的ASLR

uintptr_t result = 0;
sMainExecutableMachHeader = mainExecutableMH;
sMainExecutableSlide = mainExecutableSlide;

这里是设置上下文的信息,包括一些回调的函数和参数以及一些标志的信息都是在这里设置的。

CRSetCrashLogMessage("dyld: launch started");
setContext(mainExecutableMH, argc, argv, envp, apple);

configureProcessRestrictions配置进程是否出现,判断当前的进程是否出现会在这个函数做出判断。checkEnvironmentVariables是检测环境变量的。这些配置和检测是dyld自身会检测加载的。例如这些环境变量可以设置是否加载第三方库等。

configureProcessRestrictions(mainExecutableMH);
....
checkEnvironmentVariables(envp);

到这一步获取当前程序的架构就结束了,其中在Xcode中分别配置DYLD_PRINT_OPTS可以打印MachO地址等信息,配置DYLD_PRINT_ENV环境变量可以打印出环江变量等的信息。具体可以自己手动实现一下。

if ( sEnv.DYLD_PRINT_OPTS )
	printOptions(argv);
if ( sEnv.DYLD_PRINT_ENV ) 
	printEnvironmentVariables(envp);
getHostInfo(mainExecutableMH, mainExecutableSlide);

2.2.1 加载共享缓存

此时就到了加载共享缓存,其中checkSharedRegionDisable是检查缓存的禁用状态的。

// load shared cache
checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
.....
if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
		mapSharedCache();
}


static void checkSharedRegionDisable(const dyld3::MachOLoaded* mainExecutableMH, uintptr_t mainExecutableSlide)
{
#if __MAC_OS_X_VERSION_MIN_REQUIRED
	// if main executable has segments that overlap the shared region,
	// then disable using the shared region
	if ( mainExecutableMH->intersectsRange(SHARED_REGION_BASE, SHARED_REGION_SIZE) ) {
		gLinkContext.sharedRegionMode = ImageLoader::kDontUseSharedRegion;
		if ( gLinkContext.verboseMapping )
			dyld::warn("disabling shared region because main executable overlaps\n");
	}
#if __i386__
	if ( !gLinkContext.allowEnvVarsPath ) {
		// <rdar://problem/15280847> use private or no shared region for suid processes
		gLinkContext.sharedRegionMode = ImageLoader::kUsePrivateSharedRegion;
	}
#endif
#endif
	// iOS cannot run without shared region
}

根据源码可以知道,iOS的共享缓存是不能被禁用的。即checkSharedRegionDisable这个函数对于iOS来说是没有意义的。其中共享缓存就是放一些系统的库,例如UIKitFoundation。通过mapSharedCache函数来加载共享缓存库。在mapSharedCache函数中通过loadDyldCache(opts, &sSharedCacheLoadInfo)来共享缓存的加载,其中有仅加载当前进程的共享缓存,如果有加载过的共享缓存也不会再加载的。接下来,加载完共享缓存就是实例化主程序了。

2.2.2 reloadAllImages

这段代码是加载主程序,其中instantiateFromLoadedImage函数通过isCompatibleMachO获取到mach_header可以获取到magic来判断是64bit还是32bit的。获取到cputype来判断MachO文件的兼容性,如果兼容性满足就可以通过instantiateMainExecutable来创建ImageLoader。然后addImage(image)reloadAllImages里面。此时还是在初始化主程序

reloadAllImages:
#endif
	CRSetCrashLogMessage(sLoadingCrashMessage);
	// instantiate ImageLoader for main executable
	sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
//======================
// 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函数是主要的实例化的函数。这个函数加载出来的ImageLoader是一个抽象类。

// determine if this mach-o file has classic or compressed LINKEDIT and number of segments it has
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)
{
	*compressed = false;
	*segCount = 0;
	*libCount = 0;
	*codeSigCmd = NULL;
	*encryptCmd = NULL;
.......
	// fSegmentsArrayCount is only 8-bits
	if ( *segCount > 255 )
		dyld::throwf("malformed mach-o image: more than 255 segments in %s", path);

	// fSegmentsArrayCount is only 8-bits
	if ( *libCount > 4095 )
		dyld::throwf("malformed mach-o image: more than 4095 dependent libraries in %s", path);

	if ( needsAddedLibSystemDepency(*libCount, mh) )
		*libCount = 1;
}

  • compressed:代表的是MachOLC_DYLD_INFO_ONLY的值来加载程序的。
  • segCount:是LC_SEGMENT的命令的长度,通过源码可以知道segCount不能超过255条。
  • libCount:是LC_LOAD_DYLIB的命令长度,就是系统库的,通过源码可以知道libCount不能超过4095条。
  • codeSigCmd:代码的签名。
  • encryptCmd:加密的信息,例如应用上传加壳等的加密信息。
2.2.3 动态库的加载

在主程序的加载完成之后,接下来就是动态库的加载了。根据DYLD_INSERT_LIBRARIES这个环境变量来判断是否插入库。如果这个环境变量有值,就通过loadInsertedDylib函数中load函数来加载插入动态库。``

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

static void loadInsertedDylib(const char* path)
{
	ImageLoader* image = NULL;
	unsigned cacheIndex;
	try {
		LoadContext context;
		context.useSearchPaths		= false;
		context.useFallbackPaths	= false;
		context.useLdLibraryPath	= false;
		context.implicitRPath		= false;
		context.matchByInstallName	= false;
		context.dontLoad			= false;
		context.mustBeBundle		= false;
		context.mustBeDylib			= true;
		context.canBePIE			= false;
		context.enforceIOSMac		= true;
		context.origin				= NULL;	// can't use @loader_path with DYLD_INSERT_LIBRARIES
		context.rpath				= NULL;
		image = load(path, context, cacheIndex);
	}
	catch (const char* msg) {
		if ( gLinkContext.allowInsertFailures )
			dyld::log("dyld: warning: could not load inserted library '%s' into hardened process because %s\n", path, msg);
		else
			halt(dyld::mkstringf("could not load inserted library '%s' because %s\n", path, msg));
	}
	catch (...) {
		halt(dyld::mkstringf("could not load inserted library '%s'\n", path));
	}
}

在加载和插入动态库完成之后,就开始链接主程序了,sInsertedDylibCount是记录插入的动态库数量。

// record count of inserted libraries so that a flat search will look at 
// inserted libraries, then main, then others.
sInsertedDylibCount = sAllImages.size()-1;
// link main executable
gLinkContext.linkingMainExecutable = true;

其中真正链接动态库的是link函数。在link函数中通过recursiveLoadLibraries函数来对三方库加载和链接(符号绑定)。

// link any inserted libraries
// do this after linking main executable so that any dylibs pulled in by inserted 
// dylibs (e.g. libSystem) will not be in front of dylibs the program uses
if ( sInsertedDylibCount > 0 ) {
  for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
	ImageLoader* image = sAllImages[i+1];
	link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
	image->setNeverUnloadRecursive();
  }
  .....
}

void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths, const char* imagePath)
{
	//dyld::log("ImageLoader::link(%s) refCount=%d, neverUnload=%d\n", imagePath, fDlopenReferenceCount, fNeverUnload);
	
	// clear error strings
	(*context.setErrorStrings)(0, NULL, NULL, NULL);

	uint64_t t0 = mach_absolute_time();
	this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths, imagePath);
	...........
}

在对动态库加载和链接完之后还会进行弱绑定,就是对懒加载绑定。

// <rdar://problem/12186933> do weak binding only after all inserted images linked
sMainExecutable->weakBind(gLinkContext);

此时这一系列的操作是:配置环境变量-->加载共享缓存-->实例化主程序-->加载动态库-->链接三方库。这一系列的流程其实都是在读取MachO的过程。并且这一系列的都是在dyld::_main函数中的。

2.3 运行主程序

接着往下就是initializeMainExecutable函数,运行主程序了。此时也走到了流程中的第四步了。

// run all initializers
initializeMainExecutable(); 

通过源码可以一步步从initializeMainExecutable-->runInitializers-->processInitializers-->recursiveInitialization,此时走到recursiveInitialization函数就跳转不了了。需要cmd + shift + o,然后搜索recursiveInitialization就可以找到这个函数的源码。通过函数的调用栈可以知道,在这个函数中会调用到notifySingle这个函数。

void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize,
										  InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
.....省略部分代码......
	// let objc know we are about to initialize this image
	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);
.......省略部分代码............
}

同理通过cmd + shift + o,然后搜索notifySingle也可以找到这个函数的源码。从中可以知道sNotifyObjCInit是一个回调,并且对sNotifyObjCInit做了判断,那就是一定有值的,可以搜索到sNotifyObjCInit是在registerObjCNotifiers函数中赋值的。

static void notifySingle(dyld_image_states state, const ImageLoader* image, ImageLoader::InitializerTimingList* timingInfo)
{
....省略部分代码.....
	if ( (state == dyld_image_state_dependents_initialized) && (sNotifyObjCInit != NULL) && image->notifyObjC() ) {
		uint64_t t0 = mach_absolute_time();
		dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
		(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
		uint64_t t1 = mach_absolute_time();
		uint64_t t2 = mach_absolute_time();
		uint64_t timeInObjC = t1-t0;
		uint64_t emptyTime = (t2-t1)*100;
		if ( (timeInObjC > emptyTime) && (timingInfo != NULL) ) {
			timingInfo->addTime(image->getShortName(), timeInObjC);
		}
	}
.....省略部分代码......
}

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;
....省略部分代码.......
}

那么通过搜索registerObjCNotifiers就可以知道在哪里调用了,得到的结果如下。

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);
}

此时再在dyld源码中搜索_dyld_objc_notify_register函数的调用是搜索不出来的,那么可以在demo中打一个符号断点。可以从函数调用栈中看到_dyld_objc_notify_register函数是在_objc_init函数中调用的。 通过up指令可以知道,_objc_init函数是在libobjc的源码库里面的,这个就是之前文章对底层原理分析一直都用到的objc4-756.2的源码。

objc4-756.2的源码中通过cmd + shift + o搜索_objc_init可以定位到源码,在源码中可以看到有调用_dyld_objc_notify_register函数。并且传的参数分别有&map_imagesload_imagesunmap_image。其中load_images是函数的指针,并且调用了call_load_methods函数,从上面的dyld的加载函数调用栈可以知道。

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);
}

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    // 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();
}

从上面的源码可以知道,在(*sNotifyObjCInit)(image->getRealPath(), image->machHeader())中拿到的image的路径image的MachOHeader是回调到objc的库里面的,即从_dyld_start这个函数栈到dyld::notifySingle这个过程都是在dyld中的。而在_objc_init函数到call_load_methods函数都是objc里面的。在call_load_methods函数中的call_class_loads函数就是遍历循环执行各个类中定义的load类方法

void call_load_methods(void)
{
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 2. Call category +loads ONCE
        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}

但是此时通过(*sNotifyObjCInit)(image->getRealPath(), image->machHeader())的回调,调用完load的类方法。需要继续执行doInitialization函数。

// let objc know we are about to initialize this image
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);

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);
}

此时到这里还没有执行到main函数,在执行到doModInitFunctions函数的时候会执行c++的固定的__attribute__((constructor))构造函数。可以在ViewController中实现如下代码,这些代码会在main之前,load之后执行。

__attribute__((constructor)) void funcC1(){
    printf("\n执行funcC1\n");
}

__attribute__((constructor)) void funcC2(){
    printf("\n执行funcC3\n");
}

__attribute__((constructor)) void funcC3(){
    printf("\n执行funcC3\n");
}

执行完这些之后,此时再次回到dyld::_main函数中,在下面这段代码就是执行到主程序的main函数了。到此时dyld的加载过程可以说是结束了。

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

3.最后

其实通过源码可以知道,dyld的加载过程,这个过程中最主要的都是在dyld的main函数里面。最后就是总结一下dyld的加载流程:

  • dyld是加载所有的库和可执行文件。
  • dyld的加载流程
    • 程序的执行流程是从_dyld_start开始的
    • 进入dyld的main函数(真正的主要函数)
      • 一开始配置一些环境变量
      • 加载共享缓存库(一开始就判断是否禁用,iOS无法被禁用的)
      • 实例化主程序(相当于创建一个对象出来而已)
      • 加载动态库
      • 链接主程序
      • 最关键的地方:初始化方法
        • 经过一系列初始化调用notifySingle函数
          • 该函数会执行一个回调
          • 通过断点调试:该回调是_objc_init的时候赋值的load_images函数
            • load_images里面执行call_load_method函数
              • call_load_method循环调用各个类的load方法
        • doModInitFunctions函数
          • 内部会调用带__attribute__((constructor))的c函数
        • 返回主程序的入口函数(uintptr_t)sMainExecutable->getEntryFromLC_MAIN(),开始进入主程序的main函数。