在我们初学iOS的时候,分析一个程序的执行流程都是从main函数开始的。但是在main函数之前其实也做了不少操作,值得我们分析一下。
我们知道一个类的load
的方法是先于main
函数执行的,通过对load
方法设置一个断点,查看调用栈可知程序在加载过程中大致所执行的一些方法。
其中可见dyld
(the dynamic link editor),它是苹果的动态链接器,是苹果操作系统一个重要组成部分,在系统内核做好程序准备工作之后,交由dyld负责余下的工作。通过分析dyld
的源码,我们来分析dyld
做了什么。
准备分析
1、通过分析_dyld_start的汇编实现。发现调用了dyldbootstrap::start
方法。
1 | # call dyldbootstrap::start(app_mh, argc, argv, slide, dyld_mh, &startGlue) |
2、在下载好的dyld源码中搜索dyldbootstrap
,在这个命名空间中寻找start
方法。
在这个方法中,通过slideOfMainExecutable
得到因ASLR
产生的偏移。通过rebaseDyld
重绑定。通过__guard_setup
来栈溢出保护。
1 | uintptr_t start(const struct macho_header* appsMachHeader, int argc, const char* argv[], |
这个start
的方法的返回值是调用了一个main
函数,将start的一些值作为参数传到main
。
1 | return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue); |
3、dyld
也可以看做一个程序的执行,它的main函数和我们日常开发应用的main函数类似,都可以看做程序的入口。接下来我们主要便是分析main
函数的实现。
1 | _main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, |
加载过程
0x01 配置环境,设置环境变量等
- 设置上下文
1 | setContext(mainExecutableMH, argc, argv, envp, apple); |
- 配置进程是否受限
1 | configureProcessRestrictions(mainExecutableMH); |
- 检查环境变量
1 | checkEnvironmentVariables(envp); |
- 根据
Xcode
设置的环境变量,来打印程序相应参数。
1 | if ( sEnv.DYLD_PRINT_OPTS ) |
会打印程序相关的目录、用户级别、插入的动态库、动态库的路径等。
1 | opt[0] = "/var/containers/Bundle/Application/731D64D1-8B04-491B-A512-4010011413E6/dyld.app/dyld" |
- 通过
getHostInfo
获取machO头部获取当前运行架构的信息。
1 | static void getHostInfo(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide) |
0x02 加载共享缓存库。
- 判断共享缓存库是否被禁用。
iOS cannot run without shared region
,注释说明iOS平台下是不能被禁用的。
1 | checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide); |
- 通过
mapSharedCache()
函数加载、进入函数内部其主要实现是loadDyldCache
这个函数。其中作了如下三种判断
1 | if ( options.forcePrivate ) { |
0x03 实例化主程序(Mach0,程序的可执行文件)
- 实例化过程:
instantiateFromLoadedImage
1 | static ImageLoaderMachO* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path) |
0x04 加载插入库
- 通过
loadInsertedDylib
方法执行插入动态库的加载。在实现中调用load
方法返回imageLoader
对象,
imageLoader
是一个抽象基类,专门用于辅助加载特定可执行文件格式的类,对于程序中需要的依赖库、插入库,会创建一个对应的image对象,对这些image进行链接,调用各image的初始化方法等等,包括对runtime的初始化。
1 | // load any inserted libraries |
0x05 链接主程序,并加载系统和第三方的动态库
- 在
main
中通过link
链接主程序。
1 | //main 函数中 |
- 内部通过imageLoader的实例对象去调用
link
方法。
1 | //image调用link |
- 递归加载我们所需要的依赖的系统库和第三方库。
1 | this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths, imagePath); |
- 对依赖库进行重定位。相当于加上ASLR滑块。
1 | this->recursiveRebase(context); |
- 递归绑定符号表和弱绑定。
绑定就是将这个二进制调用的外部符号进行绑定的过程。
比如我们objc代码中需要使用到NSObject, 即符号OBJC_CLASS$_NSObject,但是这个符号又不在我们的二进制中,在系统库 Foundation.framework中,因此就需要binding这个操作将对应关系绑定到一起。
lazyBinding就是在加载动态库的时候不会立即binding, 当第一次调用这个方法的时候再实施binding。 做到的方法也很简单: 通过dyld_stub_binder 这个符号来做。
lazy binding的方法第一次会调用到dyld_stub_binder, 然后dyld_stub_binder负责找到真实的方法,并且将地址bind到桩上,下一次就不用再bind了。
1 | this->recursiveBindWithAccounting(context, forceLazysBound, neverUnload); |
1 | this->weakBind(context); |
- 插入动态库。
1 | if ( sInsertedDylibCount > 0 ) {//有的话就开始链接加载。 |
0x06 初始化函数,承前启后
一、dyld流程分析
-> 在main函数中我们进入initializeMainExecutable
->runInitializers
初始化主程序
->processInitializers
->recursiveInitialization
循环初始化
->关键函数 :notifySingle
。在这个方法中调用了objc
的loadImages
。通过command+shift+o
全局搜索寻找实现。
1 | static void notifySingle(dyld_image_states state, const ImageLoader* image, ImageLoader::InitializerTimingList* timingInfo) |
发现一个函数指针的调用:
1 | (*sNotifyObjCInit)(image->getRealPath(), image->machHeader()); |
通过在本文件搜索sNotifyObjCInit
函数指针,我们找到了赋值的地方。
1 | void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped) |
全局搜索registerObjCNotifiers
调用的地方
1 | void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped, |
再次全局搜索_dyld_objc_notify_register
便找不到这个方法调用。于是我们通过Xcode设置符号断点来分析。如图所示,我们则能推断出这个方法的调用是在runtime
中。
二、runtime流程分析
分析runtime
源码。可知上面的函数是在其初始化的时候进行调用的。load_images
赋值到dyld
中的sNotifyObjCInit
指针。
1 | void _objc_init(void) |
在load_images
中,完成call_load_methods
的调用。
1 | load_images(const char *path __unused, const struct mach_header *mh) |
在call_load_methods
中,通过doWhile
循环来调用call_class_loads
实现每个类的load
方法。
1 | void call_load_methods(void) |
三、_ _ attribute_ _((constructor))
是GCC的扩展语法(黑魔法),由它修饰过的函数,会在main函数之前调用。原理是在ELF的.ctors段增加一条函数引用,加载器在执行main函数前,检查.ctror section,并执行里面的函数。
继续dyld
分析。在imageLoader.cpp
文件中,notifySingle
调用之后,接着调用了doInitialization
方法。
其中doModInitFunctions
会调用machO
文件中_mod_init_func
段的函数,也就是我们在文件中所定义的全局C++
构造函数。
1 | bool ImageLoaderMachO::doInitialization(const LinkContext& context) |
通过以上分析加载流程我们可得知函数的执行顺序为:
1 | load -> attribute((constructor)) -> main -> initialize |
0x07 寻找应用程序主函数入口
最后return
,dyld的main
函数结束。
1 | // find entry point for main executable |
至此,程序进入了main函数,开启了我们熟知的一切。