前言
思考一下我们写的代码是如何装载进内存的,我们打包的APP在手机里只是一个文件,如何把这个文件加载进内存的呢?我们都知道OC是运行时,那运行时是何时介入的呢?今天就探索一下Dyld动态链接器,这是程序进内存的通道。Dyld源码比较复杂,我们只探索一下主流程以及几个关键的点,太详细的看会迷失。Dyld自ios11之后就使用dyld3版本,之前的dyld2我们今天就不做分析。提供一下探索必备的资源oc源码,dyld源码,libsystem源码,都是从苹果官方opensource中找的。
lldb探索dlyd加载流程
我们知道load()方法在main()函数之前运行,至于为什么我们下面会分析。为了下面分析的理解,我们用load()方法做钩子看一下dyld加载流程。先写个demo,断点断在load()处真机运行,bt看一下调用栈。
分析:上图已经清晰的显示在调用load()之前,按顺序调用了如下所示函数
_dyld_startdyldbootstrap::startdyld::_maindyld::initializeMainExecutable()ImageLoader::runInitializersImageLoader::processInitializersImageLoader::recursiveInitializationdyld::notifySingleload_images下面挨个简单分析一下这些由dyld调用的函数都干了什么?
1.0 _dyld_start
#if __arm64__ && !TARGET_OS_SIMULATOR
//省略汇编...
__dyld_start:
//省略读取内存进寄存器等...
//调用dyldbootstrap::start方法
// call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
bl __ZN13dyldbootstrap5startEPKN5dyld311MachOLoadedEiPPKcS3_Pm
mov x16,x0 // save entry point address in x16
//L64架构判断
// LC_MAIN case, set up stack for call to main() main方法的处理
//省略main方法函数的处理
br x16
#endif
分析:在dyld源码中搜索发现dyld_starts是用汇编写的,分了很多架构,这里只看一下真机下的汇编。由于汇编代码很长影响大家阅读,所以就省略了很大一部分,只保留了一些注释。主要是调用dyldbootstrap::start方法,该方法应该返回的是main()函数,接着执行这个main()函数。
2.0 dyldbootstrap::start
uintptr_t start(const dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[],
const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue)
{
//通知debugService dyld启动了
dyld3::kdebug_trace_dyld_marker(DBG_DYLD_TIMING_BOOTSTRAP_START, 0, 0, 0, 0);
//在内存中重定位dyld
rebaseDyld(dyldsMachHeader);
const char** apple = envp;
while(*apple != NULL) { ++apple; }
++apple;
__guard_setup(apple);
#if DYLD_INITIALIZER_SUPPORT
runDyldInitializers(argc, argv, envp, apple);
#endif
_subsystem_init(apple);
//现在我们已经完成了 dyld 的引导,调用 dyld 的 main
//获取ASLR虚拟内存偏移量
uintptr_t appsSlide = appsMachHeader->getSlide();
//返回main()方法
return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}
分析:dyldbootstrap::start主要是完成dyld的初始化,通知debugService,获取主程序的虚拟内存偏移,然后调用dyld::_main()
3.0 dyld::_main()
dyld::_main()函数方法有900多行代码,全部贴出来太影响阅读了,这里分段看一下该函数整体流程。
3.1 初始化环境变量
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue)
{
if (dyld3::kdebug_trace_dyld_enabled(DBG_DYLD_TIMING_LAUNCH_EXECUTABLE)) {
launchTraceID = dyld3::kdebug_trace_dyld_duration_start(DBG_DYLD_TIMING_LAUNCH_EXECUTABLE, (uint64_t)mainExecutableMH, 0, 0);
}
//检查并查看是否有任何内核标志
dyld3::BootArgs::setFlags(hexToUInt64(_simple_getenv(apple, "dyld_flags"), nullptr));
//...架构判断
//从环境中获取主可执行文件的 Hash
uint8_t mainExecutableCDHashBuffer[20];
const uint8_t* mainExecutableCDHash = nullptr;
if ( const char* mainExeCdHashStr = _simple_getenv(apple, "executable_cdhash") ) {
unsigned bufferLenUsed;
if ( hexStringToBytes(mainExeCdHashStr, mainExecutableCDHashBuffer, sizeof(mainExecutableCDHashBuffer), bufferLenUsed) )
mainExecutableCDHash = mainExecutableCDHashBuffer;
}
//根据macho头文件配置CPU架构的信息,就是一些文件配置
getHostInfo(mainExecutableMH, mainExecutableSlide);
//...
//result是该函数的返回值,即主程序main()方法
uintptr_t result = 0;
//可执行文件的头文件 mach header
sMainExecutableMachHeader = mainExecutableMH;
//ASLR虚拟内存偏移
sMainExecutableSlide = mainExecutableSlide;
//...
//设置上下文 保存到 gLinkContext中
setContext(mainExecutableMH, argc, argv, envp, apple);
// 拾取指向 exec 路径的指针。
sExecPath = _simple_getenv(apple, "executable_path");
//...
//文件是否是受限的,AFMI 苹果移动文件保护机制
configureProcessRestrictions(mainExecutableMH, envp);
// 检查我们是否应该强制 dyld3
if ( dyld3::internalInstall() ) {
//.....
}
//.....
//环境变量的配置
if ( sEnv.DYLD_PRINT_OPTS )
printOptions(argv);
if ( sEnv.DYLD_PRINT_ENV )
//xcode配置环境变量控制台可以打印信息
printEnvironmentVariables(envp);
分析:
- 进行
内核环境的检测 读取可执行文件的头文件信息,初始化ASLR内存偏移- 以及
设置环境变量
3.2 加载共享缓存
mapSharedCache加载共享缓存,通过loadDyldCache实例化共享缓存
bool loadDyldCache(const SharedCacheOptions& options, SharedCacheLoadInfo* results)
{
results->loadAddress = 0;
results->slide = 0;
results->errorMessage = nullptr;
#if TARGET_OS_SIMULATOR
// 模拟器只支持 mmap() 缓存私下进入进程
return mapCachePrivate(options, results);
#else
//forcePrivate=true 强制私有
//此时系统库不会进入共享缓存,而是在当前进程中
if ( options.forcePrivate ) {
return mapCachePrivate(options, results);
}
else {
bool hasError = false;
//如果系统库在共享缓存中就返回
if ( reuseExistingCache(options, results) ) {
hasError = (results->errorMessage != nullptr);
} else {
//如果系统库不在共享缓存中就加载
hasError = mapCacheSystemWide(options, results);
}
return hasError;
}
#endif
}
分析:ios系统是必须要有共享缓存的,共享缓存就是系统库的初始化。如果APP中用到的系统库存在于共享缓存就不加载,如果不在共享缓存中就加载进共享缓存。注意如果是强制私有的话,系统库就只会加载进当前进程不会在共享缓存中。
3.3 dyld3判断
//...从共享缓存查找mainClosure,没有就创建
//dyld3 开始启动
if ( mainClosure != nullptr ) {
CRSetCrashLogMessage("dyld3: launch started");
if ( mainClosure->topImage()->fixupsNotEncoded() )
sLaunchModeUsed |= DYLD_LAUNCH_MODE_MINIMAL_CLOSURE;
Diagnostics diag;
bool closureOutOfDate;
bool recoverable;
bool launched = launchWithClosure(mainClosure, sSharedCacheLoadInfo.loadAddress, (dyld3::MachOLoaded*)mainExecutableMH,
mainExecutableSlide, argc, argv, envp, apple, diag, &result, startGlue, &closureOutOfDate, &recoverable);
if ( !launched && closureOutOfDate && allowClosureRebuilds ) {
//...
}
//启动成功,获取main函数地址并返回
if ( launched ) {
gLinkContext.startedInitializingMainExecutable = true;
if (sSkipMain)
result = (uintptr_t)&fake_main;
return result;
}
//启动失败
else {
if ( gLinkContext.verboseWarnings ) {
dyld::log("dyld: unable to use closure %p\n", mainClosure);
}
if ( !recoverable )
halt(diag.errorMessage());
}
分析:ios11之后主程序都是用dyld3加载,IOS13以后动态库和三方库用dyld3加载。dyld3通过使用mainClosure闭包模式进行加载,这种方式比dyld2效率更高,但本质流程还是更dyld2是一致的,dyld2经历的流程dyld3都会经历。
3.4 实例化主程序
ImageLoader* ImageLoaderMachO::instantiateMainExecutable(const macho_header* mh, uintptr_t slide, const char* path, const LinkContext& context)
{
bool compressed;
unsigned int segCount;//segment段个数,最大255
unsigned int libCount;//动态库数量 最大4096
const linkedit_data_command* codeSigCmd;
const encryption_info_command* encryptCmd;
//加载LoadCommands
sniffLoadCommands(mh, path, false, &compressed, &segCount, &libCount, context, &codeSigCmd, &encryptCmd);
if ( compressed ) //
return ImageLoaderMachOCompressed::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
else
#if SUPPORT_CLASSIC_MACHO
return ImageLoaderMachOClassic::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
#else
throw "missing LC_DYLD_INFO load command";
#endif
}
分析:主要是实例化主程序,读取macho中LoadCommands信息。macho文件主要是4块内容Header、Load Commands、Data、Text
3.5 加载动态库
//加载任何插入的库,即动态库
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}
// 动态库的数量需要减去主程序,第一个是主程序
sInsertedDylibCount = sAllImages.size()-1;
分析:loadInsertedDylib加载动态库,动态库的数量需要减去主程序,因为第一个是主程序。
3.6 链接主程序
void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths, const char* imagePath)
{
//...
//递归加载所有的动态库
this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths, imagePath);
context.notifyBatch(dyld_image_state_dependents_mapped, preflightOnly);
//...
if ( !context.linkingMainExecutable )
//弱绑定
this->weakBind(context);
t5 = mach_absolute_time();
}
//...
//插入任何动态加载的镜像
if ( !context.linkingMainExecutable && (fgInterposingTuples.size() != 0) ) {
dyld3::ScopedTimer timer(DBG_DYLD_TIMING_APPLY_INTERPOSING, 0, 0, 0);
this->recursiveApplyInterposing(context);
//....
}
分析:link中调用了ImageLoader::link方法,ImageLoader负责加载image文件,递归加载所有动态库,进行弱绑定。
总结:这里我们先总结一下dyld::_main()的流程,因为后面的流程是我们一开始lldb调试的4、5、6、7、8流程,为了更清晰的梳理流程,这里先总结一下上面的流程。
初始化环境变量加载共享缓存dyld3闭包的判断实例化主程序加载动态库链接主程序
4.0 dyld::initializeMainExecutable()
接着上面的分析,链接主程序之后会运行初始化主程序的方法,这个方法很重要,提前划重点这是与OC runtime建立连接的关键,所以要打起精神仔细分析,否则会迷失。
void initializeMainExecutable()
{
gLinkContext.startedInitializingMainExecutable = true;
//为任何插入的dylib 运行初始值设定项
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]);
}
}
// 主程序的初始化
sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);
if ( gLibSystemHelpers != NULL )
(*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL);
//...
}
分析:根据注释,主要功能就是初始化动态库和主程序。跟进入看一下初始化方法runInitializers,源码搜索此方法在ImageLoader.cpp文件中。
5.0 ImageLoader::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() };
//实例化镜像image文件列表
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初始化镜像,跟进processInitializers。
6.0 ImageLoader::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;
//对镜像列表调用递归初始化,构建一个新的镜像列表
//未初始化的向上依赖。
for (uintptr_t i=0; i < images.count; ++i) {
images.imagesAndPaths[i].first->recursiveInitialization(context, thisThread, images.imagesAndPaths[i].second, timingInfo, ups);
}
//如果任何向上的依赖仍然存在,初始化它们。再次把没有初始化的image去初始化
if ( ups.count > 0 )
processInitializers(context, thisThread, timingInfo, ups);
}
分析:递归调用recursiveInitialization初始化镜像。根进recursiveInitialization
7.0 ImageLoader::recursiveInitialization
void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize,
InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
recursive_lock lock_info(this_thread);
recursiveSpinLock(lock_info);
//....
//优先初始化依赖最深的库
for(unsigned int i=0; i < libraryCount(); ++i) {
ImageLoader* dependentImage = libImage(i);
if ( dependentImage != NULL ) {
if ( libIsUpward(i) ) {
uninitUps.imagesAndPaths[uninitUps.count] = { dependentImage, libPath(i) };
uninitUps.count++;
}else if ( dependentImage->fDepth >= fDepth ) {
dependentImage->recursiveInitialization(context, this_thread, libPath(i), timingInfo, uninitUps);
}
}
}
//...
//让 objc 知道我们将要初始化这个图像
uint64_t t1 = mach_absolute_time();
fState = dyld_image_state_dependents_initialized;
oldState = fState;
context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
//初始化image镜像
bool hasInitializers = this->doInitialization(context);
// 初始化完成
fState = dyld_image_state_initialized;
oldState = fState;
context.notifySingle(dyld_image_state_initialized, this, NULL);
//...
}
分析:
- 系统会根据每个库的依赖
深度去初始化,深度值最大的先去初始化,每次初始化都会有一个image文件 所有镜像初始化完成后会调用notifySingle方法,跟进notifySingle方法,注意这个方法的第二个参数是this,this是ImageLoader,不注意这个上下文ImageLoader可能下面的分析会迷失。
8.0 notifySingle
跟进notifySingle发现实际的上下文参数ImageLoader操作的是下面这个方法
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
全局搜索sNotifyObjCInit,发现在registerObjCNotifiers里进行了赋值,全局搜索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_objc_notify_register中调用了registerObjCNotifiers,也就是说此方法在所有镜像文件初始化完成后调用了notifySingle方法。很遗憾的是_dyld_objc_notify_register方法没有再dyld中找到哪个函数调用了。怎么办?下个_dyld_objc_notify_register符号断点看一下调用栈,否则要迷失了。
* frame #0: 0x00000001a1fc34ec libdyld.dylib`_dyld_objc_notify_register
frame #1: 0x00000001b60bc138 libobjc.A.dylib`_objc_init + 1300
frame #2: 0x00000001002d86fc libdispatch.dylib`_os_object_init + 20
frame #3: 0x00000001002e7c80 libdispatch.dylib`libdispatch_init + 292
frame #4: 0x00000001d0bee824 libSystem.B.dylib`libSystem_initializer + 204
frame #5: 0x00000001003b9030 dyld`ImageLoaderMachO::doModInitFunctions(ImageLoader::LinkContext const&) + 424
frame #6: 0x00000001003b93f8 dyld`ImageLoaderMachO::doInitialization(ImageLoader::LinkContext const&) + 52
frame #7: 0x00000001003b3c94 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 536
frame #8: 0x00000001003b3c00 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 388
frame #9: 0x00000001003b1ee0 dyld`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 184
frame #10: 0x00000001003b1fa8 dyld`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 92
frame #11: 0x00000001003a27bc dyld`dyld::initializeMainExecutable() + 136
frame #12: 0x00000001003a7c6c dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 5644
frame #13: 0x00000001003a1208 dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) + 396
frame #14: 0x00000001003a1038 dyld`_dyld_start + 56
分析:通过调用栈发现流程是这样的
dyld_start...上面分析过了...->recursiveInitialization->ImageLoaderMachO::doInitialization->ImageLoaderMachO::doModInitFunctions->ibSystem_initializer->_os_object_init->_objc_init->_dyld_objc_notify_register
也就是说_dyld_objc_notify_register是在_objc_init里调用的。到这里我们总算是从dyld中绕回来,这个在oc源码里有,我们看下一下_objc_init。
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
//环境初始化
environ_init();
tls_init();
static_init();
runtime_init();
exception_init();
#if __OBJC2__
cache_t::init();
#endif
_imp_implementationWithBlock_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}
分析:_objc_init中调用了_dyld_objc_notify_register,dyld_objc_notify_register方法主要是调用load_images方法。
9.0 load_images
void
load_images(const char *path __unused, const struct mach_header *mh)
{
if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
didInitialAttachCategories = true;
loadAllCategories();
}
//如果这里没有 +load 方法,则不带锁返回。
if (!hasLoadMethods((const headerType *)mh)) return;
recursive_mutex_locker_t lock(loadMethodLock);
// 发现加载load方法
{
mutex_locker_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
}
//按顺序调用在列表中的load方法
call_load_methods();
}
分析:load_images主要作用就是调用load()方法,注意这个时候还没有返回main()方法,所以这也解释了为什么load()方法再main()方法之前了,那么load()方法调用有没有顺序呢?因为我们之前面试总会问load方法调用顺序,什么父load > 子load,下面看下prepare_load_methods。
7.3 prepare_load_methods
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;
runtimeLock.assertLocked();
//获取所有非懒加载的类,有load的类都是非懒加载的类
classref_t const *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
//load列表排序
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");

}
//初始化类的方法、属性、协议 即ro rw初始化
realizeClassWithoutSwift(cls, nil);
ASSERT(cls->ISA()->isRealized());
//把分类load加入到列表,按编译顺序
add_category_to_loadable_list(cat);
}
}
分析:先获取所有非懒加载类的load()方法添加进列表,然后再获取分类的load()方法添加进列表,执行load()方法是按照列表顺序挨个执行的,所以说类的load()方法在分类之前执行,完美解析面试题。那么类中load()顺序呢?看下schedule_class_load
7.4 schedule_class_load
static void schedule_class_load(Class cls)
{
if (!cls) return;
ASSERT(cls->isRealized());
//重新排序,递归获取父类的load方法插入到列表前面
schedule_class_load(cls->getSuperclass());
//插入子类load
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
分析:schedule_class_load会递归获取父类的load()插入到列表的前面,然后才是本类的load方法,也就是说父类的load()在子类的load()方法之前执行,完美解析面试题。
总结:
load_image主要是执行懒加载类以及分类的load方法
父类load 先于 子类类load 先于 分类分类与分类的load调用顺序根据编译的先后顺序不存在关系的类与类的load调用顺序根据编译先后顺序
我们来补充一张图总结上面的分析:
总结
痛并快乐着,本来不想写dyld的怕迷失自己,但是又感觉dyld很重要有必要梳理一下。