前言
写代码时一般都离不开main,我们知道main是程序的入口,那么系统在main之前做了什么呢,我们去探究下。
编译过程和库
- 程序的编译过程为:
预编译->编译->汇编->链接->可执行文件,如下图:
编译图解
代码查看
- 也可以代码查看流程,以
main.m为例子,命令为:clang -ccc-print-phases -framework Foundation main.m -o main:
clang -ccc-print-phases -framework Foundation main.m -o main
-framework Foundation main.m -o main
+- 0: input, "Foundation", object
| +- 1: input, "main.m", objective-c
| +- 2: preprocessor, {1}, objective-c-cpp-output
| +- 3: compiler, {2}, ir
| +- 4: backend, {3}, assembler
|- 5: assembler, {4}, object
+- 6: linker, {0, 5}, image
7: bind-arch, "x86_64", {6}, image
核心步骤的意思如下:
preprocessor, {1}, objective-c-cpp-output:预处理,编译器前端compiler, {2}, ir:编译器生成中间代码irbackend, {3}, assembler:LLVM后端成成汇编assembler, {4}, object:生存机器码linker, {0, 5}, image:链接器bind-arch, "x86_64", {6}, image:生成可执行的二进制文件image
静态库和动态库
应用程序的加载原理实质是库的二进制文件夹到到内存的过程,库通常分为两种:静态库和动态库:
静态库
- 静他库在
链接的过程就已经把内容链接到可执行文件中。 - 优点:链接完后,库文件就没有作用了,直接可以运行可执行文件。
执行速度比较快 - 缺点:静态库链接时,可能会被多次复制,导致包提及偏大。链接后,内容不可更改
动态库
- 动态库在链接的过程不会把内容链接进去,而是在
执行过程中会去找要链接的内存。 - 优点:由于链接时,重复的地方只被复制一次,
占用的包体积小。链接后还可以通过运行时对内容进行替换。 - 缺点:运行时去找
链接需要的内容会影响运行速度,如果链接后,环境缺少动态库,程序就会无法运行。
图示
动态链接器dyld
dyld简介:
dyld(the dynamic link editor)是苹果的动态链接器,是苹果操作系统一个重要组成部分,在app被编译打包成可执行文件Mach-O后,交由dyld负责余下的工作。
- 所以
App的启动流程如下:
启动入口
现在main上打断点运行,来到断点后,发现main之前有个start:
但是看不到里面内容,然后下一个start符号断点,还是没有断住,说明最开始走的不是这个。我们知道+load函数在main函数之前调用,可以在+load方法处达断点,然后查看调用堆栈:
- 这里我们得知:程序运行是从
_dyld_start开始
源码分析
接下来,我们在源码dyld-852中分析
_dyld_start
- 在
dyld-852工程中搜索_dyld_start:
- 本文以
arm64架构为例,_dyld_start实质是调用dyldbootstrap::start方法,在C++中,::的前面是命名空间,后面是命名空间里的方法,类似类调用类方法。
dyldbootstrap::start
-
这里是
dyldbootstrap调用start,然后去搜索dyldbootstrap,然后找到start函数:- 观察发现,
start函数的核心是返回值为dyld::_main。dyld的main第一个参数是macho_header,它是Mach-O的组成部分之一。 Mach-O是可执行文件的合集,它会将数据流分组,主要为三部分:Mach_header,Load Command,Data, 可以通过MachOView查看。Mach_header:里会有Mach-O的CPU信息,以及Load Command的信息Load Command:包含Mach-O里命令类型信息,名称和二进制的位置。Data:由Segment的数据组成,是Mach-O占比最多的部分,有代码有数据,比如符号表。Data共三个Segment,TEXT、DATA、LINKEDIT。其中TEXT和DATA对应一个或多个Section,LINKEDIT没有Section,需要配合LC_SYMTAB来解析symbol table和string table。这些里面是Mach-O的主要数据
- 详细参考Apple 操作系统可执行文件 Mach-O
- 观察发现,
dyld::_main
- 进入
_main,通过观察发现,_main返回一个result,我们可以从result着手进行分析,大致分为几个步骤:
环境变量配置
- 配置环境,获取
cpu等架构信息
共享缓存
- 检查共享缓存是否开启,未开启则加载共享缓存
主程序初始化
- 加载可执行文件,并生
ImageLoaderMachO成实例
插入动态库
- 遍历
DYLD_INSERT_LIBRARIES(插入的动态库),并调用loadInsertedDylib加载
链接主程序
- 调用link函数链接可执行文件
链接动态库
- 链接插入的动态库
弱符号绑定
执行初始化方法
寻找main
- 从
Load Command中读取LC_MAIN,如果找到则使用libSystemHelper调用main(),如果没找到,dyld需要让start在程序中启动main()
下面将主要介绍
主程序初始化和执行初始化
dyld::_main的 主程序初始化
instantiateFromLoadedImage
- 主程序的初始化是调用
instantiateFromLoadedImage方法,进入方法查看:
//内核映射在主可执行文件之前,dyld得到控制。我们需要为已经映射在主可执行文件中的ImageLoader*创建一个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;
}
- 该方法是通过
instantiateMainExecutable创建一个ImageLoader实例,然后addImage,再强转成ImageLoaderMachO类型返回
instantiateMainExecutable
- 进入
instantiateMainExecutable方法:
// create image for main executable
ImageLoader* ImageLoaderMachO::instantiateMainExecutable(const macho_header* mh, uintptr_t slide, const char* path, const LinkContext& context)
{
sizeof(ImageLoaderMachOClassic), sizeof(ImageLoaderMachOCompressed));
bool compressed;
unsigned int segCount;
unsigned int libCount;
const linkedit_data_command* codeSigCmd;
const encryption_info_command* encryptCmd;
// 确定这个macho文件是否具有经典的或压缩的LINKEDIT以及它的段数
sniffLoadCommands(mh, path, false, &compressed, &segCount, &libCount, context, &codeSigCmd, &encryptCmd);
// instantiate concrete class based on content of load commands
if ( compressed )
return ImageLoaderMachOCompressed::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
else
#if SUPPORT_CLASSIC_MACHO
return ImageLoaderMachOClassic::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
#else
throw "missing LC_DYLD_INFO load command";
#endif
}
- 这里的主要作用是创建主程序的映射,返回一个
ImageLoader类型对象,也就是image镜像文件。sniffLoadCommands函数是确定machO文件的相关信息
dyld::_main的 执行初始化
initializeMainExecutable
- 执行初始化是调用
initializeMainExecutable方法:
void initializeMainExecutable()
{
// record that we've reached this step
gLinkContext.startedInitializingMainExecutable = true;
// run initialzers for any inserted dylibs
ImageLoader::InitializerTimingList initializerTimes[allImagesCount()];
initializerTimes[0].count = 0;
const size_t rootCount = sImageRoots.size();
if ( rootCount > 1 ) {
for(size_t i=1; i < rootCount; ++i) {
sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
}
}
// run initializers for main executable and everything it brings up
sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);
// register cxa_atexit() handler to run static terminators in all loaded images when this process exits
if ( gLibSystemHelpers != NULL )
(*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL);
// dump info if requested
if ( sEnv.DYLD_PRINT_STATISTICS )
ImageLoader::printStatistics((unsigned int)allImagesCount(), initializerTimes[0]);
if ( sEnv.DYLD_PRINT_STATISTICS_DETAILS )
ImageLoaderMachO::printStatisticsDetails((unsigned int)allImagesCount(), initializerTimes[0]);
}
- 先通过
sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0])执行主可执行文件的一些依赖文件,都执行完后再去执行主可执行文件
runInitializers
- 进入
runInitializers:
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.imagesAndPaths[0] = { this, this->getPath() };
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);
}
- 通过观察猜测核心代码是
processInitializers和notifyBatch
processInitializers
- 进入
processInitializers:
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.
// 对images列表中的所有镜像递归init,构建一个未初始化向上依赖的新列表
for (uintptr_t i=0; i < images.count; ++i) {
images.imagesAndPaths[i].first->recursiveInitialization(context, thisThread, images.imagesAndPaths[i].second, timingInfo, ups);
}
// If any upward dependencies remain, init them.
if ( ups.count > 0 )
processInitializers(context, thisThread, timingInfo, ups);
}
- 这里每个
image都调用recursiveInitialization方法,
recursiveInitialization
- 全局搜索定位到该函数的实现:
- 这里面主要是两个函数,一个是
notifySingle,另一个是doInitialization,先来看看第一个
notifySingle
- 搜索
notifySingle:
- 通过分析定位到方法的重点
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader()),再来看看sNotifyObjCInit,它是一个全局的_dyld_objc_notify_init类型变量:
static _dyld_objc_notify_init 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
}
// <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());
}
}
}
- 也就是说调用
registerObjCNotifiers方法时,sNotifyObjCInit会被赋值,再继续搜索registerObjCNotifiers:
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);
}
- 最终点位到了
_dyld_objc_notify_register,但此时dyld源码中并没有它的调用,再去objc源码中查看:
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();
runtime_init();
exception_init();
#if __OBJC2__
cache_t::init();
#endif
_imp_implementationWithBlock_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}
-
由此可见
sNotifyObjCInit的赋值实质上是load_images,再来看看load_images: -
再进入
call_load_methods方法,里面有个do-while循环调用call_load_methods
- 分别进入
call_load_methods和call_category_loads方法:call_load_methods:call_category_loads:
- 这里就能看到
cls和分类调用load方法,也就是load_images方法就是调用+load方法,刚好走完前面的堆栈信息:
总结:
+load方法的流程为:_dyld_start->dyldbootstrap::start->dyld::_main->initializeMainExecutable->ImageLoader::runInitializers->ImageLoader::processInitializers->ImageLoader::recursiveInitialization->notifySingle->sNotifyObjCInit->load_images->+load
- 现在知道了
+load的流程,但是上面提到的_objc_init是什么时候调用,接下来继续去探究
doInitialization
- 在
objc源码的_objc_init处打个断点,然后bt查看堆栈: - 在堆栈里发现了
_objc_init的调用,doInitialization->doModInitFunctions->libSystem_initializer->libdispatch_init->_os_object_init->_objc_init,下面今天反向推导: -
- 先看
_os_object_init,它是在libdispatch源码中,
- 先看
void
_os_object_init(void)
{
_objc_init();
...
}
然后就找到_os_object_init调用_objc_init
- 2. 再搜索
libdispatch_init:
void
libdispatch_init(void)
{
...
_os_object_init();
...
}
找到了libdispatch_init调用_os_object_init,然后根据堆栈提示继续看
- 3.
libSystem_initializer是在libSystem源码中,在源码中搜索方法:
static void
libSystem_initializer(int argc,
const char* argv[],
const char* envp[],
const char* apple[],
const struct ProgramVars* vars)
{
...
libdispatch_init();
...
}
- 不出意外的在
libSystem_initializer找到了libdispatch_init方法,再继续看堆栈,doModInitFunctions和doInitialization都在dyld源码,我们继续去查看dyld源码: -
- 在
dyld源码搜索doModInitFunctions
这里doModInitFunctions加载了所有C++的库
- 在
-
- 在搜索
doInitialization的实现:
- 在搜索
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);
}
然后在然后我们得知doInitialization调用了doModInitFunctions,在搜索的时候,发现这里这里调用doInitialization方法:
此时此刻主体流程形成了一个闭环。
notifySingle是发送消息,registerObjCNotifiers是添加监听
总结
+load方法的流程为:_dyld_start->dyldbootstrap::start->dyld::_main->initializeMainExecutable->ImageLoader::runInitializers->ImageLoader::processInitializers->ImageLoader::recursiveInitialization->notifySingle->sNotifyObjCInit->load_images->+load_objc_init调用流程为:doInitialization->doModInitFunctions->libSystem_initializer->libdispatch_init->_os_object_init->_objc_init
main函数
我们知道_dyld_start是个汇编函数以模拟器为例,然后再看看:
- 分析汇编得到,
rax中又main相关信息,怎么验证呢,我们知道load函数之后会走C++函数,在main中定义一个C++函数:
__attribute__((constructor)) void wushuang_func(){
printf("来了 : %s \n",__func__);
}
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
NSLog(@"1223333");
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
再来查看打印结果:
结果执行顺序为+load > C++ > main,然后在C++函数打个断点,根据汇编点击Step Over往下走走到_dyld_start,过掉dyldbootstrap::start,然后读取寄存器register read:
此刻也就印证了rax存的是main,而汇编最后走的是jmpq *%rax,也就是跳到main函数,此时就走完了从_dyld_start到main函数的全过程。
dyld加载流程
总结
- 本文通过粗略的分析
dyld流程,主要传达一种探索思想,有时候分析问题到一定地方就卡住,常常拿他没办法,这个时候也可以试试反向推导,可能会获得意想不到的效果~