dyld
全称是 the dynamic link editor
。他是苹果的动态链接器,是苹果操作系统一个重要的组成部分,系统加载、链接mach-O
文件,就是由dyld
负责的。
前面聊到的应用启动加载过程、类、分类加载,都不可避免的触及dyld
,所以了解 dyld源码可以让我们更好的理解iOS应用的工作原理。
本文篇幅较长,都是源码和堆栈分析,比较枯燥,因此先上结论:
- 加载从
_dyld_start
开始- 进入
dyld::main
函数
2.1 配置一些环境变量
2.2 加载共享缓存库(判断是否禁用,iOS无法被禁用)
2.3 初例化主程序
2.4 加载插入的动态库
2.5 链接主程序、链接绑定插入的动态库、弱绑定
2.6 初始化方法
2.6.1 经过一系列的方法,会(回调_objc_init
的load_images
函数)
2.6.2doModInitFunctions
函数,内部会调用c++
构造函数
2.7 返回入口(找到主程序的main
方法)
一、_dyld_start
首先我们要找到dyld
的入口,即dyld
调用可执行文件的地方。
新建一个项目,添加load
方法,并打上断点。
运行,
上图就是我们app开始的地方,下面我们打开dyld的源码
,找到对应的方法。
二、dyldbootstrap::start
从上面的堆栈我们可以看到 _dyld_start
,调起的第一个方法就是dyldbootstrap::start
(这个是c++语法,dyldbootstrap
是命名空间,start
是方法名)。
当然我们也可以全局搜索_dyld_start
,找到真正的方法实现。
通过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-O
的header
。
slide
:这个其实就是ASLR,mac OS10.7
、iOS4.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_OPTS
、DYLD_PRINT_ENV
:环境变量,通过scheme
配置,分别可以打印mach-O即可执行文件地址、启动时环境变量及其值。
3.2 加载共享缓存
1:
检测共享缓存的禁用状态checkSharedRegionDisable
,ios不可以被禁用。
由于 iOS 系统中 UIKit
、Foundation
等系统动态库每个应用都会通过 dyld 加载到内存中,因此,为了节约空间,苹果将这些系统库放在了一个地方:动态库共享缓存区 dyld shared cache
, Mac OS 一样有 。
例如,NSLog
的函数实现地址,并不在我们自己的工程的 Mach-O 中。那么我们的工程想要调用 NSLog 方法,如何能找到其真实的实现地址呢???
在工程编译时,所产生的
Mach-O
可执行文件中会预留出一段空间,这个空间其实就是符号表,存放在_DATA
数据段中(因为_DATA
段在运行时是可读可写的)
编译时:工程中所有引用了共享缓存区中的系统库方法 , 其指向的地址设置成符号地址,(例如工程中有一个NSLog
,那么编译时就会在Mach-O
中创建一个NSLog
的符号,工程中的NSLog
就指向这个符号)
运行时:当dyld
将应用进程加载到内存中时,根据load commands
加载需要的库文件,然后去做相应的绑定操作。(如NSLog
,dyld
就会去找到Foundation
中NSLog
的真实地址,映射到_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-O
的header
里的magic
、cputype
、cpusubtype
去检测是否兼容。
// 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
,这是一个全局的镜像列表,设置好上下文,至此主程序的实例化已经完成。
主程序会第一个被加载到镜像文件列表,所以
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);
}
}
// 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 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-O
的data段
中对应__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()的调用堆栈
图。
根据调用堆栈,下一步就是调用 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
}
// call 'init' function on all images already init'ed (below libSystem)
for (std::vector::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
,看看函数调用栈。
至此,我们就看到了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_images
:dyld
将image
加载进内存时,会触发该函数。
load_images
:dyld初始化 image
会触发该方法。(我们所熟知的load
方法也是在此处调用)。
unmap_image
:dyld 将image
移除时,会触发该函数。
我们可以通过LLDB验证一下:
5.3 dyld链接主程序 到 调起_objc_init的具体流程
- 当
dyld
加载到开始链接主程序
的时候,递归调用recursiveInitialization
函数 - 这个函数第一次执行,进行
libsystem
的初始化,会走到doInitialization -> doModInitFunctions -> libSystemInitialized
-
LibSystem
初始化,会调用起libdispatch_init
,然后会调用_os_object_init
,这个函数里面调用了_objc_init
-
_objc_init
中注册并保存了map_images
、load_images
、unmap_image
函数地址 - 注册完毕,继续回到
recursiveInitialization
递归下一次调用。例如libobjc
,当dyld
来到recursiveInitialization
调用时,会触发libsystem
调用到_objc_init
里注册好的回调函数进行调用,就来到了libobjc
,然后调用load_images
推荐参考:dyld详解