dyld(the dynamic link editor)是苹果的动态连接器,是苹果操作系统一个重要组成部分,在系统内核做好程序准备工作之后,交由dyld负责余下的工作
1.+ (void)load{} 方法断点,起始 _dyld_start,跳入
dyldbootstrap::start(macho_header const*, int, char const**, long, macho_header const*, unsigned long*)
2.dyldInitialization.cpp文件
uintptr_t start(const struct macho_header* appsMachHeader, int argc, const char* argv[], intptr_t slider, const struct macho_header* dyldsMachHeader, uintptr_t* startGlue)
ASLR(Address space layout randomization)是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的。据研究表明ASLR可以有效的降低缓冲区溢出攻击的成功率,如今Linux、FreeBSD、Windows等主流操作系统都已采用了该技术。
3. dyldInitialization::start方法
slide = slideOfMainExecutable(dyldsMachHeader); 计算偏移地址
if (shouldRebase) { rebaseDyld(dyldMachHeader, slide) }; 重定向
mach_init(); 初始化
__guard_setup(apple); 保护栈溢出
return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
4.跳转 dyld.cpp文件
_main(const macho_header* mainExecutableH, uintptr_t mainExecutableSlide, int argc, const char * argv[], const char *envp[], const char *apple[], unitptr_t *startGlue)
第一步,设置运行环境,处理环境变量
代码在开始时候,将传入的变量mainExecutableMH
赋值给了sMainExecutableMachHeader
,这是一个macho_header
类型的变量,其结构体内容就是本章前面介绍的mach_header
结构体,表示的是当前主程序的Mach-O头部信息,有了头部信息,加载器就可以从头开始,遍历整个Mach-O文件的信息。
接着执行了setContext()
,此方法设置了全局一个链接上下文,包括一些回调函数、参数与标志设置信息
第二步,初始化主程序
这一步主要执行了instantiateFromLoadedImage()
static ImageLoader* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path)
执行了instantiateFromLoadedImage(),isCompatibleMachO()
主要检查Mach-O的头部的cputype
与cpusubtype
来判断程序与当前的系统是否兼容。如果兼容接下来就调用instantiateMainExecutable()
实例化主程序
第三步,加载共享缓存
这一步主要执行mapSharedCache()
来映射共享缓存。该函数先通过_shared_region_check_np()
来检查缓存是否已经映射到了共享区域了,如果已经映射了,就更新缓存的slide
与UUID
,然后返回。反之,判断系统是否处于安全启动模式(safe-boot mode)下,如果是就删除缓存文件并返回,正常启动的情况下,接下来调用openSharedCacheFile()
打开缓存文件,该函数在sSharedCacheDir
路径下,打开与系统当前cpu架构匹配的缓存文件,也就是/var/db/dyld/dyld_shared_cache_x86_64h,接着读取缓存文件的前8192字节,解析缓存头dyld_cache_header
的信息,将解析好的缓存信息存入mappings
变量,最后调用_shared_region_map_and_slide_np()
完成真正的映射工作。
共享缓存加载完毕后,接着进行动态库的版本化重载,这主要通过函数checkVersionedPaths()完成。该函数读取DYLD_VERSIONED_LIBRARY_PATH
与DYLD_VERSIONED_FRAMEWORK_PATH
环境变量,将指定版本的库比当前加载的库的版本做比较,如果当前的库版本更高的话,就使用新版本的库来替换掉旧版本的。
第四步,加载插入的动态库
这一步循环遍历DYLD_INSERT_LIBRARIES
环境变量中指定的动态库列表,并调用loadInsertedDylib()
将其加载。该函数调用load()
完成加载工作。load()
会调用loadPhase0()
尝试从文件加载,loadPhase0()
会向下调用下一层phase来查找动态库的路径,直到loadPhase6()
,查找的顺序为DYLD_ROOT_PATH
->LD_LIBRARY_PATH
->DYLD_FRAMEWORK_PATH
->原始路径->DYLD_FALLBACK_LIBRARY_PATH
,找到后调用ImageLoaderMachO::instantiateFromFile()
来实例化一个ImageLoader
,之后调用checkandAddImage()
验证映像并将其加入到全局映像列表中。如果loadPhase0()
返回为空,表示在路径中没有找到动态库,就尝试从共享缓存中查找,找到就调用ImageLoaderMachO::instantiateFromCache()
从缓存中加载,否则就抛出没找到映像的异常。
第五步,链接主程序
这一步执行link()
完成主程序的链接操作。该函数调用了ImageLoader
自身的link()
函数,主要的目的是将实例化的主程序的动态数据进行修正,达到让进程可用的目的,典型的就是主程序中的符号表修正操作。
第六步,链接插入的动态库
链接插入的动态库与链接主程序一样,都是使用的link()
,插入的动态库列表是前面调用addImage()
保存到sAllImages
中的,之后,循环获取每一个动态库的ImageLoader
,调用link()
对其进行链接,注意:sAllImages
中保存的第一项是主程序的映像。接下来调用每个映像的registerInterposing()
方法来注册动态库插入与调用applyInterposing()
应用插入操作。registerInterposing()
查找__DATA
段的__interpose
节区,找到需要应用插入操作(也可以叫作符号地址替换)的数据,然后做一些检查后,将要替换的符号与被替换的符号信息存入fgInterposingTuples
列表中,供以后具体符号替换时查询。applyInterposing()
调用了虚方法doInterpose()
来做符号替换操作,在ImageLoaderMachOCompressed
中实际是调用了eachBind()
与eachLazyBind()
分别对常规的符号与延迟加载的符号进行应用插入操作,具体使用的是interposeAt()
,该方法调用interposedAddress()
在fgInterposingTuples
中查找要替换的符号地址,找到后然后进行最终的符号地址替换。
第七步,执行弱符号绑定
weakBind()
函数执行弱符号绑定。首先通过调用context
的getCoalescedImages()
将sAllImages
中所有含有弱符号的映像合并成一个列表,合并完后调用initializeCoalIterator()
对映像进行排序,排序完成后调用incrementCoalIterator()
收集需要进行绑定的弱符号,后者是一个虚函数,在ImageLoaderMachOCompressed
中,该函数读取映像动态链接信息的weak_bind_off
与weak_bind_size
来确定弱符号的数据偏移与大小,然后挨个计算它们的地址信息。之后调用getAddressCoalIterator()
,按照映像的加载顺序在导出表中查找符号的地址,找到后调用updateUsesCoalIterator()
执行最终的绑定操作,执行绑定的是bindLocation()
,前面有讲过,此处不再赘述。
第八步,执行初始化方法
执行初始化的方法是initializeMainExecutable()
。该函数主要执行runInitializers()
,后者调用了ImageLoader
的runInitializers()
方法,最终迭代执行了ImageLoaderMachO
的doInitialization()
方法,后者主要调用doImageInit()
与doModInitFunctions()
执行映像与模块中设置为init的函数与静态初始化方法
第九步,查找入口点并返回
这一步调用主程序映像的getThreadPC()
函数来查找主程序的LC_MAIN
加载命令获取程序的入口点,没找到就调用getMain()
到LC_UNIXTHREAD
加载命令中去找,找到后就跳到入口点指定的地址并返回了。
到这里,dyld整个加载动态库的过程就算完成了。
另外再讨论下延迟符号加载的技术细节。在所有拥有延迟加载符号的Mach-O文件里,它的符号表中一定有一个dyld_stub_helper
符号,它是延迟符号加载的关键!延迟绑定符号的修正工作就是由它完成的。绑定符号信息可以使用XCode
提供的命令行工具dyldinfo
来查看