iOS应用程序在进入main函数前做了什么?

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

当我们用Xcode创建一个工程时,就会自动为我们生成这样一个main函数,我们通常把它认为是程序的入口函数,但事实真的如此吗?
我们在其他一个类中写一个load方法,然后打上断点,在main函数上也打上断点,运行程序,你会发现,先运行到了load方法,然后才是main函数。

事实上,在我们运行程序,再到main方法被调用之间,程序已经做了许许多多的事情,比如我们熟知的runtime的初始化就发生在main函数调用前,还有程序动态库的加载链接也发生在这阶段,本文主要对从程序启动到main函数中发生的主要事情进行简单介绍。

其实简单总结起来就是:
系统先读取App的可执行文件(Mach-O文件),从里面获得dyld的路径,然后加载dyld,dyld去初始化运行环境,开启缓存策略,加载程序相关依赖库(其中也包含我们的可执行文件),并对这些库进行链接,最后调用每个依赖库的初始化方法,在这一步,runtime被初始化。当所有依赖库的初始化后,轮到最后一位(程序可执行文件)进行初始化,在这时runtime会对项目中所有类进行类结构初始化,然后调用所有的load方法。最后dyld返回main函数地址,main函数被调用,我们便来到了熟悉的程序入口。
下面我们将结合代码对整个过程进行分析:

dyld加载

在上一片文章认识MachO中,我们已经大概知道MachO是什么,以及其结构。
系统加载程序MachO文件后,通过分析文件来获得dyld所在路径来加载dyld,然后就将后面的事情甩给dyld了。

从dyld开始

dyld: (the dynamic link editor)动态链接器,其源码是开源的
ImageLoader: 用于辅助加载特定可执行文件格式的类,程序中对应实例可简称为image(如程序可执行文件,Framework库,bundle文件)。

dyld接手后得做很多事情,主要负责初始化程序环境,将可执行文件以及相应的依赖库与插入库加载进内存生成对应的ImageLoader类的image(镜像文件)对象,对这些image进行链接,调用各image的初始化方法等等(注:这里多数事情都是递归的,从底向上的方法调用),其中runtime也是在这个过程中被初始化,这些事情大多数在dyld:_mian方法中被发生,我们可以看段"简洁"的代码:

uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, 
        int argc, const char* argv[], const char* envp[], const char* apple[], 
        uintptr_t* startGlue)
{
        ………………
        sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
      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;

        // link main executable
        gLinkContext.linkingMainExecutable = true;
      ………………
      link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
        sMainExecutable->setNeverUnloadRecursive();
      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);
            }
        }
      ………………
      initializeMainExecutable(); 
      ………………
      
  return result;
}

这里的_main函数是dyld的函数,并非我们程序里的main函数。

  1. sMainExecutable = instantiateFromLoadedImage(....)与loadInsertedDylib(...)
    这一步dyld将我们可执行文件以及插入的lib加载进内存,生成对应的image。
    sMainExecutable对应着我们的可执行文件,里面包含了我们项目中所有新建的类。
    InsertDylib一些插入的库,他们配置在全局的环境变量sEnv中,我们可以在项目中设置环境变量DYLD_PRINT_ENV为1来打印该sEnv的值。
设置打印环境变量

运行后打印如下:

TMPDIR=/Users/a123/Library/Developer/CoreSimulator/Devices/67AD8C57-9304-49B8-AA20-F1D6BD52B2BA/data/Containers/Data/Application/54FA8E15-B04C-4B53-B2BA-9C120C8D53C3/tmp
CA_DEBUG_TRANSACTIONS=0
DYLD_FRAMEWORK_PATH=/Users/a123/Library/Developer/Xcode/DerivedData/WeChat-beoysufqwgvfjaheuqhcnrtbrbnk/Build/Products/Debug-iphonesimulator
OS_ACTIVITY_DT_MODE=YES
__XPC_DYLD_FRAMEWORK_PATH=/Users/a123/Library/Developer/Xcode/DerivedData/WeChat-beoysufqwgvfjaheuqhcnrtbrbnk/Build/Products/Debug-iphonesimulator
HOME=/Users/a123/Library/Developer/CoreSimulator/Devices/67AD8C57-9304-49B8-AA20-F1D6BD52B2BA/data/Containers/Data/Application/54FA8E15-B04C-4B53-B2BA-9C120C8D53C3
SQLITE_ENABLE_THREAD_ASSERTIONS=1
SIMULATOR_VERSION_INFO=CoreSimulator 572.2 - Device: iPhone XR - Runtime: iOS 12.0 (16A366) - DeviceType: iPhone XR
SIMULATOR_UDID=67AD8C57-9304-49B8-AA20-F1D6BD52B2BA
SIMULATOR_MAINSCREEN_SCALE=2.000000
SIMULATOR_EXTENDED_DISPLAY_PROPERTIES=/Users/a123/Library/Developer/CoreSimulator/Devices/67AD8C57-9304-49B8-AA20-F1D6BD52B2BA/data/Library/Application Support/Simulator/extended_display.plist
SIMULATOR_DEVICE_NAME=iPhone XR
SIMULATOR_AUDIO_SETTINGS_PATH=/Users/a123/Library/Developer/CoreSimulator/Devices/67AD8C57-9304-49B8-AA20-F1D6BD52B2BA/data/var/run/simulatoraudio/audiosettings.plist
CFFIXED_USER_HOME=/Users/a123/Library/Developer/CoreSimulator/Devices/67AD8C57-9304-49B8-AA20-F1D6BD52B2BA/data/Containers/Data/Application/54FA8E15-B04C-4B53-B2BA-9C120C8D53C3
DYLD_FALLBACK_LIBRARY_PATH=/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib
SIMULATOR_RUNTIME_VERSION=12.0
SIMULATOR_PRODUCT_CLASS=N84
SIMULATOR_MODEL_IDENTIFIER=iPhone11,8
SIMULATOR_MAINSCREEN_WIDTH=828
SIMULATOR_MAINSCREEN_PITCH=326.000000
SIMULATOR_LEGACY_ASSET_SUFFIX=iphone
SIMULATOR_CAPABILITIES=/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/DeviceTypes/iPhone XR.simdevicetype/Contents/Resources/capabilities.plist
SIMULATOR_BOOT_TIME=1544492876
IPHONE_TVOUT_EXTENDED_PROPERTIES=/Users/a123/Library/Developer/CoreSimulator/Devices/67AD8C57-9304-49B8-AA20-F1D6BD52B2BA/data/Library/Application Support/Simulator/extended_display.plist
IPHONE_SHARED_RESOURCES_DIRECTORY=/Users/a123/Library/Developer/CoreSimulator/Devices/67AD8C57-9304-49B8-AA20-F1D6BD52B2BA/data
CUPS_SERVER=/private/tmp/com.apple.launchd.4AHLLEkczr/Listeners
NSUnbufferedIO=YES
__XPC_DYLD_LIBRARY_PATH=/Users/a123/Library/Developer/Xcode/DerivedData/WeChat-beoysufqwgvfjaheuqhcnrtbrbnk/Build/Products/Debug-iphonesimulator
SIMULATOR_ROOT=/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot
SIMULATOR_HOST_HOME=/Users/a123
PATH=/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/bin:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/bin:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/sbin:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/sbin:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/local/bin
DYLD_ROOT_PATH=/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot
XPC_SERVICE_NAME=UIKitApplication:com.jingying.WeChat[0x43a7][87777]
DYLD_FALLBACK_FRAMEWORK_PATH=/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks
__XCODE_BUILT_PRODUCTS_DIR_PATHS=/Users/a123/Library/Developer/Xcode/DerivedData/WeChat-beoysufqwgvfjaheuqhcnrtbrbnk/Build/Products/Debug-iphonesimulator
RWI_LISTEN_SOCKET=/private/tmp/com.apple.launchd.gljLoJQqm6/com.apple.webinspectord_sim.socket
DYLD_INSERT_LIBRARIES=/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/libBacktraceRecording.dylib:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/libMainThreadChecker.dylib:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/Developer/Library/PrivateFrameworks/DTDDISupport.framework/libViewDebuggerSupport.dylib
CLASSIC=0
SIMULATOR_RUNTIME_BUILD_VERSION=16A366
SIMULATOR_LOG_ROOT=/Users/a123/Library/Logs/CoreSimulator/67AD8C57-9304-49B8-AA20-F1D6BD52B2BA
SIMULATOR_AUDIO_DEVICES_PLIST_PATH=/Users/a123/Library/Developer/CoreSimulator/Devices/67AD8C57-9304-49B8-AA20-F1D6BD52B2BA/data/var/run/com.apple.coresimulator.audio.plist
IOS_SIMULATOR_SYSLOG_SOCKET=/tmp/com.apple.CoreSimulator.SimDevice.67AD8C57-9304-49B8-AA20-F1D6BD52B2BA/syslogsock
DYLD_PRINT_ENV=1
TESTMANAGERD_SIM_SOCK=/private/tmp/com.apple.launchd.IQNoAmSfs1/com.apple.testmanagerd.unix-domain.socket
SIMULATOR_MEMORY_WARNINGS=/Users/a123/Library/Developer/CoreSimulator/Devices/67AD8C57-9304-49B8-AA20-F1D6BD52B2BA/data/var/run/memory_warning_simulation
SIMULATOR_MAINSCREEN_HEIGHT=1792
SIMULATOR_HID_SYSTEM_MANAGER=/Library/Developer/PrivateFrameworks/CoreSimulator.framework/Resources/Platforms/iphoneos/Library/Frameworks/SimulatorHID.framework
IPHONE_SIMULATOR_ROOT=/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot
XPC_FLAGS=0x1
CA_ASSERT_MAIN_THREAD_TRANSACTIONS=0
DYLD_LIBRARY_PATH=/Users/a123/Library/Developer/Xcode/DerivedData/WeChat-beoysufqwgvfjaheuqhcnrtbrbnk/Build/Products/Debug-iphonesimulator:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/system/introspection
XPC_SIMULATOR_LAUNCHD_NAME=com.apple.CoreSimulator.SimDevice.67AD8C57-9304-49B8-AA20-F1D6BD52B2BA
SIMULATOR_SHARED_RESOURCES_DIRECTORY=/Users/a123/Library/Developer/CoreSimulator/Devices/67AD8C57-9304-49B8-AA20-F1D6BD52B2BA/data

其中通DYLD_INSERT_LIBRARIES字段可以看到插入的库为:libBacktraceRecording.dylib和libViewDebuggerSupport.
有时我们会在三方App的Mach-O文件中通过修改DYLD_INSERT_LIBRARIES的值来加入我们自己的动态库,从而注入代码,hook别人的App

  1. link(sMainExecutable,...)和link(image,....)
    对上面生成的Image进行进行链接。其主要做的事有对image进行load(加载),rebase(基地址复位),bind(外部符号绑定),我们可以查看源码:
void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths, const char* imagePath)
{
      ......
      this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths, imagePath);
      ......
      __block uint64_t t2, t3, t4, t5;
    {
        dyld3::ScopedTimer(DBG_DYLD_TIMING_APPLY_FIXUPS, 0, 0, 0);
        t2 = mach_absolute_time();
        this->recursiveRebase(context);
        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();
    }
      ......
      this->recursiveGetDOFSections(context, dofs);
      ......
      
}
  • recursiveLoadLibraries(context, preflightOnly, loaderRPaths)
    递归加载所有依赖库进内存。
  • recursiveRebase(context)
    递归对自己以及依赖库进行复基位操作。在以前,程序每次加载其在内存中的堆栈基地址都是一样的,这意味着你的方法,变量等地址每次都一样的,这使得程序很不安全,后面就出现ASLR(Address space layout randomization,地址空间配置随机加载),程序每次启动后地址都会随机变化,这样程序里所有的代码地址都是错的,需要重新对代码地址进行计算修复才能正常访问。
  • recursiveBindWithAccounting(context, forceLazysBound, neverUnload)
    对库中所有nolazy的符号进行bind,一般的情况下多数符号都是lazybind的,他们在第一次使用的时候才进行bind。
  1. initializeMainExecutable()
    这一步主要是调用所有image的Initalizer方法进行初始化。这里的Initalizers方法并非名为Initalizers的方法,而是C++静态对象初始化构造器,atribute((constructor))进行修饰的方法,在LmageLoader类中initializer函数指针所指向该初始化方法的地址。
typedef void (*Initializer)(int argc, const char* argv[], const char* envp[], const char* apple[]);

我们可以在程序中设置环境变量DYLD_PRINT_INITIALIZERS为1来打印出程序的各种依赖库的initializer方法:

可以打印出调用了Initalizers

运行程序,系统Log打印如下:
lnitializer调用的log

(由于打印的比较长,这样就截取开头的log)可以看到每个依赖库对应着一个初始化方法,名称各有不同。
这里最开始调用的libSystem.dylib的initializer function比较特殊,因为runtime初始化就在这一阶段,而这个方法其实很简单,我们可以在这里看到init.c源码,主要方法如下:

/*
 * libsyscall_initializer() initializes all of libSystem.dylib <rdar://problem/4892197>
 */
static __attribute__((constructor)) 
void libSystem_initializer(int argc, const char* argv[], const char* envp[], const char* apple[], const struct ProgramVars* vars)
{
    _libkernel_functions_t libkernel_funcs = {
        .get_reply_port = _mig_get_reply_port,
        .set_reply_port = _mig_set_reply_port,
        .get_errno = __error,
        .set_errno = cthread_set_errno_self,
        .dlsym = dlsym,
    };

    _libkernel_init(libkernel_funcs);

    bootstrap_init();
    mach_init();
    pthread_init();
    __libc_init(vars, libSystem_atfork_prepare, libSystem_atfork_parent, libSystem_atfork_child, apple);
    __keymgr_initializer();
    _dyld_initializer();
    libdispatch_init();
    _libxpc_initializer();

    __stack_logging_early_finished();

    /* <rdar://problem/11588042>
     * C99 standard has the following in section 7.5(3):
     * "The value of errno is zero at program startup, but is never set
     * to zero by any library function."
     */
    errno = 0;
}

其中libdispatch_init里调用了到了runtime初始化方法_objc_init.我们可以、在程序中打个符号断点来验证:


添加断点

运行程序我们可以看到调用栈:


调用栈

这里可以看到_objc_init调用的顺序,先libSystem_initializer调用libdispatch_init再到_objc_init初始化runtime。

runtime初始化后不会闲着,在_objc_init中注册了几个通知,从dyld这里接手了几个活,其中包括负责初始化相应依赖库里的类结构,调用依赖库里所有的laod方法。
就拿sMainExcuatable来说,它的initializer方法是最后调用的,当initializer方法被调用前dyld会通知runtime进行类结构初始化,然后再通知调用load方法,这些目前还发生在main函数前,但由于lazy bind机制,依赖库多数都是在使用时才进行bind,所以这些依赖库的类结构初始化都是发生在程序里第一次使用到该依赖库时才进行的。

总结
  • dyld 加载所有的库和可执行文件
  • dyld加载流程:
    • 配置一些环境
    • 加载共享缓存库
    • 实例化主程序
    • 加载动态库
    • 链接主程序
    • 最关键的:初始化函数
      • 经过一系列初始化函数的调用notifiySingle函数
        • 此函数执行了一个回调
        • 通过断点调试:此函数是被objc_init初始化时赋值的一个函数load_images
          • load_images里面执行call_load_methods函数
            • 循环各个类的load方法
      • doModInitFunctions函数
        • 内部会调用全局C++对象的构造函数 带__attribubute__(constructor)的c函数
      • 返回主程序的入口函数。开始进入主程序的main函数

main函数被调用

当所有的依赖库库的lnitializer都调用完后,dyld::main函数会返回程序的main函数地址,main函数被调用,从而代码来到了我们熟悉的程序入口。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容