笔者整理了一系列有关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的加载过程中会依赖很多底层的库,但是库是什么呢?库就是可执行代码的二进制,可以被操作系统识别写入到内存中的。在底层库中有分别有静态库和动态库。
在程序的编译过程中是有一个流程的,这个流程如下
静态库:在链接阶段,会将汇编生成的目标与引用的库一起链接打包到可执行文件当中。
动态库:程序编译并不会链接到目标代码中,而是在程序运行时才被载入。
在这个过程中,很明显动态库是比静态库有优势的,这样的话动态库就可以减少打包后的App的大小,可以共享内容节约资源,可以通过更新动态库达到更新App程序的目的。在iOS系统,我们的用到的系统库一般都是动态库,例如:UIKit,libdispatch,libobjc.dyld等。
dyld
全名The dynamic link editor
苹果的动态链接器,是苹果操作系统一个重要组成部分 ,在应用被编译打包成可执行文件格式Mach-O
文件之后,交由dyld
负责链接和加载程序,所以在iOS系统中App启动加载都是在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
。
可以在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-O
的header
。slide
是一个ASLR
随机值,当每一个MachO
加载到内存的会随机加一个变量保证在不定的内存分布,也是为了缓存溢出
的手段。那么start
函数的主要的作用是
ASLR
偏移值。rebaseDyld
开始重定向dyld
和通过mach_init()
来初始化。__guard_setup
来做栈溢出的保护。接着就是执行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
,并且App启动的关键函数也是在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); 复制代码
此时就到了加载共享缓存,其中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来说是没有意义的。其中共享缓存就是放一些系统的库,例如UIKit
,Foundation
。通过mapSharedCache
函数来加载共享缓存库。在mapSharedCache
函数中通过loadDyldCache(opts, &sSharedCacheLoadInfo)
来共享缓存的加载,其中有仅加载当前进程的共享缓存,如果有加载过的共享缓存也不会再加载的。接下来,加载完共享缓存就是实例化主程序了。
这段代码是加载主程序,其中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; } 复制代码
MachO
的LC_DYLD_INFO_ONLY
的值来加载程序的。LC_SEGMENT
的命令的长度,通过源码可以知道segCount
不能超过255
条。LC_LOAD_DYLIB
的命令长度,就是系统库的,通过源码可以知道libCount
不能超过4095
条。在主程序的加载完成之后,接下来就是动态库的加载了。根据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
函数中的。
接着往下就是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_images
,load_images
和unmap_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(); 复制代码
其实通过源码可以知道,App的加载启动流程就是dyld
的加载过程,这个过程中最主要的都是在dyld的main
函数里面。最后就是总结一下dyld
的加载流程:
dyld
是加载所有的库和可执行文件。dyld
的加载流程
_dyld_start
开始的dyld的main
函数(真正的主要函数)
notifySingle
函数
_objc_init
的时候赋值的load_images
函数
load_images
里面执行call_load_method
函数
call_load_method
循环调用各个类的load
方法doModInitFunctions
函数
__attribute__((constructor))
的c函数(uintptr_t)sMainExecutable->getEntryFromLC_MAIN()
,开始进入主程序的main
函数。