上一章咱们通过编译的旅程,咱们的App已经成功编译完成,生成了对应的Mach-O可执行文件,那么咱们以后要进行启动的相关操做了,启动的时候,咱们是如何加载的动态库,若是执行相似objc_init这些代码的呢git
编译过程传送门☞iOS底层学习 - 从编译到启动的奇幻旅程(一)程序员
在运行的时候,咱们通常都已main
函数为起点,来进行代码编写,可是咱们发现main
函数以前咱们也进行了许多的操做,好比dyld
的一系列操做,本章就来详细探究github
首先安利一本书《程序员的自我修养--连接、装载与库》,看完神清气爽。bootstrap
一个App从可执行文件到真正启动运行代码,基本须要通过装载和动态库连接两个步骤缓存
可执行文件(程序)是一个静态的概念,在运行以前它只是硬盘上的一个文件;而进程是一个动态的概念,它是程序运行时的一个过程,咱们知道每一个程序被运行起来后,它会拥有本身独立的虚拟地址空间,这个地址空间大小的上限是由计算机的硬件(CPU的位数)决定的。bash
进程的虚拟空间都在操做系统的掌握之中,且在操做系统中会同时运行着多个进程,它们彼此之间的虚拟地址空间是隔离的,若是进程访问了操做系统分配给该进程之外的地址空间,会被系统当作非法操做而强制结束进程。网络
装载就是将硬盘上的可执行文件映射到虚拟内存中的过程,但内存是昂贵且稀有的,因此将程序执行时所需的指令和数据所有装载到内存中显然是行不通的,因而人们研究发现了程序运行时是有局部性原理的,能够只将最经常使用的部分驻留在内存中,而不太经常使用的数据存放在磁盘里,这也是动态装载的基本原理架构
装载的过程也能够理解为进程创建的过程,操做系统只须要作如下三件事情:app
连接的共用库分为静态库和动态库:静态库是编译时连接的库,须要连接进你的 Mach-O 文件里,若是须要更新就要从新编译一次,没法动态加载和更新;而动态库是运行时连接的库,使用 dyld 就能够实现动态加载。框架
在真实的 iOS 开发中,你会发现不少功能都是现成可用的,不光你可以用,其余 App 也在用,好比 GUI 框架、I/O、网络等。连接这些共享库到你的Mach-O文件,也是经过连接器来完成的。
iOS 中用到的全部系统framework
(UIKit,Foundation等)都是动态连接的,类比成插头和插排,静态连接的代码在编译后的静态连接过程就将插头和插排一个个插好,运行时直接执行二进制文件;而动态连接须要在程序启动时去完成“插插销”的过程,因此在咱们写的代码执行前,动态链接器须要完成准备工做。
为了节约空间 , 苹果将这些系统库放在了一个地方 : 动态库共享缓存区 (dyld shared cache)
Mach-O 文件是编译后的产物,而动态库在运行时才会被连接,并没参与 Mach-O 文件的编译和连接,所以Mach-O文件中并无包含动态库里的符号定义。
也就是说,这些符号会显示为未定义,但它们的名字和对应的库的路径会被记录下来。运行时经过 dlopen
和 dlsym
导入动态库时,先根据记录的库路径找到对应的库,再经过记录的名字符号找到绑定的地址。
dlopen
会把共享库载入运行进程的地址空间,载入的共享库也会有未定义的符号,这样会触发更多的共享库被载入。dlopen
也能够选择是马上解析全部引用仍是滞后去作。dlopen
打开动态库后返回的是引用的指针,dlsym
的做用就是经过 dlopen
返回的动态库指针和函数符号,获得函数的地址而后使用。
系统使用动态库连接的好处以下:
dyld(the dynamic link editor)是苹果的动态连接器,是苹果操做系统的一个重要组成部分,在应用被编译打包成可执行文件格式的 Mach-O 文件以后,交由 dyld 负责连接,加载程序 。
dyld
的相关代码是开源的☞源码地址
建立一个空工程,咱们知道load函数
是优于main函数
来调用的,因此将断点打在load方法里,看一下函数的调用堆栈。
_dyld_start
开始
dyldbootstrap::start
就是指 dyldbootstrap
这个命名空间做用域里的 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
slide = slideOfMainExecutable(dyldsMachHeader);
bool shouldRebase = slide != 0;
#if __has_feature(ptrauth_calls)
shouldRebase = true;
#endif
if ( shouldRebase ) {
rebaseDyld(dyldsMachHeader, slide);
}
// allow dyld to use mach messaging
mach_init();
// kernel sets up env pointer to be just past end of agv array
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(dyldsMachHeader, slide, argc, argv, envp, apple);
#endif
// now that we are done bootstrapping dyld, call dylds main
uintptr_t appsSlide = slideOfMainExecutable(appsMachHeader);
return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}
复制代码
start函数主要的调用流程为:
1.首先进行bootstrap自举
操做,由于dyld自己也是一个动态库,可是因为它须要连接其余动态库,因此它不依赖其余库,且自己所须要的全局和静态变量的重定位工做由它自己完成,这样就防止了“蛋生鸡,鸡生蛋”的问题
const struct macho_header
这个指Mach-O
文件里的header
intptr_t slide
这个其实就是 ALSR , 说白了就是经过一个随机值 ( 也就是咱们这里的 slide ) 来实现地址空间配置随机加载 ,防止被攻击rebaseDyld
是dyld的重定向2.开放函数消息使用:mach_init()
3.设置堆栈保护:__guard_setup
4.开始连接共享对象:dyld::_main
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue)
{
...这是dyld连接的主要函数,代码太长,逐步分析...
}
复制代码
1.1 从环境变量中主要可执行文件的cdHash
。其中环境变量是系统定义的,能够再Xcode中进行配置
setContext
configureProcessRestrictions
checkEnvironmentVariables
getHostInfo
2.1 验证共享缓存路径:checkSharedRegionDisable
mapSharedCache
将dyld自己添加到UUID列表addDyldImageToUUIDList
4.1 实例化主程序instantiateFromLoadedImage
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
复制代码
内核会映射到主可执行文件中。咱们须要已经映射到主可执行文件中的文件建立一个ImageLoader
// 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
if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
addImage(image);
return (ImageLoaderMachO*)image;
}
throw "main executable not a known format";
}
复制代码
经过instantiateMainExecutable
中的sniffLoadCommands
加载主程序其实就是对MachO文件中LoadCommons段的一些列加载
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)
{
...
for (uint32_t i = 0; i < cmd_count; ++i) {
...
}
复制代码
生成镜像文件后,添加到sAllImages
全局镜像中,主程序永远是sAllImages的第一个对象
static void addImage(ImageLoader* image)
{
// add to master list
allImagesLock();
sAllImages.push_back(image);
allImagesUnlock();
...
}
复制代码
4.2 加载插入动态库loadInsertedDylib
// load any inserted libraries
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}
复制代码
4.3 连接主程序link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
连接主程序中各动态库,进行符号绑定
// link main executable
gLinkContext.linkingMainExecutable = true;
#if SUPPORT_ACCELERATE_TABLES
if ( mainExcutableAlreadyRebased ) {
// previous link() on main executable has already adjusted its internal pointers for ASLR
// work around that by rebasing by inverse amount
sMainExecutable->rebase(gLinkContext, -mainExecutableSlide);
}
#endif
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
sMainExecutable->setNeverUnloadRecursive();
if ( sMainExecutable->forceFlat() ) {
gLinkContext.bindFlat = true;
gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
}
复制代码
至此 , 配置环境变量 -> 加载共享缓存 -> 实例化主程序 -> 加载动态库 -> 连接动态库 就已经完成了 .
函数调用为initializeMainExecutable();
。为主要可执行文件及其带来的一切运行初始化程序
5.1 runInitializers
->processInitializers
初始化准备
void ImageLoader::runInitializers(const LinkContext& context, InitializerTimingList& timingInfo)
{
uint64_t t1 = mach_absolute_time();
mach_port_t thisThread = mach_thread_self();
ImageLoader::UninitedUpwards up;
up.count = 1;
up.images[0] = this;
processInitializers(context, thisThread, timingInfo, up);
context.notifyBatch(dyld_image_state_initialized, false);
mach_port_deallocate(mach_task_self(), thisThread);
uint64_t t2 = mach_absolute_time();
fgTotalInitTime += (t2 - t1);
}
复制代码
5.2 遍历image.count
,递归开始初始化镜像,
void ImageLoader::processInitializers(const LinkContext& context, mach_port_t thisThread,
InitializerTimingList& timingInfo, ImageLoader::UninitedUpwards& images)
{
uint32_t maxImageCount = context.imageCount()+2;
ImageLoader::UninitedUpwards upsBuffer[maxImageCount];
ImageLoader::UninitedUpwards& ups = upsBuffer[0];
ups.count = 0;
// Calling recursive init on all images in images list, building a new list of
// uninitialized upward dependencies.
for (uintptr_t i=0; i < images.count; ++i) {
images.images[i]->recursiveInitialization(context, thisThread, images.images[i]->getPath(), timingInfo, ups);
}
// If any upward dependencies remain, init them.
if ( ups.count > 0 )
processInitializers(context, thisThread, timingInfo, ups);
}
复制代码
5.3 recursiveInitialization
获取到镜像的初始化
void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize,
InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
...
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);
...
}
复制代码
5.3.1 notifySingle
获取到镜像的回调
static void notifySingle(dyld_image_states state, const ImageLoader* image, ImageLoader::InitializerTimingList* timingInfo)
{ ... }
复制代码
重头戏来了 . 根据函数调用栈咱们发现 , 下一步是调用load_images , 但是这个 notifySingle 里并无找到 load_images,其实这是一个回调函数的调用
5.3.2 sNotifyObjCInit
的赋值在registerObjCNotifiers
函数中
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
}
// <rdar://problem/32209809> call 'init' function on all images already init'ed (below libSystem) for (std::vector<ImageLoader*>::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()); } } } 复制代码
5.3.3 registerObjCNotifiers
的调用在_dyld_objc_notify_register
函数中
这个函数是用来给外部共享动态库调用的,好比runtime
中须要加载的objc
库
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped)
{
dyld::registerObjCNotifiers(mapped, init, unmapped);
}
复制代码
咱们能够看到源码中在_objc_init
调用了_dyld_objc_notify_register
3个参数的含义以下:
map_images
: dyld 将 image 加载进内存时 , 会触发该函数.load_images
: dyld 初始化 image 会触发该方法. ( 咱们所熟知的 load 方法也是在此处调用 ) .unmap_image
: dyld 将 image 移除时 , 会触发该函数 .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);
}
复制代码
5.4 doInitialization
这是一个系统特定的C++构造函数的调用方法。
bool ImageLoaderMachO::doInitialization(const LinkContext& context)
{
CRSetCrashLogMessage2(this->getPath());
// mach-o has -init and static initializers
doImageInit(context);
doModInitFunctions(context);
CRSetCrashLogMessage2(NULL);
return (fHasDashInit || fHasInitializers);
}
复制代码
这种C++构造函数有特定的写法,在MachO文件中找到对应的方法,以下
__attribute__((constructor)) void CPFunc(){
printf("C++Func1");
}
复制代码
notifyMonitoringDyldMain
监听dyld的main找到真正 main
函数入口 并返回.
// find entry point for main executable
result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();
复制代码
至此,整个启动流程结束了
大致runtime的加载流程以下
map_images
作解析和处理,接下来 load_images
中调用call_load_methods
方法,遍历全部加载进来的 Class,按继承层级依次调用 Class 的 +load 方法和其 Category 的 +load 方法1.从 kernel 留下的原始调用栈引导和启动本身
2.将程序依赖的动态连接库递归加载进内存,固然这里有缓存机制
3.non-lazy 符号当即 link 到可执行文件,lazy 的存表里
4.Runs static initializers for the executable
5.找到可执行文件的 main 函数,准备参数并调用
6.程序执行中负责绑定 lazy 符号、提供 runtime dynamic loading services、提供调试器接口
7.程序main函数 return 后执行 static terminator
8.某些场景下 main 函数结束后调 libSystem 的 _exit 函数