前言
写代码时一般都离不开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
:编译器生成中间代码ir
backend, {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
流程,主要传达一种探索思想,有时候分析问题到一定地方就卡住,常常拿他没办法,这个时候也可以试试反向推导,可能会获得意想不到的效果~