OC底层 - 应用程序加载(1)
前言
一、编译和加载流程简述
二、dyld简介
三、__dyld_start
四、dyldbootstrap::start
五、dyld::_main (核心流程)这里主要分析dyld2的流程
六、总结
最近很多同事在找工作的过程中,都被问到了应用程序运行的时候,main函数之前都做了哪些事情。这就涉及到了dyld的加载流程。因为dyld的知识点确实太多了,我们就先从比较核心的流程来分析一下。
首先我们创建一个demo工程,然后加入一个c++函数,并设置函数属性为constructor,然后在ViewController里面加入+(void)load方法。
__attribute__((constructor))void myTest() {
NSLog(@"1");
}
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
NSLog(@"3");
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
@implementation ViewController
+ (void)load {
NSLog(@"2");
}
- (void)viewDidLoad {
[super viewDidLoad];
}
@end
运行之后,打印顺序为 213。那么为什么加载顺序为 +(void)load -> c++ -> main呢?带着这个问题来进行探索。
一、编译和加载流程简述
基本的编译流程如下:
源文件(.h .m .cpp )-> 预编译 -> 编译 (llvm-cl) -> 汇编 (llvm-as) -> 链接(llvm-ld)动静态库 -> 可执行文件 EXEC。
关于动静态库,可以查看强化篇中具体对静态库和动态库的分析。
基本的加载流程如下:
App启动 -> _dyld_start -> 通过dyld加载 libSytem -> libDispath -> libObjc -> runtime注册回调函数 -> 加载Image -> 执行 map_Images / load_Images (继续加载新image) -> 调用main函数。
从基本的加载流程上可以看出,一切都要从dyld进行分析。
二、dyld简介
首先我们在demo工程里面的+(void)load方法中打一个断点,然后看下它的堆栈:
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 5.1 6.1
* frame #0: 0x00000001014acc97 dyldDemo`+[ViewController load](self=ViewController, _cmd="load") at ViewController.m:17:5
frame #1: 0x00007fff20181ff2 libobjc.A.dylib`load_images + 1439
frame #2: 0x00000001014c0e2c dyld_sim`dyld::notifySingle(dyld_image_states, ImageLoader const*, ImageLoader::InitializerTimingList*) + 425
frame #3: 0x00000001014cfba5 dyld_sim`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 437
frame #4: 0x00000001014cdec7 dyld_sim`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 191
frame #5: 0x00000001014cdf68 dyld_sim`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 82
frame #6: 0x00000001014c126b dyld_sim`dyld::initializeMainExecutable() + 199
frame #7: 0x00000001014c5f56 dyld_sim`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 4789
frame #8: 0x00000001014c01c2 dyld_sim`start_sim + 122
frame #9: 0x0000000106a38a8e dyld`dyld::useSimulatorDyld(int, macho_header const*, char const*, int, char const**, char const**, char const**, unsigned long*, unsigned long*) + 2093
frame #10: 0x0000000106a36168 dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 1198
frame #11: 0x0000000106a30224 dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) + 450
frame #12: 0x0000000106a30025 dyld`_dyld_start + 37
(lldb)
可以看到,一切都是从_dyld_start开始。
dyld 全名: The dynamic link editor, 它是苹果的动态链接器,是苹果操作系统一个重要组成部分。
应用被编译打包成可执行文件格式为Mach-O的文件之后,启动的时候,会把 dyld 加载到进程的地址空间里,然后把后续的启动过程交由dyld负责动态链接,加载程序。
dyld 主要有两个版本:dyld2 和 dyld3,他们的区别简单来说如下:
(1)dyld2 是从 iOS 3.1 引入,一直持续到 iOS 12。dyld2 有个比较大的优化是加入了dyld shared cache(共享缓存)
(2)dyld3 的最重要的特性就是启动闭包,闭包里包含了启动所需要的缓存信息,从而提高启动速度。
我下载了一份dyld-852的源码:源码下载地址
三、__dyld_start
#if __arm__
.text
.align 2
__dyld_start:
mov r8, sp // save stack pointer
sub sp, #16 // make room for outgoing parameters
bic sp, sp, #15 // force 16-byte alignment
// call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
.... 省略
#endif /* __arm__ */
通过源码查看__dyld_start,_dyld_start是用汇编写的。
当我们点开一个应用,系统内核会开启一个进程,然后由dyld开始加载这个可执行文件。
四、dyldbootstrap::start
紧跟着的核心流程是:
call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)。
我们继续进入dyldbootstrap::start中(如果是模拟器会走start_sim)。
#define DYLD_INITIALIZER_SUPPORT 0
//
// 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 dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[],
const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue)
{
// Emit kdebug tracepoint to indicate dyld bootstrap has started <rdar://46878536>
//发出 kdebug 跟踪点以指示 dyld 引导程序已启动
dyld3::kdebug_trace_dyld_marker(DBG_DYLD_TIMING_BOOTSTRAP_START, 0, 0, 0, 0);
// if kernel had to slide dyld, we need to fix up load sensitive locations
// we have to do this before using any global variables
//如果内核必须滑动 dyld,我们需要修复负载敏感位置 // 我们必须在使用任何全局变量之前执行此操作
rebaseDyld(dyldsMachHeader);
// kernel sets up env pointer to be just past end of agv array
//内核将 env 指针设置为刚好经过 agv 数组的末尾
const char** envp = &argv[argc+1];
// kernel sets up apple pointer to be just past end of envp array
const char** apple = envp;
while(*apple != NULL) { ++apple; }
++apple;
// set up random value for stack canary
__guard_setup(apple);
#if DYLD_INITIALIZER_SUPPORT
// run all C++ initializers inside dyld
runDyldInitializers(argc, argv, envp, apple);
#endif
_subsystem_init(apple);
// now that we are done bootstrapping dyld, call dyld's main
uintptr_t appsSlide = appsMachHeader->getSlide();
return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}
在这个函数中的主要流程如下:
1、执行rebaseDyld,根据计算出来的ASLR的slide来重定位macho 以及 初始化 , 允许 dyld 使用 mach 消息传递。
2、执行__guard_setup,栈溢出保护。
3、调用dyld的main函数:dyld::_main。
其中macho_header 对应的就是一下结构体:
struct mach_header_64 {
uint32_t magic; /* mach magic number identifier */
cpu_type_t cputype; /* cpu specifier */
cpu_subtype_t cpusubtype; /* machine specifier */
uint32_t filetype; /* type of file */
uint32_t ncmds; /* number of load commands */
uint32_t sizeofcmds; /* the size of all the load commands */
uint32_t flags; /* flags */
uint32_t reserved; /* reserved */
};
ASLR今后我们会具体分析。
五、dyld::_main (核心流程),这里主要分析dyld2的流程
dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
这个函数的代码太多了,显然一行一行分析估计要炸,所以我们只分析核心部分。
1、配置相关环境变量(平台信息,版本信息,文件路径以及主机信息等等)
2、设置上下文信息 setContext
3、检测进程是否受限,在上下文中做出对应处理configureProcessRestrictions,检测环境变量checkEnvironmentVariables。
4、加载共享缓存库
// iOS cannot run without shared region
// load shared cache
checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
#if TARGET_OS_SIMULATOR
if ( sSharedCacheOverrideDir)
mapSharedCache(mainExecutableSlide);
#else
mapSharedCache(mainExecutableSlide);
#endif
// If this process wants a different __DATA_CONST state from the shared region, then override that now
if ( (sSharedCacheLoadInfo.loadAddress != nullptr) && (gEnableSharedCacheDataConst != sharedCacheDataConstIsEnabled) ) {
uint32_t permissions = gEnableSharedCacheDataConst ? VM_PROT_READ : (VM_PROT_READ | VM_PROT_WRITE);
sSharedCacheLoadInfo.loadAddress->changeDataConstPermissions(mach_task_self(), permissions,
(gLinkContext.verboseMapping ? &dyld::log : nullptr));
}
}
(1)检测共享缓存禁用状态 checkSharedRegionDisable (ios下不会被禁用)
(2)加载共享缓存库 mapSharedCache -> loadDyldCache (模拟器仅支持加载到当前进程) 。
static void mapSharedCache(uintptr_t mainExecutableSlide)
{
dyld3::SharedCacheOptions opts;
opts.cacheDirOverride = sSharedCacheOverrideDir;
opts.forcePrivate = (gLinkContext.sharedRegionMode == ImageLoader::kUsePrivateSharedRegion);
#if __x86_64__ && !TARGET_OS_SIMULATOR
opts.useHaswell = sHaswell;
#else
opts.useHaswell = false;
#endif
opts.verbose = gLinkContext.verboseMapping;
// <rdar://problem/32031197> respect -disable_aslr boot-arg
// <rdar://problem/56299169> kern.bootargs is now blocked
opts.disableASLR = (mainExecutableSlide == 0) && dyld3::internalInstall(); // infer ASLR is off if main executable is not slid
//sSharedCacheLoadInfo 全局变量,加载的共享动态库缓存在这里保存
loadDyldCache(opts, &sSharedCacheLoadInfo);
// update global state
if ( sSharedCacheLoadInfo.loadAddress != nullptr ) {
gLinkContext.dyldCache = sSharedCacheLoadInfo.loadAddress;
dyld::gProcessInfo->processDetachedFromSharedRegion = opts.forcePrivate;
dyld::gProcessInfo->sharedCacheSlide = sSharedCacheLoadInfo.slide;
dyld::gProcessInfo->sharedCacheBaseAddress = (unsigned long)sSharedCacheLoadInfo.loadAddress;
sSharedCacheLoadInfo.loadAddress->getUUID(dyld::gProcessInfo->sharedCacheUUID);
dyld3::kdebug_trace_dyld_image(DBG_DYLD_UUID_SHARED_CACHE_A, sSharedCacheLoadInfo.path, (const uuid_t *)&dyld::gProcessInfo->sharedCacheUUID[0], {0,0}, {{ 0, 0 }}, (const mach_header *)sSharedCacheLoadInfo.loadAddress);
}
}
bool loadDyldCache(const SharedCacheOptions& options, SharedCacheLoadInfo* results)
{
results->loadAddress = 0;
results->slide = 0;
results->errorMessage = nullptr;
#if TARGET_OS_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
}
满足options.forcePrivate 这个条件的话,只加载当前进程。
reuseExistingCache如果缓存已经加载不再处理,如果第一次加载执行mapCacheSystemWide这个函数。
得出结论:动态库的共享缓存是最先被加载的(我们自己开发的动态库不可以)
5. 实例化主程序 , 检测可执行程序格式
// instantiate ImageLoader for main executable
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
// The kernel maps in main executable before dyld gets control. We need to
// make an ImageLoader* for the already mapped in main executable.
//内核在 dyld 获得控制之前映射到主可执行文件中。 我们需要为已经映射到主可执行文件中的 // 制作一个 ImageLoader*。
static ImageLoaderMachO* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path)
{
ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
addImage(image);
return (ImageLoaderMachO*)image;
}
这个函数其实就是在加载我们的macho文件,然后实例化创建一个imageLoader,并添加到全局变量sAllImages中,最后返回一个主程序实例。
static void addImage(ImageLoader* image)
{
// add to master list
allImagesLock();
//添加到sAllImages
sAllImages.push_back(image);
allImagesUnlock();
....省略
}
其中真正实例化主程序是用ImageLoaderMachO作用域中sniffLoadCommands 这个函数去做的 。
void ImageLoaderMachO::sniffLoadCommands(const macho_header* mh, const char* path, bool inCache, bool* compressed,
unsigned int* segCount, unsigned int* libCount, const LinkContext& context,
const linkedit_data_command** codeSigCmd,
const encryption_info_command** encryptCmd)
{
*compressed = false;
*segCount = 0;
*libCount = 0;
*codeSigCmd = NULL;
*encryptCmd = NULL;
/*
...省略掉.
*/
// fSegmentsArrayCount is only 8-bits
if ( *segCount > 255 )
dyld::throwf("malformed mach-o image: more than 255 segments in %s", path);
// fSegmentsArrayCount is only 8-bits
if ( *libCount > 4095 )
dyld::throwf("malformed mach-o image: more than 4095 dependent libraries in %s", path);
if ( needsAddedLibSystemDepency(*libCount, mh) )
*libCount = 1;
}
这个函数就是根据 Load Commands 来加载主程序。
核心点如下 :
compressed (是否压缩) -> 根据 LC_DYLD_INFO_ONYL 来决定。
segCount 段命令数量 , 最大不能超过 255 个。
libCount 依赖库数量 , LC_LOAD_DYLIB (Foundation / UIKit ..) , 最大不能超过 4095 个。
codeSigCmd , 应用签名。
encryptCmd , 应用加密信息。
经过以上步骤 , 主程序的实例化就已经完成了 .
6. 加载插入所有动态库
// 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 环境变量来决定是否需要加载插入的动态库。如果需要插入,会循环去根据动态库的路径,加入动态库。
7.链接主程序
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
8.链接插入的所有动态库
// 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();
}
if ( gLinkContext.allowInterposing ) {
// 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);
}
}
}
9.弱绑定主程序
在链接所有插入的镜像文件后进行弱绑定主程序 (不需要浪费性能再去强引用,也更能方便释放)
sMainExecutable->weakBind(gLinkContext);
至此 , 配置环境变量 -> 加载共享缓存 -> 实例化主程序 -> 加载动态库 -> 链接主程序 -> 链接动态库 就已经完成了
10.初始化主程序
initializeMainExecutable();
11. 通知dyld此进程进入 main()
//通知任何监控进程此进程即将进入 main()
notifyMonitoringDyldMain();