应用程序加载:从dyld到objc

想要了解应用程序加载,我们需要了解下面几个问题:

  1. 我们写的代码是如何加载到内存的?
  2. 我们使用的动静态库是如何加载到内存的?
  3. objc是如何启动的?

我们程序执行都会依赖很多库,比如UIKitCoreFoundationlibsystem等,这些库其实就是可执行的二进制文件,能够被操作系统加载到内存。库分为两种:动态库和静态库。

整个编译过程如下图:

image.png

首先我们的代码会经历预编译阶段,进行词法和语法树的分析,然后交给编译器编译,会生成相应的汇编文件,把这些内容链接装载到内存,生成我们的可执行文件。

动态库和静态库区别一个是动态链接,一个是静态链接。

image.png

静态链接:一个一个装载进内存,会有重复的问题,浪费相应性能。 动态链接:并不是直接加载,通过映射加载到内存,内存是共享的只会有一份,大部分苹果的库都为动态库。

静态库和动态库是如何加载到内存的呢?需要连接器dylddyld作用如下图:

image.png

下面我们开始研究dyld,先在main函数位置打一个断点,执行程序:

image.png

看到函数调用栈在main函数之前会执行start方法,点击可以看到start就是执行dyld中的start方法:

image.png

我们再添加一个名字为start的符号断点,再执行程序,发现断点并没有断到start位置

image.png

为什么没有停在start?证明在下层真正调用不是start而是其他方法。我们知道load方法是在程序执行之前就跑,我们添加个load方法并打一个断点,

image.png

看函数调用栈。点击最下面的_dyld_start

image.png

dyld源码可以在官网下载。 打开源码工程,首先全局搜索_dyld_start,发现是汇编实现的,根据架构不同,代码不同,我们直接看真机arm64

image.png

在这里我们分析主要代码逻辑即可,配合注释找到start方法调用:

image.png

全局搜索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函数,点击进去,发现main函数代码非常多,有将近1000行代码,看这种代码需要点技巧,我们知道这个函数是有返回值的,那就先看下返回值是什么,有什么对应操作。

返回值是result,赋值的地方有:

  1. result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();
  2. result = (uintptr_t)sMainExecutable->getEntryFromLC_UNIXTHREAD();
  3. result = (uintptr_t)&fake_main;

上面三个地方有对result赋值,可以看到最主要就是sMainExecutable相关处理,找一下sMainExecutable初始化:

/// instantiate ImageLoader for main executable
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
复制代码

看到注释写的也很明白,初始化镜像文件。点击进去:

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(mh, path, false, &compressed, &segCount, &libCount, context, &codeSigCmd, &encryptCmd);这个代码就是按照machO格式进行加载文件。

共享缓存相关处理:

// load shared cache
checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
复制代码

加载插入的动态库:

// 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);
复制代码

链接插入的动态库:

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(); 
复制代码

回调函数:

// 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]);
}
复制代码

首先拿到所有镜像文件个数,循环跑起来。点击进入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函数,点进去

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);
}
复制代码

还是循环调用recursiveInitialization,找到这个方法的实现void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize, InitializerTimingList& timingInfo, UninitedUpwards& uninitUps), 里面最重要的代码如图:

image.png

先对依赖文件进行初始化,然后再初始化自己。notifySingle全局搜索找到这个方法实现,观察里面最重要代码:(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());。 全局搜索sNotifyObjCInit,发现在registerObjCNotifiers方法中有赋值,再搜索看哪调用这个方法:_dyld_objc_notify_register,这个方法和objc源码中objc_init里面的方法名字是一模一样的。但其实从dyld加载到objc这条线到现在已经断了,需要再找下别的方法。

打开objc源码工程,在源码中_objc_init打一个断点,查看函数调用栈:

image.png

从下往上看执行流程:

  1. _dyld_start
  2. dyldbootstrap::start
  3. dyld::_main
  4. dyld::initializeMainExecutable()
  5. runInitializers
  6. processInitializers
  7. recursiveInitialization
  8. doInitialization
  9. doModInitFunctions

到这是dyld处理的流程,在往上看就不知道是什么流程,我们现在从上往下看,先走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确实调用了_objc_init,再全局搜索_os_object_init,发现是在libdispatch_init有调用,再全局搜索libdispatch_init发现没有调用地方,看上面截图函数调用栈,libSystem调用过来的,我们下载libSystem源码,打开libSystem源码工程全局搜索libdispatch_init,发现是libSystem_initializer方法进行调用,而libSystem_initializer这个方法没有调用,看函数调用栈回到dyld源码进行搜索,没找到相关信息,全局搜索doModInitFunctions看这个方法实现,可以看到框住就是调用libSystem_initializer相关的方法:

image.png

doModInitFunctions是由doInitialization方法调用,最后找到位置:

image.png

到此,整个项目从dyld如何加载到objc_init的流程已经分析完了。 我们可以看到_dyld_objc_notify_register(&map_images, load_images, unmap_image);方法调用,map_imagesload_image都是参数,那么他们什么时候调用的呢,时机肯定在别的地方,继续探索。

dyld源码中搜索_dyld_objc_notify_register ,发现有好几个调用地方,到底真实的是执行哪个呢?我们新建个工程,下一个符号断点_dyld_objc_notify_register ,运行:

image.png

所以_dyld_objc_notify_register调用的代码应该是下面代码:

void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,
                                _dyld_objc_notify_init      init,
                                _dyld_objc_notify_unmapped  unmapped)
{
	if ( gUseDyld3 )
		return dyld3::_dyld_objc_notify_register(mapped, init, unmapped);

	DYLD_LOCK_THIS_BLOCK;
    typedef bool (*funcType)(_dyld_objc_notify_mapped, _dyld_objc_notify_init, _dyld_objc_notify_unmapped);
    static funcType __ptrauth_dyld_function_ptr p = NULL;

	if(p == NULL)
	    dyld_func_lookup_and_resign("__dyld_objc_notify_register", &p);
	p(mapped, init, unmapped);
}
复制代码
void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,
                                _dyld_objc_notify_init      init,
                                _dyld_objc_notify_unmapped  unmapped)
{
    log_apis("_dyld_objc_notify_register(%p, %p, %p)\n", mapped, init, unmapped);

    gAllImages.setObjCNotifiers(mapped, init, unmapped);
}
复制代码

image.png

想要找到map_imagesload_images调用,就需要找到_objcNotifyMapped_objcNotifyInit调用。

之前分析notifySingle调用sNotifyObjCInit,找到sNotifyObjCInit赋值的地方,

image.png

_dyld_objc_notify_register调用的registerObjCNotifiers,也就是说_dyld_objc_notify_register会对三个参数进行初始化,也就是说notifySingle会对将要执行的方法进行初始化赋值,但是具体调用还不知道。

之前我们看到有_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);
}
复制代码

这个方法三个参数对应着objc源码中同名方法的map_imagesload_imagesumap_image,进去看赋值:

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());
		}
	}
}
复制代码

也就是我们现在需要找sNotifyObjCMapped()sNotifyObjCInit()sNotifyObjCUnmapped(),这三个函数调用才会执行我们最关心的map_imagesload_imagesumap_image

  1. 全局搜索sNotifyObjCMapped找调用的地方,发现_dyld_objc_notify_register->registerObjCNotifiers->notifyBatchPartial->sNotifyObjCMapped这个流程会调用,也就是函数执行时候,就会执行第一个参数对应的方法。
  2. 全局搜索sNotifyObjCInit,有两个调用地方,a(dyld_image_state_initialized):_dyld_objc_notify_register->registerObjCNotifiers->sNotifyObjCInit(),加载自己的库的时候会直接调用load_images。b(dyld_image_state_dependents_initialized):notifySingle->sNotifyObjCInit(),加载自己依赖库的时候,依赖库是通过notifySingle来调用load_images
  3. 全局搜索sNotifyObjCUnmapped,发现removeImage才会调用,移除镜像文件时候,暂时不深入分析。

总结下从dyld开始怎么加载到objc以及map_imagesload_images调用的流程图:

image.png

load C++ main 执行顺序

接下来我们再看一个有意思的现象,在main函数中输出Hello, World!,并且在main函数中添加一个C++方法:

int main(int argc, const char * argv[]) {
    @autoreleasepool {        
        SJLog(@"Hello, World!");
    }
    return 0;
}

__attribute__  ((constructor)) void sjFunc() {
    printf("C++ 来了  %s \n", __func__);
}
复制代码

添加SJPerson类,并实现load方法:

@implementation SJPerson

+ (void)load
{
    SJLog(@"%s", __func__);
}

@end
复制代码

运行程序,可以看到打印结果如下:

image.png

首先执行load方法,然后执行C++方法,最后才会走到main函数里面。 研究方法执行顺序,首先我们看下load方法是怎么调用:

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准备所有的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);
    }
}
复制代码

这里实现准备和调用类、分类的load方法。 下一步C++方法为什么会调用呢?在C++方法打个断点,调试打印函数调用栈:

image.png

dyld源码中查看doInitialization函数调用:

image.png

这个位置我们很熟悉了吧,上面已经解释过了,也就是doInitialization会调用我们写的C++方法。

在搞一个骚操作,我们在objc-os.mm文件中,添加一个C++方法,再执行程序:

__attribute__  ((constructor)) void sjFunc() {
    printf("objc C++ 来了  %s \n", __func__);
}
复制代码

看到执行的结果:

image.png

也就是说,在同一个镜像文件中,才会先执行load方法,再执行C++方法。 doInitialization加载所有镜像文件的初始化,并不包含工程的初始化。最后一个C++方法是在工程里面的。

执行完dyld然后是如何走到我们工程里面的main函数呢? 在C++方法打断点,进入断点后显示汇编:

image.png 跳出这个方法:

image.png

读取寄存器:

image.png

汇编最后执行jump raxrax寄存器存的就是我们工程的main函数。

猜你喜欢

转载自juejin.im/post/7066697622370648077