有坑位的小伙伴联系我.微信bgwx7788
这篇文章主要记录
dyld
的启动加载流程,探索main
函数之前,系统底层干了哪些活?
0x00 - main
方法和load
以及__attribute__((constructor))修饰
方法的执行顺序?
新建一个
iOS
demo工程,在ViewController.m
里重写load
方法, 在main.m
写一个__attribute__((constructor))
修饰的c++
函数
// ViewController.m
+ (void)load{
NSLog(@"%s",__func__);
}
// main.m
__attribute__((constructor)) void kcFunc(){
// printf("来了 : %s \n",__func__);
NSLog(@"%s", __func__);
}
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
NSLog(@"main 来了");
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
通过运行结果查看:执行顺序为:load
、kcFunc
、main
,即 先执行load
方法,再执行被__attribute((constructor))
修饰的c/c++
方法,最后执行main
函数。
按照正常思维,main
函数不是程序的入口嘛?为什么最后执行呢?
那么问题来了,
- 为什么
main函数
最后执行? load
方法和被修饰的c++方法
什么时候执行?main
执行前都做了些什么?
围绕这些问题,展开我们本篇文章的探索之路~~~😄
0x01 - 编译过程
分析之前,先普及一下常识,也就是编译过程
在项目编译时,会把项目中编写的源文件
编译成一个一个对应的.o的目标文件
,最后打包成Mach-O
的可执行文件(.o文件的集合)。
编译过程
主要分为以下几步:
源文件
:.m
和cpp
等文件预处理
:把源文件
的带#
的宏替换,比如#include
,#define
等编译
:把预处理
之后输出的结果编译
成object file
目标文件链接
: 把.o
文件集合链接Linker
以及引用到的其它静态库
和标准库文件
,生成可执行文件mac
平台Mach-O
格式,windows
平台exe
格式等。
0x02 - Mach-O
**
WWDC - 2016
**有关于Mach-O
的详细介绍,以及Optimizing start up time
的实操。并且详细介绍了app的启动流程
通过编译出来的产物是Mach-O
文件,这个格式的文件是可以被OSX
和iOS
平台可以理解的,就像exe
可以被Windows
平台识别一样,Mach-O
是Mach Object
文件的缩写。
0x03 - 静态库与动态库
app运行,会依赖底层的库 ,比如一些动态库UIKit
、Foundation
等。还有一些静态库
静态库
格式有
.a .lib framework
静态链接的时候会完整的复制到可执行文件中, 多个程序使用,就会有多份
静态库
,因为参与链接,会复制到可执行文件中, 所以使可执行文件体积增大
,对内存、速度、性能
影响消耗很大
动态库
格式有
.dylib .so .framework
动态库在程序链接时不会复制到执行文件中,程序运行时由系统动态加载到内存,供程序使用,系统只加载一次,多个程序共用,节省内存
Mach-O 文件是编译后的产物,而动态库在运行时才会被链接,并没参与 Mach-O 文件的编译和链接,所以 Mach-O 文件中并没有包含动态库里的符号定义。也就是说,这些符号会显示为“未定义”,但它们的名字和对应的库的路径会被记录下来。运行时通过 dlopen 和 dlsym 导入动态库时,先根据记录的库路径找到对应的库,再通过记录的名字符号找到绑定的地址。
dlopen 会把共享库载入运行进程的地址空间,载入的共享库也会有未定义的符号,这样会触发更多的共享库被载入。dlopen 也可以选择是立刻解析所有引用还是滞后去做。dlopen 打开动态库后返回的是引用的指针,dlsym 的作用就是通过 dlopen 返回的动态库指针和函数符号,得到函数的地址然后使用。
Dylib
是动态库,分为动态链接库
和动态加载库
动态加载库
: 当需要的时候再用dlopen
等通过代码或命令的方式去加载,即程序运行中需要库再去加载库到内存中,类似懒加载。
动态链接库
:在没有被加载到内存的前提下,当可执行文件
被加载,动态库也随着加载到内存中,即随着程序启动而启动。属于程序依赖的启动库使用 dyld 加载动态库,有两种方式:有程序启动加载时绑定和符号第一次被用到时绑定(1.动态链接库 ,2 动态加载库)。为了减少启动时间,大部分动态库使用的都是符号第一次被用到时再绑定的方式。
加载过程开始会修正地址偏移,iOS 会用 ASLR 来做地址偏移避免攻击,确定 Non-Lazy Pointer 地址进行符号地址绑定,加载所有类,最后执行 load 方法和 Clang Attribute 的 constructor 修饰函数。每个函数、全局变量和类都是通过符号的形式定义和使用的,当把目标文件链接成一个 Mach-O 文件时,链接器在目标文件和动态库之间对符号做解析处理。
⚠️ Xcode6
之后支持创建动态库工程,创建的动态库
是为App Extension
之间所需要使用的。
那么动态库
不参与链接, 那么它是怎么启动的?并且在程序
需要动态库的时候是怎么识别?所以要做这些工作,需要一个动态连接器
,也就是著名的dyld
,下面开始分析下dyld
的流程。
0x04 - dyld
加载流程
dyld(the dynamic link editor)
,苹果的动态链接器,Apple OS
的重要组成部分,WWDC - 2017- 413详细介绍了dyld
的历史与特性![]()
![]()
![]()
0x05 - app
启动的起始点
在之前的
demo
中,知道启动最先执行load
方法,所以先从load
方法入手。
通过堆栈信息
查看到程序的起点是dyld
里的_dyld_start
,所幸dyld
是开源的,可以去Apple Open Source dyld这里下载源码查看
打开新下载的源码,在其中搜索_dyld_start
从左侧的搜索结果可以看到,__dyld_start
在汇编中,随便找一个架构可以看其中走向,以arm64
为例,尽管不同架构下有所区别,但通过汇编的注释发现都调用dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
方法,这和上边的堆栈顺序也是符合的,在源码里搜索dyldbootstrap
这个namespace
,在这里找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.
// 这是dyld的引导代码。这个工作通常是由dyld和crt完成,在dyld中, 手动执行这个操作
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>
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
rebaseDyld(dyldsMachHeader);
// 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(argc, argv, envp, apple);
#endif
// 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);
}
系统的启动应用入口是dyldbootstrap::start
,首先通过汇编调用dyld
的dyldbootstrap::start
,其核心主要是调用了dyld::_main
函数,其中macho_header
是Mach-O
的头部信息, Mach-O
可以通过MachOView
查看由四部分组成,Mach-O头部
,Load Command
,section
,Other Data
。
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)
{
......
// 设置运行环境,可执行文件准备工作
......
// load shared cache 加载共享缓存
mapSharedCache();
......
reloadAllImages:
......
// instantiate ImageLoader for main executable 加载可执行文件并生成一个ImageLoader实例对象
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
......
// load any inserted libraries 加载插入的动态库
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}
// link main executable 链接主程序
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
......
// link any inserted libraries 链接所有插入的动态库
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);
}
}
}
......
//弱符号绑定
sMainExecutable->weakBind(gLinkContext);
sMainExecutable->recursiveMakeDataReadOnly(gLinkContext);
......
// run all initializers 执行初始化方法
initializeMainExecutable();
// notify any montoring proccesses that this process is about to enter main()
notifyMonitoringDyldMain();
return result;
}
开始进入dyld::_main
方法实现,整个方法体大概600多行代码,整体主要做了这么几件事情:
设置运行环境,配置环境变量,根据环境变量设置相应的值以及获取当前运行架构
加载共享缓存 -> load share cache
主程序image的初始化mainExecutable
插入动态库 loadInsertedDylib
link主程序
link插入的动态库
weakBind
initializeMainExecutable()
返回 main函数
对其中主要几件事情进行分析:
-
第三步:从
Mach-O
格式可执行文件读取数据实例化成image
,加入到allImages
中
sMainExecutable
代表主程序的变量,通过instantiateFromLoadedImage
初始化image
,然后将image
加入到allImages
中。
// 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
读取Mach-O
可执行文件的信息,比如CPU Type
,Load Commands
个数以及Size of Load Commands
.
// create image for main executable 为主执行文件创建镜像文件
ImageLoader* ImageLoaderMachO::instantiateMainExecutable(const macho_header* mh, uintptr_t slide, const char* path, const LinkContext& context)
{
//dyld::log("ImageLoader=%ld, ImageLoaderMachO=%ld, ImageLoaderMachOClassic=%ld, ImageLoaderMachOCompressed=%ld\n",
// sizeof(ImageLoader), sizeof(ImageLoaderMachO), sizeof(ImageLoaderMachOClassic), sizeof(ImageLoaderMachOCompressed));
bool compressed;
unsigned int segCount;
unsigned int libCount;
const linkedit_data_command* codeSigCmd;
const encryption_info_command* encryptCmd;
//sniffLoadCommands方法用于确定这个mach-O文件是否具有经典或压缩的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
}
再看一下sniffLoadCommands
方法
-
第五步开始链接
sMainExecutable
主image
,再链接插入的动态库// link main executable // 第五步 link主程序 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; } link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1); // 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 // 第六步 link动态库 // 链接插入的动态库,链接主执行文件后执行此操作,这样插入的dylib,(例如libSystem)插入的dylib不会在程序使用的dylib的前面 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 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); } } }
第八步 : initializeMainExecutable
,开始初始化之前加入的iamge
, 主要遍历
各个image
,执行runInitializers
方法
开始初始化链接加入的images
,再initializeMainExecutable()
函数中,主要递归调用runInitializers
🤮先贴一张程序启动调用堆栈图
在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.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
方法,主要是对动态库
的依赖调用``recursiveInitialization`递归初始化
// <rdar://problem/14412057> upward dylib initializers can be run too soon
// To handle dangling dylibs which are upward linked but not downward, all upward linked dylibs
// have their initialization postponed until after the recursion through downward dylibs
// has completed.
// 为了处理向上链接但不向下链接的悬空控件, 所有向上链接的控件都将其初始化推迟到完成通过向下控件的递归完成以后
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.
// 在image列表中所有image调用递归实例化,以建立未初始化的向上依赖关系新列表
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);
}
然后就进入了recursiveInitialization
递归调用
void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize,
InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
recursive_lock lock_info(this_thread);
recursiveSpinLock(lock_info); // 递归加锁
if ( fState < dyld_image_state_dependents_initialized-1 ) {
uint8_t oldState = fState;
// break cycles // 打破递归循环
fState = dyld_image_state_dependents_initialized-1;
try {
// initialize lower level libraries first
for(unsigned int i=0; i < libraryCount(); ++i) {
ImageLoader* dependentImage = libImage(i);
if ( dependentImage != NULL ) {
// don't try to initialize stuff "above" me yet
if ( libIsUpward(i) ) {
uninitUps.imagesAndPaths[uninitUps.count] = { dependentImage, libPath(i) };
uninitUps.count++;
}
else if ( dependentImage->fDepth >= fDepth ) {
// 依赖库的递归初始化
dependentImage->recursiveInitialization(context, this_thread, libPath(i), timingInfo, uninitUps);
}
}
}
// record termination order
if ( this->needsTermination() )
context.terminationRecorder(this);
// let objc know we are about to initialize this image
// 让objc知道要初始化这个镜像了
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);
}
}
catch (const char* msg) {
// this image is not initialized
fState = oldState;
recursiveSpinUnLock();
throw;
}
}
recursiveSpinUnLock(); // 递归解锁
}
会有动态库依赖的递归调用初始化,主要是研究的代码是notifySingle
和doInitialization
,
- 先看
notifySingle
函数
在notifySingle
函数中,重点是(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
,在源码中全局搜索sNotifyObjCInit
,并没有找到实现,直发发现了对其的赋值操作
然后搜索registerObjCNotifiers
方法,看其在哪里调用并对其赋值了。
接着搜索_dyld_objc_notify_register
函数,并没有找到它的实现,找到这么一坨东西,
注意第一句only for use by objc runtime
,意思这玩意只提供给runtime
使用,因为runtime
在libObjc
中,所以在我们的libObjC
中搜索
在_objc_init
中找到调用赋值_dyld_objc_notify_register(&map_images, load_images, unmap_image);
把load_images
赋值给了sNotifyObjCInit
, load_images
会调用所有类的+load
方法。即在objc
这层注册了回调函数,在dyld
调用这些回调函数。
load
函数加载
在load_images
通过call_load_methods
调用+load
方法
进入call_load_methods
方法,其核心通过do-while
循环调用call_class_loads
来到call_class_loads
方法,明显的看到在这里调用了load
方法, 携带俩个默认的参数
+ load
方法总结:
- 是
load_images
函数调用了所有的load
方法,通过堆栈对应了以上的分析。
既然在
_objc_init
调用了dyld
的注册回调函数_dyld_objc_notify_register
,那什么时候调用_objc_init
,因为libObjc
也是一个image
,即libObjc
什么时候初始化,也就会调用了_objc_init
这个函数,形成闭环。
回到dyld
的recursiveInitialization
函数,有一个doInitialization
函数,在这个函数看是否能找到我们想要的。
- 进入
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);
}
主要做了doImageInit
和doModInitFunctions
,
在doImageInit
中, 核心就是for循环调用image的初始化方法
,但是需要注意的是libSystem
库需要第一个初始化,
⚠️libSystem initializer must run first
void ImageLoaderMachO::doImageInit(const LinkContext& context)
{
if ( fHasDashInit ) {
const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds;
const struct load_command* const cmds = (struct load_command*)&fMachOData[sizeof(macho_header)];
const struct load_command* cmd = cmds;
for (uint32_t i = 0; i < cmd_count; ++i) {
switch (cmd->cmd) {
case LC_ROUTINES_COMMAND:
Initializer func = (Initializer)(((struct macho_routines_command*)cmd)->init_address + fSlide);
#if __has_feature(ptrauth_calls)
func = (Initializer)__builtin_ptrauth_sign_unauthenticated((void*)func, ptrauth_key_asia, 0);
#endif
// <rdar://problem/8543820&9228031> verify initializers are in image
if ( ! this->containsAddress(stripPointer((void*)func)) ) {
dyld::throwf("initializer function %p not in mapped image for %s\n", func, this->getPath());
}
if ( ! dyld::gProcessInfo->libSystemInitialized ) {
// <rdar://problem/17973316> libSystem initializer must run first
dyld::throwf("-init function in image (%s) that does not link with libSystem.dylib\n", this->getPath());
}
if ( context.verboseInit )
dyld::log("dyld: calling -init function %p in %s\n", func, this->getPath());
{
dyld3::ScopedTimer(DBG_DYLD_TIMING_STATIC_INITIALIZER, (uint64_t)fMachOData, (uint64_t)func, 0);
func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
}
break;
}
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}
}
}
为什么libSystem
要第一个初始化,是因为在libObjc
库的初始化是在libDispatch
库执行的, 而libDispatch
库是在libSystem
库初始化后执行。
在libObjc
库的初始化函数中_objc_init
/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/
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();
cache_init();
_imp_implementationWithBlock_init();
//调用dyld的注册回调函数
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}
在objc
库中调用dyld
库中的函数_dyld_objc_notify_register
,因为我们是在可编译的objc-781
的环境, 在这里打个断点,看下堆栈流程
粉色横线标记的就是我们上边梳理的流程, 现在从objc_init
开始自顶向下的梳理,流程为
_objc_init
<-- _os_object_init
<--- libdispatch_init
<----libSystem_initializer
所幸这些库都是开源的,在苹果的Apple Open Source 下载libSystem
和libdispatch
这些库来一探究竟
首先打开系统要求第一个加载的libSystem
,搜索libSystem_initializer
// libsyscall_initializer() initializes all of libSystem.dylib
// <rdar://problem/4892197>
__attribute__((constructor))
static void
libSystem_initializer(int argc,
const char* argv[],
const char* envp[],
const char* apple[],
const struct ProgramVars* vars)
{
.....
// TODO: Move __malloc_init before __libc_init after breaking malloc's upward link to Libc
// Note that __malloc_init() will also initialize ASAN when it is present
__malloc_init(apple);
_libSystem_ktrace_init_func(MALLOC);
#if TARGET_OS_OSX
/* <rdar://problem/9664631> */
__keymgr_initializer();
_libSystem_ktrace_init_func(KEYMGR);
#endif
_dyld_initializer();
_libSystem_ktrace_init_func(DYLD);
libdispatch_init();
_libSystem_ktrace_init_func(LIBDISPATCH);
#if !TARGET_OS_DRIVERKIT
_libxpc_initializer();
_libSystem_ktrace_init_func(LIBXPC);
#if CURRENT_VARIANT_asan
setenv("DT_BYPASS_LEAKS_CHECK", "1", 1);
#endif
#endif // !TARGET_OS_DRIVERKIT
// must be initialized after dispatch
_libtrace_init();
_libSystem_ktrace_init_func(LIBTRACE);
....
}
// system library initialisers
extern void mach_init(void); // from libsystem_kernel.dylib
extern void __libplatform_init(void *future_use, const char *envp[], const char *apple[], const struct ProgramVars *vars);
extern void __pthread_init(const struct _libpthread_functions *libpthread_funcs, const char *envp[], const char *apple[], const struct ProgramVars *vars); // from libsystem_pthread.dylib
extern void __malloc_init(const char *apple[]); // from libsystem_malloc.dylib
extern void __keymgr_initializer(void); // from libkeymgr.dylib
extern void _dyld_initializer(void); // from libdyld.dylib
extern void libdispatch_init(void); // from libdispatch.dylib
extern void _libxpc_initializer(void); // from libxpc.dylib
extern void _libsecinit_initializer(void); // from libsecinit.dylib
extern void _libtrace_init(void); // from libsystem_trace.dylib
extern void _container_init(const char *apple[]); // from libsystem_containermanager.dylib
extern void __libdarwin_init(void); // from libsystem_darwin.dylib
放了部分的源码, 这里很明显的可以看到libSystem
初始化的时候, 会初始化其它的库,比如_dyld_initializer();
这个是dyld
库的初始化调用,因为dyld
也是一个动态库,
在启动一个可执行文件的时候,系统内核做完环境的初始化,就把控制权交给
dyld
去执行加载和链接。
__malloc_init(apple);
,这其中就有libdispatch_init();
,因为libdispatch
属于libdispatch
库,所以打开libdispatch
工程,搜索它的初始化代码
void
libdispatch_init(void)
{
...
_dispatch_hw_config_init();
_dispatch_time_init();
_dispatch_vtable_init();
_os_object_init();
_voucher_init();
_dispatch_introspection_init();
}
在这里就可以看到_os_object_init();
调用,因为这个函数也属于libdispatch
库,所以查到
void
_os_object_init(void)
{
_objc_init();
Block_callbacks_RR callbacks = {
sizeof(Block_callbacks_RR),
(void (*)(const void *))&objc_retain,
(void (*)(const void *))&objc_release,
(void (*)(const void *))&_os_objc_destructInstance
};
_Block_use_RR2(&callbacks);
#if DISPATCH_COCOA_COMPAT
const char *v = getenv("OBJC_DEBUG_MISSING_POOLS");
if (v) _os_object_debug_missing_pools = _dispatch_parse_bool(v);
v = getenv("DISPATCH_DEBUG_MISSING_POOLS");
if (v) _os_object_debug_missing_pools = _dispatch_parse_bool(v);
v = getenv("LIBDISPATCH_DEBUG_MISSING_POOLS");
if (v) _os_object_debug_missing_pools = _dispatch_parse_bool(v);
#endif
}
在这里第一行就看到_objc_init()
,所以总结_objc_init()
初始化链是:
_dyld_start
--> dyldbootstrap::start
--> dyld::_main
--> dyld::initializeMainExecutable
---> ImageLoader::runInitializers
---> ImageLoader::processInitializers
--> ImageLoader::recursiveInitialization
---> doInitialization
--->doModInitFunctions
----> libSystem_initializer 属于libSystem.B.dylib
---> libdispatch_init 属于libdispatch.dylib
---> _os_object_init 属于 libdispatch.dylib
--> _objc_init 属于libobjc.A.dylib
- 当
dyld
加载到开始链接mainExecutable
的时候,递归调用recursiveInitialization
函数。 - 这个函数
第一次
运行,会进行libSystem
的初始化,会走到doInitialization
->doModInitFunctions
->libSystem_initializer
libSystem
的初始化,会调用libdispatch_init
,libdispatch_init
会调用_os_object_init
,_os_object_init
会调用_objc_init
- 在
_objc_init
中注册保存了map_images
,load_images
,unmap_images
的函数地址 - 注册完回到
dyld
的recursiveInitialization
递归下一次调用,例如libObjc
,当libObjc
来到recursiveInitialization
调用时,会触发保存的load_images
回调,就调用了load_images
函数
- 还有一个问题是在
doModInitFunctions
方法是怎么调用libSystem
的初始化函数,即libSystem_initializer
,因为在doModInitFunctions
没有看到显示调用。但是在doModInitFunctions
有这么个一个判断
void ImageLoaderMachO::doModInitFunctions(const LinkContext& context){
.....
for (uint32_t i = 0; i < cmd_count; ++i) {
if ( cmd->cmd == LC_SEGMENT_COMMAND ) {
for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) {
const uint8_t type = sect->flags & SECTION_TYPE;
if ( type == S_MOD_INIT_FUNC_POINTERS ) {
Initializer* inits = (Initializer*)(sect->addr + fSlide);;
for (size_t j=0; j < count; ++j) {
Initializer func = inits[j];
if ( ! dyld::gProcessInfo->libSystemInitialized ) {
const char* installPath = getInstallPath();
if ( (installPath == NULL) || (strcmp(installPath, LIBSYSTEM_DYLIB_PATH) != 0) )
dyld::throwf("initializer in image (%s) that does not link with libSystem.dylib\n", this->getPath());
}
bool haveLibSystemHelpersBefore = (dyld::gLibSystemHelpers != NULL);
{
func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
}
bool haveLibSystemHelpersAfter = (dyld::gLibSystemHelpers != NULL);
if ( !haveLibSystemHelpersBefore && haveLibSystemHelpersAfter ) {
// now safe to use malloc() and other calls in libSystem.dylib
dyld::gProcessInfo->libSystemInitialized = true;
}
}
}
}
}
}
}
这个函数,就是找镜像
中的flags
字段匹配S_MOD_INIT_FUNC_POINTERS
的section,这种类型的section
只存储了入口地址。
使用otool
或者MachOView
工具看一下libSystem.dylib
通过烂苹果
打开,可以直观的看到libSystem_initializer
函数地址以及名称,它们在被镜像加载时调用。
对于 libSystem.dylib 而言,该 section 名为__mod_init_func
(一般都是这个名字),存储了一个函数指针,该函数指针恰好对应_libSystem_initializer
符号。当libSystem_initializer
被调用时,dyld 会对gProcessInfo->libSystemInitialized
进行标记,表示 libSystem 已经被初始化。
补充说明
如何确保自定义函数被编译到
__mod_init_func
中呢?一种常见的做法是对函数标记__attribute__((constructor))
。libSystem 的初始化是一个内部行为,dyld 是如何知道它被初始化的呢?libSystem 是一个特殊的 dylib,默认情况下会被所有可执行文件所依赖,dyld 为它单独提供了一个 API:
_dyld_initializer
,当 libSystem 被初始化时,会调用该函数,进而 dyld 内部就知道了 libSystem 被初始化了。
0x06 - main
函数的返回
走到这里,开始进入我们主执行程序
的入口点,从dyld
怎么走到应用的main
函数
dyld
的dyldbootstrap::start
函数走完后,之后的处理通过汇编查看, 在LLDB
通过读取寄存器,rax
的值是我们main
函数的地址了,然后查看dyld
的__dyld_start:
汇编实现。
在执行完dyldbootstrap::start
后, 会call to main()
,说明main
函数是底层写定函数,如果我们改了main
函数名字,会ld报错
0x07 - 收获总结
- 编译阶段有了链接器,所以可以根据模块功能在不同文件里写代码,每个文件都可以独立编译成
Mach-O
文件,编译器可以根据修改范围来减少编译,提高编译速度 - 通过链接机制,也能够明白, 源文件越多,链接器链接
Mach-O
文件所需要绑定遍历的操作就会越多,拖慢编译速度。