iOS开发攻城狮的集散地iOS 开发每天分享优质文章iOS Developer

一步一步带你揭开main函数之前的面纱(iOS)

2019-02-22  本文已影响1128人  Qinz
Qinz
我们都知道APP的入口函数是main(),而在main()函数调用之前,APP的加载过程是怎样的呢?接下来我们一起来分析APP的加载流程。

一、利用断点进行追踪

  1. 通过上图我们可以看到,在调用堆栈中,我们只看到了star和main,并开启了主线程,其它的什么都看不到。那要怎么才能看到调用堆栈详细点的信息了?我们都知道,有一个方法比main()函数调用更早,那就是load()函数,此时在控制器中写一个load函数,并断点运行,如下图:
02
  1. 通过上图,我们看到了比较详细的函数调用顺序,从第13行的_dyld_start到第3行的dyld:notifySingle,频率出现最多的就是这个dyld的家伙,那么dyld是什么?它在做什么?简单来说dyld是一个动态链接器,用来加载所有的库和可执行文件。接下来我们将通过图2的调用关系,去追踪dyld到底在什么?

二、 dyld加载流程分析

1. 首先下载dyld源码:https://opensource.apple.com/tarballs/dyld/
2. 打开dyld源码工程,根据图2的第12行dyldbootstrap:start为关键字搜索dyldbootstrap中调用的start方法,如下图:
03
3. 该方法源码如下,接下来我们对该方法的重点部分进行分析:
uintptr_t start(const struct macho_header* appsMachHeader, int argc, const char* argv[], intptr_t slide)
{
    // 读取macho文件的头部信息
    const struct macho_header* dyldsMachHeader =  (const struct macho_header*)(((char*)&_mh_dylinker_header)+slide);
    
    // 滑块,设置偏移量,用于重定位
    if ( slide != 0 ) {
        rebaseDyld(dyldsMachHeader, slide);
    }
    
    uintptr_t appsSlide = 0;
        
    // 针对偏移异常的监测
    dyld_exceptions_init(dyldsMachHeader, slide);
    
    // 初始化machO文件
    mach_init();

    // 设置分段保护,这里的分段下面会介绍,属于machO文件格式
    segmentProtectDyld(dyldsMachHeader, slide);
    
    //环境变量指针
    const char** envp = &argv[argc+1];
    
    // 环境变量指针结束的设置
    const char** apple = envp;
    while(*apple != NULL) { ++apple; }
    ++apple;

    // 在dyld中运行所有c++初始化器
    runDyldInitializers(dyldsMachHeader, slide, argc, argv, envp, apple);
    
    // 如果主可执行文件被链接-pie,那么随机分配它的加载地址
    if ( appsMachHeader->flags & MH_PIE )
        appsMachHeader = randomizeExecutableLoadAddress(appsMachHeader, envp, &appsSlide);
    
    // 传入头文件信息,偏移量等。调用dyld的自己的main函数(这里并不是APP的main函数)。
    return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple);
}


struct mach_header_64 {
    uint32_t    magic;      /* 区分系统架构版本 */
    cpu_type_t  cputype;    /*CPU类型 */
    cpu_subtype_t   cpusubtype; /* CPU具体类型 */
    uint32_t    filetype;   /* 文件类型 */
    uint32_t    ncmds;      /* loadcommands 条数,即依赖库数量*/
    uint32_t    sizeofcmds; /* 依赖库大小 */
    uint32_t    flags;      /* 标志位 */
    uint32_t    reserved;   /* 保留字段,暂没有用到*/
};
4. 接下来我们继续追踪,根据图2的调用堆栈,我们知道在dyldbootstrap:star方法中调用了dyld::_main方法,也就是我们上面说到的进入dyld的主程序,如下图:
07
if ( sProcessIsRestricted )
        pruneEnvironmentVariables(envp, &apple);
    else
        checkEnvironmentVariables(envp, ignoreEnvironmentVariables);
    if ( sEnv.DYLD_PRINT_OPTS ) 
        printOptions(argv);
    if ( sEnv.DYLD_PRINT_ENV ) 
        printEnvironmentVariables(envp);
    getHostInfo();  
08
static void getHostInfo()
{
#if 1
    struct host_basic_info info;
    mach_msg_type_number_t count = HOST_BASIC_INFO_COUNT;
    mach_port_t hostPort = mach_host_self();
    kern_return_t result = host_info(hostPort, HOST_BASIC_INFO, (host_info_t)&info, &count);
    if ( result != KERN_SUCCESS )
        throw "host_info() failed";
    
    sHostCPU        = info.cpu_type;
    sHostCPUsubtype = info.cpu_subtype;
#else
    size_t valSize = sizeof(sHostCPU);
    if (sysctlbyname ("hw.cputype", &sHostCPU, &valSize, NULL, 0) != 0) 
        throw "sysctlbyname(hw.cputype) failed";
    valSize = sizeof(sHostCPUsubtype);
    if (sysctlbyname ("hw.cpusubtype", &sHostCPUsubtype, &valSize, NULL, 0) != 0) 
        throw "sysctlbyname(hw.cpusubtype) failed";
#endif
}
    try {
        // 实例化主程序,也就是machO这个可执行文件
        sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
        sMainExecutable->setNeverUnload();
        gLinkContext.mainExecutable = sMainExecutable;
        gLinkContext.processIsRestricted = sProcessIsRestricted;
        // 加载共享缓存库
        checkSharedRegionDisable();
    #if DYLD_SHARED_CACHE_SUPPORT
        if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion )
            mapSharedCache();
    #endif
{
    // isCompatibleMachO 是检查mach-o的subtype是否是当前cpu可以支持
    if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
        ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
//将image添加到imagelist。所以我们在Xcode使用image list命令查看的第一个便是我们的machO
        addImage(image);
        return image;
    }
    
    throw "main executable not a known format";
}
5. 插入库:我们继续看该方法中的剩余源码,这里将会加载所有插入库,逆向中的代码注入就是在这一步完成的,后续我将会详细分析Framwork及dylib的代码注入流程。这里有一个sAllImages.size()-1的操作,实际上是排除了主程序。
    // load any inserted libraries
        if  ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
            for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) 
                loadInsertedDylib(*lib);
        }
        // record count of inserted libraries so that a flat search will look at 
        // inserted libraries, then main, then others.
        sInsertedDylibCount = sAllImages.size()-1;


6. 链接主程序:内部通过imageLoader的实例对象去调用link方法,递归加载所依赖的系统库和第三方库。
        // link main executable
        gLinkContext.linkingMainExecutable = true;
        link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, ImageLoader::RPathChain(NULL, NULL));
        gLinkContext.linkingMainExecutable = false;
        if ( sMainExecutable->forceFlat() ) {
            gLinkContext.bindFlat = true;
            gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
        }
        result = (uintptr_t)sMainExecutable->getMain();
7. 初始化函数
10
8. 运行初始化程序:
11
9. notifySingle函数,这是一个与运行时建立联系的关键函数:
13
void     (*notifySingle)(dyld_image_states, const ImageLoader* image);
  _dyld_objc_notify_register(&map_images, load_images, unmap_image);
14
load_images(const char *path __unused, const struct mach_header *mh)
{
    // 如果这里没有+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();
}
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. 循环调用所有类文件的laod方法
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 2.调用所有分类方法
        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;
}
10. 在调用完notifySigin后,我们发现继续调用了doInitialization,doModInitFunctions会调用machO文件中_mod_init_func段的函数,也就是我们在文件中所定义的全局C++构造函数。
// let objc know we are about to initalize this image
fState = dyld_image_state_dependents_initialized;
oldState = fState;
context.notifySingle(dyld_image_state_dependents_initialized, this);

// initialize this image
this->doInitialization(context);

通过上面的分析,我们从断点开始,查看方法的堆栈调用顺序,一步一步追踪dyld的加载流程,也就将main函数调用前的神秘面纱揭露无疑,你也可以根据上述的步骤自己动手追踪APP的加载过程,这样会更加印象深刻!

总结:main()函数调用之前,其实是做了很多准备工作,主要是dyld这个动态链接器在负责,核心流程如下:

1. 程序执行从_dyld_star开始
2. 配置一些环境变量
3. 实例化主程序,即macho可执行文件。
4. 加载共享缓存库。
5. 插入动态缓存库。
6. 链接主程序。
7. 初始化函数。
8. 返回主程序的入口函数,开始进入主程序的main()函数。

我是Qinz,希望我的文章对你有帮助。

参考文档:https://www.jianshu.com/p/71c38b56c61a

上一篇 下一篇

猜你喜欢

热点阅读