在之前的篇章中,我们了解的方法调用的本质,知道了消息查找以及消息转发,今天我们来研究一个新的话题,应用程序的加载。
曾经我在面试的时候被问过这么一个问题:点击Run
到main
函数之间发生了什么?在此也是借这个问题来展开我们的分析。当我们点击Run之后,会经历一个编译到执行的过程,本篇暂时对编译部分不做分析(正在看《程序员的自我修养》这本书,对理解编译
很有帮助,后期也会写一篇文章来进行总结和串联),因此本文将通过start
函数并结合Dyld
来进行分析。
dyld(the dynamic link editor)是苹果的动态链接器,是苹果操作系统的一个重要组成部分,在应用被编译打包成可执行文件格式的 Mach-O 文件之后,交由 dyld 负责链接,加载程序 。
dyld源码是开源的,可以到 官网 进行下载
在iOS系统中,每个程序依赖的动态库都需要通过dyld
(位于/usr/lib/dyld)一个一个加载到内存,然而如果在每个程序运行的时候都重复的去加载一次,势必造成运行缓慢,为了优化启动速度和提高程序性能,共享缓存机制就应运而生。所有默认的动态链接库被合并成一个大的缓存文件,放到/System/Library/Caches/com.apple.dyld/
目录下,按不同的架构保存分别保存着。
没有动态库缓存的情况
由图可以看出:如果没有缓存库存在的话,那么我们手机上的每一个App,如果要用到系统动态库的话,是需要每一个App都要去加载一次的,一样的资源被加载多次,无论是空间还是执行效率,都是造成了浪费。有动态库缓存的情况
如果有缓存库存在的话,那么我们手机上的每一个App,如果要用到系统动态库的话,都去加载缓存库就好了,加载缓存库里的动态库会通过dyld这个动态连接器,dyld在加载动态库会做些优化。既然动态库是运行时才加载到内存的,那么意味着Mach-O
中没有这些内容,那么系统是如何找到外部函数的地址并进行调用的呢?
- 在工程编译时 , 所产生的
Mach-O
可执行文件中会预留出一段空间 , 这个空间其实就是符号表 , 存放在_DATA
数据段中 ( 因为_DATA
段在运行时是可读可写的 )- 编译时 : 工程中所有引用了共享缓存区中的系统库方法 , 其指向的地址设置成符号地址 , ( 例如工程中有一个
NSLog
, 那么编译时就会在Mach-O
中创建一个NSLog
的符号 , 工程中的NSLog
就指向这个符号 )- 运行时 : 当
dyld
将应用进程加载到内存中时 , 根据load commands
中列出的需要加载哪些库文件 , 去做绑定的操作 ( 以NSLog
为例 ,dyld
就会去找到Foundation
中NSLog
的真实地址写到_DATA
段的符号表中NSLog
的符号上面 )
这个过程我们称之为:PIC
技术(Position Independent Code
: 位置代码独立 )
我们在main
函数中打上断点,可以看到在main
函数之前还调用了start
start
的调用位于libdyld.dylib
这个库中
也就是说主程序的main函数是由dyld
调起的。下面我们将对dyld
的 源码 进行解读
在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
// // 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()
函数地址。
接下来我将挑重点进行分析
这部分代码篇幅较长就不贴代码了,大家可以自己去看一下,当然我们也可以在Xcode中设置环境变量
setContext
setContext(mainExecutableMH, argc, argv, envp, apple); 复制代码
configureProcessRestrictions
configureProcessRestrictions(mainExecutableMH, envp); 复制代码
checkEnvironmentVariables
{ checkEnvironmentVariables(envp); defaultUninitializedFallbackPaths(envp); } 复制代码
{ getHostInfo(mainExecutableMH, mainExecutableSlide); } 复制代码
checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide); 复制代码
mapSharedCache
if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) { #if TARGET_OS_SIMULATOR if ( sSharedCacheOverrideDir) mapSharedCache(); #else mapSharedCache(); #endif 复制代码
将dyld本身添加到UUID列表addDyldImageToUUIDList
// add dyld itself to UUID list addDyldImageToUUIDList(); 复制代码
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; } 复制代码
这里几个参数我们稍微说明下 :
- 生成镜像文件后,添加到sAllImages全局镜像中
static void addImage(ImageLoader* image) { // add to master list allImagesLock(); sAllImages.push_back(image); allImagesUnlock(); ...... } 复制代码
经过以上步骤 , 主程序的实例化就已经完成了
// 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 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)
中链接主程序中各动态库,进行符号绑定
到了这里,配置环境变量 -> 加载共享缓存 -> 实例化主程序 -> 加载动态库 -> 链接动态库 就已经完成了
在这里将会为主要可执行文件及其带来的一切运行初始化程序
调用顺序 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]); } 复制代码
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); } 复制代码
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); } 复制代码
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); } 复制代码
在此对其中的参数稍作解释:
通过符号断点的方式,我们发现在_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); } 复制代码
doInitialization
在 doModInitFunctions
中 , 值得一提的是会调用 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); } 复制代码
// find entry point for main executable result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN(); 复制代码
到此,dyld的加载流程结束
iOS逆向- 动态库共享缓存(dyld shared cache)