selector

dyld源码解读

2020-09-22  本文已影响0人  正_文

dyld 全称是 the dynamic link editor。他是苹果的动态链接器,是苹果操作系统一个重要的组成部分,系统加载、链接mach-O文件,就是由dyld负责的。
前面聊到的应用启动加载过程类、分类加载,都不可避免的触及dyld,所以了解 dyld源码可以让我们更好的理解iOS应用的工作原理。

本文篇幅较长,都是源码和堆栈分析,比较枯燥,因此先上结论:

  1. 加载从 _dyld_start开始
  2. 进入dyld::main函数
    2.1 配置一些环境变量
    2.2 加载共享缓存库(判断是否禁用,iOS无法被禁用)
    2.3 初例化主程序
    2.4 加载插入的动态库
    2.5 链接主程序、链接绑定插入的动态库、弱绑定
    2.6 初始化方法
    2.6.1 经过一系列的方法,会(回调_objc_initload_images函数)
    2.6.2 doModInitFunctions函数,内部会调用c++构造函数
    2.7 返回入口(找到主程序的main方法)

一、_dyld_start

首先我们要找到dyld的入口,即dyld调用可执行文件的地方。
新建一个项目,添加load方法,并打上断点。

load.png

运行,

_dyld_start.png

上图就是我们app开始的地方,下面我们打开dyld的源码,找到对应的方法。

二、dyldbootstrap::start

从上面的堆栈我们可以看到 _dyld_start,调起的第一个方法就是dyldbootstrap::start这个是c++语法,dyldbootstrap是命名空间,start是方法名)。
当然我们也可以全局搜索_dyld_start,找到真正的方法实现。

start.png

通过dyldbootstrap,找到start方法实现(简化):

//
//  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
    // ASLR=slide 滑块 内存地址布局随机化,防止缓存溢出
    slide = slideOfMainExecutable(dyldsMachHeader);
    bool shouldRebase = slide != 0;
    if ( shouldRebase ) {
        rebaseDyld(dyldsMachHeader, slide);
    }

    // allow dyld to use mach messaging
    mach_init();
    
    // set up random value for stack canary
    //栈溢出保护
    __guard_setup(apple);

    // 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-Oheader
slide:这个其实就是ASLRmac OS10.7iOS4.3均开始导入ASLR

具体的流程:根据计算出来的slide(ASLR)来重定向mach-O;初始化允许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){
      //方法太长,900+
}
3.1准备工作,配置环境变量
    uintptr_t result = 0;
    sMainExecutableMachHeader = mainExecutableMH;  //主程序的MachO的头
    sMainExecutableSlide = mainExecutableSlide;    //主程序的ASLR值

    //设置上下文信息
    setContext(mainExecutableMH, argc, argv, envp, apple);

    {
        //检测环境变量 如果受限  禁止加载第三方库
        checkEnvironmentVariables(envp);
        defaultUninitializedFallbackPaths(envp);
    }

    if ( sEnv.DYLD_PRINT_OPTS )
        printOptions(argv);
    if ( sEnv.DYLD_PRINT_ENV ) //打印machO地址
        printEnvironmentVariables(envp);
    //获取主程序的信息:架构、环境变量,准备工作完成
    getHostInfo(mainExecutableMH, mainExecutableSlide);

checkEnvironmentVariables(envp):熟悉越狱插件的同学应该都很清楚,某些环境变量会直接影响该库是否会被加载,有些防护操作就是基于这个原理来做的。
DYLD_PRINT_OPTSDYLD_PRINT_ENV:环境变量,通过scheme配置,分别可以打印mach-O即可执行文件地址、启动时环境变量及其值。

3.2 加载共享缓存

1:检测共享缓存的禁用状态checkSharedRegionDisableios不可以被禁用
由于 iOS 系统中 UIKitFoundation 等系统动态库每个应用都会通过 dyld 加载到内存中,因此,为了节约空间,苹果将这些系统库放在了一个地方:动态库共享缓存区 dyld shared cache, Mac OS 一样有 。
例如,NSLog 的函数实现地址,并不在我们自己的工程的 Mach-O 中。那么我们的工程想要调用 NSLog 方法,如何能找到其真实的实现地址呢???

在工程编译时,所产生的 Mach-O 可执行文件中会预留出一段空间,这个空间其实就是符号表,存放在 _DATA 数据段中(因为 _DATA 段在运行时是可读可写的)
编译时:工程中所有引用了共享缓存区中的系统库方法 , 其指向的地址设置成符号地址,(例如工程中有一个 NSLog,那么编译时就会在 Mach-O 中创建一个 NSLog 的符号,工程中的 NSLog 就指向这个符号)
运行时:当 dyld 将应用进程加载到内存中时,根据 load commands 加载需要的库文件,然后去做相应的绑定操作。(如 NSLogdyld 就会去找到 FoundationNSLog 的真实地址,映射到 _DATA 段符号表的 NSLog 上)。

这个过程被称为 PIC 技术(Position Independent Code:位置代码独立)。这里提一下fishhook,他的函数名称是 rebind_symbols,他的工作原理:将编译后系统库函数所指向的符号,在运行时重绑定到用户指定的函数地址,然后将原系统函数的真实地址赋值到用户指定的指针上

2:加载共享缓存 mapSharedCache() -> loadDyldCache()

bool loadDyldCache(const SharedCacheOptions& options, SharedCacheLoadInfo* results)
{
    results->loadAddress        = 0;
    results->slide              = 0;
    results->errorMessage       = nullptr;

#if TARGET_IPHONE_SIMULATOR
    // simulator only supports mmap()ing cache privately into process
    return mapCachePrivate(options, results);//模拟器 模拟器仅支持加载到当前进程
#else
    if ( options.forcePrivate ) {
        // mmap cache into this process only  仅加载到当前进程
        return mapCachePrivate(options, results);
    }
    else {
        // fast path: when cache is already mapped into shared region
        bool hasError = false;
        // 共享缓存库已经被加载过
        if ( reuseExistingCache(options, results) ) {
            hasError = (results->errorMessage != nullptr);
        } else {
            // slow path: this is first process to load cache
            // 共享缓存第一次加载
            hasError = mapCacheSystemWide(options, results);
        }
        return hasError;
    }
#endif
}
3.3 实例化主程序
// instantiate ImageLoader for main executable
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);

3.3.1:检测可执行程序格式,isCompatibleMachO会通过mach-Oheader里的magiccputypecpusubtype 去检测是否兼容。

// 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 检查machO格式是否合法
    if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
        //实例化主程序
        ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
        addImage(image);//添加到allimage
        return (ImageLoaderMachO*)image;
    }
    
    throw "main executable not a known format";
}

然后真正实例化主程序:instantiateFromLoadedImage -> instantiateMainExecutable -> sniffLoadCommands

sniffLoadCommands,有几个参数(请结合mach-O文件看):
compressed,根据 LC_DYLD_INFO_ONYL 来决定
segCount,segment段命令数量 , 最大不能超过 255
libCount,依赖库数量 , LC_LOAD_DYLIB (Foundation、UIKit等) , 最大不能超过 4095
codeSigCmd , 应用签名
encryptCmd , 应用加密信息

3.3.2:实例化之后,addImage(image)会将这个image添加到static std::vector<ImageLoader*> sAllImages;,这是一个全局的镜像列表,设置好上下文,至此主程序的实例化已经完成。

主程序会第一个被加载到镜像文件列表,所以image list的第一个永远是我们的主程序的可执行文件。

3.3.3:继续往 dyld :: main 下面找,会发现一个方法

// Bind and notify for the main executable now that interposing has been registered
gLinkContext.notifyBatch(dyld_image_state_bound, false);

跟进去,notifyBatch -> notifyBatchPartial -> (*sNotifyObjCMapped)(objcImageCount, paths, mhs);,这个回调函数最终会调起_objc_init里面的回调函数map_images

3.4、加载插入动态库
// load any inserted libraries(越狱的环境都是用这个!) 插入的动态库
if  ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
    for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) 
        loadInsertedDylib(*lib);
}

越狱开发中,根据 DYLD_INSERT_LIBRARIES 环境变量,可以决定是否加载插入的动态库。
越狱的插件就是基于这个原理来实现的,只需要下载插件,就可以影响到应用,有部分防护手段就用到了这个环境变量。

// record count of inserted libraries so that a flat search will look at 
// inserted libraries, then main, then others.
sInsertedDylibCount = sAllImages.size()-1; //记录 插入的动态库的数量
3.5、链接主程序

这里会多次调用link函数,循环加载动态库,对主程序的依赖库进行rebase、符号绑定、弱绑定操作。
3.5.1 链接主程序

// link main executable  链接主程序
gLinkContext.linkingMainExecutable = true;

link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);

3.5.2 链接主程序完成后,判断sInsertedDylibCount插入的动态库数量是否大于0,然后循环调用link链接插入的动态库。

// 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();
    }
    // only INSERTED libraries can interpose 只有插入动态库可以被插入
    // register interposing info after all inserted libraries are bound so chaining works  
    //注册插入的动态库,已保证链接有效
    for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
        ImageLoader* image = sAllImages[i+1];
        image->registerInterposing(gLinkContext);
    }
}

3.5.3 bind 符号绑定和弱绑定

// Bind and notify for the main executable now that interposing has been registered
uint64_t bindMainExecutableStartTime = mach_absolute_time();
sMainExecutable->recursiveBindWithAccounting(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true);
uint64_t bindMainExecutableEndTime = mach_absolute_time();
ImageLoaderMachO::fgTotalBindTime += bindMainExecutableEndTime - bindMainExecutableStartTime;
gLinkContext.notifyBatch(dyld_image_state_bound, false);

// Bind and notify for the inserted images now interposing has been registered
if ( sInsertedDylibCount > 0 ) {
    for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
        ImageLoader* image = sAllImages[i+1];
        image->recursiveBind(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true);
    }
}

// <rdar://problem/12186933> do weak binding only after all inserted images linked
// 弱绑定 完成符号绑定
sMainExecutable->weakBind(gLinkContext);

weakBind弱绑定, 懒加载绑定,也就是说弱绑定一定发生在其他库链接绑定完成之后。

进入link函数后,(注意 linkingMainExecutable 变量)

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);
    //在链接的时候,不仅仅是对主程序进行链接,还有很多依赖库也需要进行链接,所以首先循环加载依赖库
    context.notifyBatch(dyld_image_state_dependents_mapped, preflightOnly);

    // we only do the loading step for preflights
    if ( preflightOnly )
        return;

    uint64_t t1 = mach_absolute_time();
    context.clearAllDepths();
    this->recursiveUpdateDepth(context.imageCount());//递归依赖层级

    __block uint64_t t2, t3, t4, t5;
    {
        dyld3::ScopedTimer(DBG_DYLD_TIMING_APPLY_FIXUPS, 0, 0, 0);
        t2 = mach_absolute_time();
        this->recursiveRebase(context); //必须对主程序和依赖库做重定位rebase(由于ASLR的存在)
        context.notifyBatch(dyld_image_state_rebased, false);

        t3 = mach_absolute_time();
        if ( !context.linkingMainExecutable ) //符号绑定
            this->recursiveBindWithAccounting(context, forceLazysBound, neverUnload);

        t4 = mach_absolute_time();
        if ( !context.linkingMainExecutable )
            this->weakBind(context); //弱绑定
        t5 = mach_absolute_time();
    }

    if ( !context.linkingMainExecutable )
        context.notifyBatch(dyld_image_state_bound, false);
    uint64_t t6 = mach_absolute_time(); 

    std::vector<DOFInfo> dofs;
    this->recursiveGetDOFSections(context, dofs); //注册DOF
    context.registerDOFs(dofs); //注册DOF
    uint64_t t7 = mach_absolute_time(); 

    // interpose any dynamically loaded images
    if ( !context.linkingMainExecutable && (fgInterposingTuples.size() != 0) ) {
        dyld3::ScopedTimer timer(DBG_DYLD_TIMING_APPLY_INTERPOSING, 0, 0, 0);
        this->recursiveApplyInterposing(context);
    }

    // clear error strings
    (*context.setErrorStrings)(0, NULL, NULL, NULL);

    fgTotalLoadLibrariesTime += t1 - t0;
    fgTotalRebaseTime += t3 - t2;
    fgTotalBindTime += t4 - t3;
    fgTotalWeakBindTime += t5 - t4;
    fgTotalDOF += t7 - t6;
    
    // done with initial dylib loads
    fgNextPIEDylibAddress = 0;
}

到这里,主程序已经实例化完毕,但是还没有加载FrameWork已经加载完毕。
思考:不同的FrameWork,谁会被先加载呢???其他他和category文件的加载顺利逻辑一样,都是根据二进制编译顺序相关,在Xcode中可以自由调整,mach-O也可以看到。

3.6、初始化

继续往 dyld :: main 下面找,我们会看到:

// run all initializers
initializeMainExecutable();

跟进去:initializeMainExecutable -> runInitializers -> processInitializers -> recursiveInitialization 递归调用
到这里就没办法跟了,cmd + shift + o搜索recursiveInitialization

// let objc know we are about to initialize this image   load方法加载结束
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);

if ( hasInitializers ) {
    uint64_t t2 = mach_absolute_time();
    timingInfo.addTime(this->getShortName(), t2-t1);
}

notifySingle函数跟进去,最后来到(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());,这个回调函数,最终会调起_objc_init里面的回调函数load_images,这块内容,在 本文第五节 具体分析。

3.6.2、doInitialization

在上一段代码,也就是recursiveInitialization函数实现中,有一个函数doInitialization,搜索然后跟进去,这里会调用 c++构造方法

bool ImageLoaderMachO::doInitialization(const LinkContext& context)
{
    CRSetCrashLogMessage2(this->getPath());

    // mach-o has -init and static initializers
    doImageInit(context);
    doModInitFunctions(context);//加载c++构造函数
    
    CRSetCrashLogMessage2(NULL);
    
    return (fHasDashInit || fHasInitializers);
}

c++构造方法,在mach-Odata段中对应__mod_init_func

3.7、找到主程序入口
// notify any montoring proccesses that this process is about to enter main()
if (dyld3::kdebug_trace_dyld_enabled(DBG_DYLD_TIMING_LAUNCH_EXECUTABLE)) {
    dyld3::kdebug_trace_dyld_duration_end(launchTraceID, DBG_DYLD_TIMING_LAUNCH_EXECUTABLE, 0, 0, 2);
}
notifyMonitoringDyldMain();

// find entry point for main executable 找到主程序的入口,调起main函数
result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();
if ( result != 0 ) {
    // main executable uses LC_MAIN, we need to use helper in libdyld to call into main()
    if ( (gLibSystemHelpers != NULL) && (gLibSystemHelpers->version >= 9) )
        *startGlue = (uintptr_t)gLibSystemHelpers->startGlueToCallExit;
    else
        halt("libdyld.dylib support not present for LC_MAIN");
}

result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN(); 找到真的main入口,至此dyld加载结束。

四、代码验证

4.1 新建一个Single View App项目,在ViewController.m 添加代码。

@implementation ViewController

+ (void)load{
    NSLog(@"来了:%s", __func__);
}

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
}

@end

//c++构造方法
__attribute__((constructor)) void func1() {
    
    printf("\n func1来了 \n");
}

4.2 给当前项目添加一个FrameWork,并添加Inject类,然后添加代码。

#import "Inject.h"

@implementation Inject

+(void)load
{
    NSLog(@"来了:%s", __func__);
}

@end

4.3 运行,查看控制台。

dyld[15456:2366528] 来了:+[Inject load]
dyld[15456:2366528] 来了:+[ViewController load]


 func1来了 

打印结果刚好印证我们上述的的结论,当然如果你想看下多个FrameWork结果,你可以自行添加测试验证。

五、 _objc_init

上文,我们两次提到过notifySingle函数,下面我们来具体分析一下上文第二个notifySingle函数。

5.1 notifySingle

首先我么来回顾一下,我们第一节提到的一张图,_dyld_start 到 load()的调用堆栈图。

image.png

根据调用堆栈,下一步就是调用 load_images,但是cmd + shift + o搜索找到notifySingle函数实现,可是这里并没有找到 load_images 的影子??
但是,注意这里有一句代码,如下:

//方法回调
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());

这是个回调函数的调用,sNotifyObjCInit上面判断了并不会为空,那就代表一定是有值的。那我们在当前文件搜索一下 sNotifyObjCInit,看看什么时候被赋的值。

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());
        }
    }
}

然后我们发现(反推):registerObjCNotifiers <- _dyld_objc_notify_register,在往前就找不到了。
这个时候,我们可以给我们的项目下一个符号断点_dyld_objc_notify_register,看看函数调用栈。

image.png

至此,我们就看到了runtime整个被加载的流程。这里涉及iOS多个库,我们不一一看。可以参考objc 750源码找到_objc_init

5.2 _objc_init
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);
}

在这里我们就看到_dyld_objc_notify_register被调用,传递了三个参数,我在ios应用启动加载过程类、分类加载 做过详细介绍。

map_imagesdyldimage 加载进内存时,会触发该函数。
load_images:dyld初始化 image会触发该方法。(我们所熟知的 load 方法也是在此处调用)。
unmap_image:dyld 将 image 移除时,会触发该函数。

我们可以通过LLDB验证一下:

lldb.png
5.3 dyld链接主程序 到 调起_objc_init的具体流程
  1. dyld 加载到开始链接主程序的时候,递归调用 recursiveInitialization 函数
  2. 这个函数第一次执行,进行 libsystem 的初始化,会走到 doInitialization -> doModInitFunctions -> libSystemInitialized
  3. LibSystem 初始化,会调用起 libdispatch_init,然后会调用 _os_object_init,这个函数里面调用了 _objc_init
  4. _objc_init 中注册并保存了 map_imagesload_imagesunmap_image 函数地址
  5. 注册完毕,继续回到 recursiveInitialization 递归下一次调用。例如 libobjc,当 dyld 来到 recursiveInitialization 调用时,会触发 libsystem 调用到 _objc_init 里注册好的回调函数进行调用,就来到了 libobjc,然后调用 load_images

推荐参考:dyld详解

上一篇下一篇

猜你喜欢

热点阅读