1. 编译与连接的过程
<<程序员的自我修养>>中将了程序的编译与链接的过程的详细内容, 这里简单总结一下:
源文件->预处理->编译->汇编->链接
复制代码
其中主要的步骤功能如下:
- 源文件是我们常见的`c/c++/oc等源文件
- 预处理: 替换宏, 预编译指令等等, 生成
.i
- 编译: 转化成汇编文件
- 汇编: 将汇编文件转化成二进制的机器码, 也就是 目标文件.o
- 静态链接: 将
.o
中合并, 符号解析与重定位, 并生成可执行文件.(依赖的外部动态库符号在启动以后, 由动态链接器进行动态链接)
链接的细分:
- 静态链接: 编译构建二进制时进行, 主要是符号解析, 重定位, 合并段, 创建虚拟内存空间等过程.
- 动态链接: 运行时, 依赖动态链接器dyld, 发生启动阶段, 在pre main时, 一般依赖操作系统.
2. 静态库与动态库
<<程序员的自我修养>>将目标文件分成有三种形式:
- 可执行目标文件. 即我们通常所认识的, 可直接运行的二进制文件。
- 可重定位目标文件. 包含了二进制的代码和数据,可以与其他可重定位目标文件合并,并创建一个可执行目标文件.
- 共享目标文件. 它是一种在加载或者运行时进行链接的特殊可重定位目标文件.
我们常见的静态库就是其中的可重定位目标文件
, 动态库就是共享目标文件
. 一句话说明静态库和动态库:
- 静态库: 编译的中间体,
.o
的集合或者ar
压缩包 - 动态库: 编译完全体, 与二进制可执行文件格式几乎一致(需要在运行时进行链接)
其他用的与动态库/静态库相关的内容:
- framework: iOS中的framework是一种包装格式, 具体是动态库or静态库,需要查看内部包装的二进制的格式
- xcframework: 多个库的外壳, 并包含一个配置索引表, xcode会识别根据APP编译的架构选择具体的framework
- Fat 二进制库: 多CPU架构的Mach-O合并的成的一个Mach-O文件.
3. dyld的执行过程
iOS APP启动
以下内容参考opensource.apple.com/tarballs/dy… 中的852.2
的dyld源码. 方法入口是:__dyld_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 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);
// 如果dyld 需要 rebase, 传入 dyld的 Mach-O Header, 进行Rebase
// 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
_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);
}
复制代码
简单来说就是:
- 调用
__dyld_start
dyldbootstrap::start
方法, 最关键的入参是const dyld3::MachOLoaded* appsMachHeader
, 也就是App主二进制的Mach-O Header
- 然后就交给
dyld::_main
正式处理
该方法的逻辑比较复杂基本是pre-main的核心流程:
// Entry point for dyld.
// The kernel loads dyld and jumps to __dyld_start which
// sets up some registers and call this function.
//
// Returns address of main() in target program which __dyld_start jumps to
/*
从注释中能看到, 方法最终会返回 主二进制可执行文件的入口函数 main 函数
因此这个函数需要做前期的dyld的工作:
1. 环境变量的配置
2. 共享缓存加载, dyld3对启动闭包的编译/加载
3. 主二进制Mach-O的加载
4. 依赖的动态库加载
5. 链接主程序 -- rebase & rebind
6. 链接其他的插入的动态库 -- rebase & rebind
7. dyld的Mach中的初始化方法与, +load方法
8. 查找并返回, 主程序的入口函数main
*/
// ImageLoaderMachO is a subclass of ImageLoader which loads mach-o format files.
typedef ImageLoaderMachO* __ptrauth_dyld_address_auth MainExecutablePointerType;
static MainExecutablePointerType sMainExecutable = NULL;
uintptr_t
_main(const macho_header* mainExecutableMH,
uintptr_t mainExecutableSlide,
int argc, const char* argv[],
const char* envp[],
const char* apple[],
uintptr_t* startGlue)
{
//1. 环境变量配置
// 前面都是操作 mainExecutableMH : 主二进制 Mach-O Header
getHostInfo...
//2. 共享缓存, dyld3对启动闭包的编译/加载
sClosureMode & ClosureBuild
// 3.主二进制Mach-O的加载 -- 从Mach-O Header --> Mach-O的加载
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
// 4. 依赖的动态库加载 -- 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);
}
// 5. 链接主程序 -- rebase & rebind
// 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);
}
// 6. 链接其他的插入的动态库
// 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);
}
}
}
7. 主程序初始化方法
// run all initializers
initializeMainExecutable();
// notify any montoring proccesses that this process is about to enter main()
notifyMonitoringDyldMain();
8. 查找并返回, 主程序的入口函数main
}
复制代码
从注释中能看到, 方法最终会返回 主二进制可执行文件的入口函数 main 函数
因此这个函数需要做前期的dyld的工作:
- 环境变量的配置
- 共享缓存加载, dyld3对启动闭包的编译/加载 –> buildClosureCachePath
- 主二进制Mach-O的加载 –> instantiateFromLoadedImage
- 依赖的动态库加载(在环境变量中) –> loadInsertedDylib
- 链接主程序 — rebase & rebind –> rebase() & link()
- 链接其他的插入的动态库 — rebase & rebind
- 主程序的初始化方法(+load方法也会在这里执行)– > initializeMainExecutable
- 查找并返回, 主程序的入口函数main
3. 主程序初始化 — initializeMainExecutable
主程序初始化的工作:
// 初始化主程序
void initializeMainExecutable() {
// record that we've reached this step
gLinkContext.startedInitializingMainExecutable = true;
...
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]);
...
}
// runInitializers() is normally called in link()
// but the main executable must run crt code before initializers
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);
}
void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize, InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
// let objc know we are about to initialize this image
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 --> 初始化这个 image!!! 调用objc_init!!!
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);
}
复制代码
基本的调用流程如下:
initializeMainExecutable
ImageLoader::runInitializers
ImageLoader::processInitializers
ImageLoader::recursiveInitialization
ImageLoaderMachO::doInitialization
–> Mach-O的init函数, C++ static 对象初始化执行context.notifySingle(dyld_image_state_initialized, this, NULL);
–> OC的+load方法执行
ImageLoaderMachO::doInitialization 线条
另外一条线_objc_init()
的调用链条:
bool ImageLoaderMachO::doInitialization(const LinkContext& context) {
// mach-o has -init and static initializers
// mach-o 如果有init段, 或者c++ 静态初始化方法在这里执行!!!
doImageInit(context);
doModInitFunctions(context);
return (fHasDashInit || fHasInitializers);
}
复制代码
ImageLoaderMachO::doModInitFunctions
中会调用libSystem_initializer
方法, 后续的流程:
libSystem_initializer -> libdispatch_init -> _os_object_init() -> _objc_init()
libSystem_initializer 可以通过符号断点 _objc_init 获取调用栈
因此, 会进行OC runtime的初始化, 例如注册OC类, Category, Selector唯一性检查等等, 调用完整流程如下:
_dyld_start -> dyldbootstrap::start -> dyld::_main -> dyld::initializeMainExecutable
-> ImageLoader::runInitializers -> ImageLoader::processInitializers -> ImageLoader::recursiveInitialization -> ImageLoader::doInitializers
-> libSystem.B.dylib(libSystem_initializer)
-> _os_object_init(libdiapatch.dylib)
-> _objc_init(libobjc.A.dylib)
复制代码
load 调用线
在初始, 调用回调方法, 核心的回调方法是sNotifyObjCInit
, 这个方法是在sNotifyObjCInit
中初始化, 实际是在_objc_init(void)
方法中注册初始化的!!!
static void notifySingle(dyld_image_states state, const ImageLoader* image, ImageLoader::InitializerTimingList* timingInfo)
{
...
if ( (state == dyld_image_state_dependents_initialized) && (sNotifyObjCInit != NULL) && image->notifyObjC() ) {
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
}
...
}
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;
...
}
复制代码
也就是_objc_init()
方法注册了一个全局的sNotifyObjCInit
方法, 具体的方法名字是load_images
, 该方法会在initializeMainExecutable
中回调通知!!!
因此+load
方法的调用链条:
_dyld_start -> dyldbootstrap::start -> dyld::_main -> dyld::initializeMainExecutable
-> ImageLoader::runInitializers -> ImageLoader::processInitializers -> ImageLoader::recursiveInitialization -> dyld::notifySignle(回调) --> sNotifyObjCInit --> load_images(libobjc.A.dylib)
复制代码
4. 通俗理解以上的过程
如果在Xcode中配置环境变量DYLD_PRINT_STATISTICS = YES
打印启动耗时:
Total pre-main time: 74.37 milliseconds (100.0%)
dylib loading time: 41.05 milliseconds (55.2%)
rebase/binding time: 8.10 milliseconds (10.9%)
ObjC setup time: 9.87 milliseconds (13.2%)
initializer time: 15.23 milliseconds (20.4%)
slowest intializers :
libSystem.B.dylib : 6.58 milliseconds (8.8%)
libBacktraceRecording.dylib : 6.27 milliseconds (8.4%)
复制代码
dylib load
简单来说就是主二进制加载, 以及依赖的动态库加载, 这个过程中主要有以下详细过程:
- 系统内核读取主二进制可执行文件的Mach-O Header, 分析依赖动态库
- 找到动态库的Mach-O, 然后校验文件
- mmap内存映射到虚拟内存
rebase/rebind
由于iOS中使用 ASLR方式, 主二进制可执行文件与动态库映射到内存以后的地址是随机的. 因此需要修复Mach-O中程序与数据的地址.
rebase: 修复指向当前Mach-O内部的符号的地址
rebind: 指向当前Mach-O外部的符号的地址
这两部过程中由于, Text端被加密过, 因此系统内核读取时,需要加解密验证, 有IO耗时.
Objc Setup
也就是objc_init()
主要工作在在runtime中注册oc相关内容,并检查:
- 注册 OC对象
- 注册OC的Category
- 检查Selector的唯一性等
这个过程在dyld3中的启动闭包对很多OC对象, Category的注册有大量Cache优化
initializer 初始化
前面内容都是在静态(fix-up), 修改_DATA segment
中的内容, 这里开始动态调整, 开始往虚拟内存的堆和栈中写入内容:
- 执行objc中的
+load
函数 - 执行使用gcc的编译器指令:
attribute((constructor)) xxx
函数 - 执行C++的
static对象的创建
启动 Pipeline
这里贴一个抖音的全链路的总结:
- 点击图标,创建进程
- mmap 主二进制,找到 dyld 的路径
- mmap dyld,把入口地址设为
_dyld_start
- 重启手机/更新/下载 App 的第一次启动,会创建启动闭包
- 把没有加载的动态库 mmap 进来,动态库的数量会影响这个阶段
- 对每个二进制做 bind 和 rebase,主要耗时在 Page In,影响 Page In 数量的是 objc 的元数据
- 初始化 objc 的 runtime,由于闭包已经初始化了大部分,这里只会注册 sel 和装载 category
- +load 和静态初始化被调用,除了方法本身耗时,这里还会引起大量 Page In
- 初始化
UIApplication
,启动 Main Runloop - 执行
will/didFinishLaunch
,这里主要是业务代码耗时 - Layout,
viewDidLoad
和Layoutsubviews
会在这里调用,Autolayout
太多会影响这部分时间 - Display,
drawRect
会调用 - Prepare,图片解码发生在这一步
- Commit,首帧渲染数据打包发给 RenderServer,启动结束