在之前的篇章中,我们了解的方法调用的本质,知道了消息查找以及消息转发,今天我们来研究一个新的话题,应用程序的加载。
曾经我在面试的时候被问过这么一个问题:点击Run到main函数之间发生了什么?在此也是借这个问题来展开我们的分析。当我们点击Run之后,会经历一个编译到执行的过程,本篇暂时对编译部分不做分析(正在看《程序员的自我修养》这本书,对理解编译很有帮助,后期也会写一篇文章来进行总结和串联),因此本文将通过start函数并结合Dyld来进行分析。
一:知识前导
1. Dyld是什么
dyld(the dynamic link editor)是苹果的动态链接器,是苹果操作系统的一个重要组成部分,在应用被编译打包成可执行文件格式的 Mach-O 文件之后,交由 dyld 负责链接,加载程序 。
dyld源码是开源的,可以到 官网 进行下载
2. 共享缓存
在iOS系统中,每个程序依赖的动态库都需要通过dyld(位于/usr/lib/dyld)一个一个加载到内存,然而如果在每个程序运行的时候都重复的去加载一次,势必造成运行缓慢,为了优化启动速度和提高程序性能,共享缓存机制就应运而生。所有默认的动态链接库被合并成一个大的缓存文件,放到/System/Library/Caches/com.apple.dyld/目录下,按不同的架构保存分别保存着。
没有动态库缓存的情况

有动态库缓存的情况

3. 延伸
既然动态库是运行时才加载到内存的,那么意味着Mach-O中没有这些内容,那么系统是如何找到外部函数的地址并进行调用的呢?
- 在工程编译时 , 所产生的
Mach-O可执行文件中会预留出一段空间 , 这个空间其实就是符号表 , 存放在_DATA数据段中 ( 因为_DATA段在运行时是可读可写的 )- 编译时 : 工程中所有引用了共享缓存区中的系统库方法 , 其指向的地址设置成符号地址 , ( 例如工程中有一个
NSLog, 那么编译时就会在Mach-O中创建一个NSLog的符号 , 工程中的NSLog就指向这个符号 )- 运行时 : 当
dyld将应用进程加载到内存中时 , 根据load commands中列出的需要加载哪些库文件 , 去做绑定的操作 ( 以NSLog为例 ,dyld就会去找到Foundation中NSLog的真实地址写到_DATA段的符号表中NSLog的符号上面 )
这个过程我们称之为:PIC技术(Position Independent Code : 位置代码独立 )
二:dyld加载过程
1. main函数
我们在main函数中打上断点,可以看到在main函数之前还调用了start

start的调用位于libdyld.dylib这个库中

也就是说主程序的main函数是由dyld调起的。下面我们将对dyld的 源码 进行解读
2. start函数
在dyld的源码中,在dyldInitialization.cpp文件中找到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
// 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);
}
在此对函数中两个重要参数稍作解释
const struct macho_header* appsMachHeader, 这个参数就是Mach-O的header.intptr_t slide, 这个其实就是ALSR, 说白了就是通过一个随机值 ( 也就是我们这里的slide) 来实现地址空间配置随机加载- 物理地址 = ALSR + 虚拟地址 ( 偏移 )
那么在这个函数中到底做了什么呢?
-
根据计算出来的
ASLR的slide来重定向macho. -
初始化 , 允许
dyld使用mach消息传递 . -
栈溢出保护 .
-
初始化完成后调用
dyld的main函数 ,dyld::_main
3. dyld::_main
//
// 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)
{
......讲真的,代码太长,下面会挑重点进行分析
}
老规矩先看注释:dyld的入口。内核程序加载dyld并跳转到__dyld_start,__dyld_start是个函数,它会对寄存器进行设置后调用此函数。返回目标程序的main()函数地址。
接下来我将挑重点进行分析
3.1 前期准备
3.1.1 环境变量的配置
这部分代码篇幅较长就不贴代码了,大家可以自己去看一下,当然我们也可以在Xcode中设置环境变量

3.1.2 设置上下文信息 setContext
setContext(mainExecutableMH, argc, argv, envp, apple);
3.1.3 检测线程是否受限,并做相关处理 configureProcessRestrictions
configureProcessRestrictions(mainExecutableMH, envp);
3.1.4 检查环境变量 checkEnvironmentVariables
{
checkEnvironmentVariables(envp);
defaultUninitializedFallbackPaths(envp);
}
3.1.5 获取程序架构getHostInfo
{
getHostInfo(mainExecutableMH, mainExecutableSlide);
}
3.2 加载共享缓存
3.2.1 检测共享缓存禁用状态
checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
3.2.2 加载共享缓存库 mapSharedCache
if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
#if TARGET_OS_SIMULATOR
if ( sSharedCacheOverrideDir)
mapSharedCache();
#else
mapSharedCache();
#endif
3.3 添加dyld到UUID列表
将dyld本身添加到UUID列表addDyldImageToUUIDList
// add dyld itself to UUID list
addDyldImageToUUIDList();
3.4 reloadAllImages
3.4.1 实例化主程序 instantiateFromLoadedImage
// 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";
}
- 在判断条件中,
isCompatibleMachO会去Mach-O的head去检测兼容情况

- 通过
instantiateMainExecutable中的sniffLoadCommands加载主程序其实就是对MachO文件中LoadCommons段的一些列加载
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 -> 根据 LC_DYLD_INFO_ONYL 来决定 .
- segCount 段命令数量 , 最大不能超过 255 个.
- libCount 依赖库数量 , LC_LOAD_DYLIB (Foundation / UIKit ..) , 最大不能超过 4095 个.
- codeSigCmd , 应用签名
- encryptCmd , 应用加密信息
- 生成镜像文件后,添加到sAllImages全局镜像中
static void addImage(ImageLoader* image)
{
// add to master list
allImagesLock();
sAllImages.push_back(image);
allImagesUnlock();
......
}
经过以上步骤 , 主程序的实例化就已经完成了
3.4.2 加载插入动态库
// load any inserted libraries
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}
3.4.3 链接主程序
// link main executable
gLinkContext.linkingMainExecutable = true;
#if SUPPORT_ACCELERATE_TABLES
if ( mainExcutableAlreadyRebased ) {
// previous link() on main executable has already adjusted its internal pointers for ASLR
// work around that by rebasing by inverse amount
sMainExecutable->rebase(gLinkContext, -mainExecutableSlide);
}
#endif
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
sMainExecutable->setNeverUnloadRecursive();
if ( sMainExecutable->forceFlat() ) {
gLinkContext.bindFlat = true;
gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
}
在link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1)中链接主程序中各动态库,进行符号绑定
到了这里,配置环境变量 -> 加载共享缓存 -> 实例化主程序 -> 加载动态库 -> 链接动态库 就已经完成了
3.5 initializeMainExecutable()运行所有初始化程序
在这里将会为主要可执行文件及其带来的一切运行初始化程序
调用顺序 initializeMainExecutableinitializeMainExecutable -> runInitializers -> processInitializers -> 递归调用 recursiveInitialization
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]);
}
3.5.1 初始化准备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.images[0] = this;
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);
}
3.5.2 processInitializers遍历image.count,递归开始初始化镜像`
// <rdar://problem/14412057> upward dylib initializers can be run too soon
// To handle dangling dylibs which are upward linked but not downward, all upward linked dylibs
// have their initialization postponed until after the recursion through downward dylibs
// has completed.
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);
}
3.5.3 recursiveInitialization获取到镜像的初始化
void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize,
InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
...
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);
...
}
notifySingle获取到镜像的回调
void (*notifySingle)(dyld_image_states, const ImageLoader* image, InitializerTimingList*);
根据调用堆栈,我们知道下一步就是调用load_images


sNotifyObjCInit进行了非空判断,也就是有值,在本文件搜索,发现它在registerObjCNotifiers中被赋值
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());
}
}
}
全局搜索,发现只在_dyld_objc_notify_register调用了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);
}
在此对其中的参数稍作解释:
- map_images : dyld 将 image 加载进内存时 , 会触发该函数.
- load_images : dyld 初始化 image 会触发该方法. ( 我们所熟知的 load 方法也是在此处调用 ) .
- unmap_image : dyld 将 image 移除时 , 会触发该函数 .
通过符号断点的方式,我们发现在_objc_init调用了_dyld_objc_notify_register

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);
}
3.5.4 doInitialization
这是系统特定调用的 c++ 的构造方法 .
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);
}
3.5.5 doModInitFunctions
void ImageLoaderMachO::doModInitFunctions(const LinkContext& context)
{
代码太多,下面截取了一处重点
}

在这里会优先加载系统的libSystem,而后才是libdispatch。
3.6 notifyMonitoringDyldMain监听dyld的main
3.7 找到主程序的入口
// find entry point for main executable
result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();
到此,dyld的加载流程结束
四:总结
流程简图

流程细分
