静态库与动态库
TARGETS -> Build Phases -> Link Binary With Libraries -> (Add/Add Other…) 中我们可以添加多个系统库或我们自己的库,其中便可包含静态库和动态库。
静态库通常以 .a .lib 或者 .framework 结尾,动态库以 .dylib .tbd .so .framework 结尾。(等等,.framework 可能是静态库也可能是动态库,后面我们会详细分析。)链接时,静态库会被完整的复制到可执行文件中,被多次使用就会有多份冗余拷贝,系统动态库链接时不复制,程序运行时由系统动态加载到内存中,供程序调用,系统只加载一次,多个程序共用,节省内存。
Shift + command + n 创建 new project,在 Framework & library 中,Framework 选项默认是创建 Dynamic Library(动态库),Static Library 选项默认是创建 Static Library(静态库),创建完成的 Mach-O Type 的值告诉了我们他们对应的类型。 当然我们也能直接切换不同的 Mach-0 Type,如 Static Library 和 Dynamic Library 进行切换。而且从 Products 中看到默认情况下动态库是 .framework 后缀,静态库是 .a 后缀,同时还看到动态库是需要进行签名的,而静态库则不需要。
如果我们创建的 framework 是动态库,那么我们直接在工程里使用的时候会报错:Reason: Image Not Found。需要在工程的 General 里的 Frameworks, Libraries, and Embedded Content 添加这个动态库并设置其 Embed 为 Embed & Sign 才能使用。
因为我们创建的这个动态库其实也不能给其他程序使用的,而你的 App Extension 和 APP 之间是需要使用这个动态库的。这个动态库可以 App Extension 和 APP 之间共用一份(App 和 Extension 的 Bundle 是共享的),因此苹果又把这种 Framework 称为 Embedded Framework,而我把这个动态库称为伪动态库。iOS里的动态库和静态库
这里继续依我们的 Test_ipa_Simple 为例,并把上面我们自己构建的动态库 DYLIB
和 静态库 STATICLIB
导入 Test_ipa_Simple 中,直接运行的话会报如下找不到 DYLIB.framework
我们把其 Embed 置为 Embed & Sign 即可正常运行,如果报找不到 STATICLIB 的话,则是在 Build Settings 的 Library Search Paths 和 Header Search Paths 中正确的导入 STATICLIB 及 .h 的路径。(同时为了作为对比,我们在 Build Phases -> Link Binary With Libraries 中导入 WebKit.framework
。)
dyld: Library not loaded: @rpath/DYLIB.framework/DYLIB
Referenced from: /Users/hmc/Library/Developer/CoreSimulator/Devices/4E072E27-E586-4E81-A693-A02A3ED83DEC/data/Containers/Bundle/Application/1208BD23-B788-4BF7-A4CE-49FBA99BA330/Test_ipa_Simple.app/Test_ipa_Simple
Reason: image not found
复制代码
hmc@bogon Test_ipa_Simple.app % file Test_ipa_Simple
Test_ipa_Simple: Mach-O 64-bit executable arm64
复制代码
hmc@bogon DYLIB.framework % file DYLIB
DYLIB: Mach-O 64-bit dynamically linked shared library arm64
复制代码
hmc@bogon Debug-iphoneos % file libSTATICLIB.a
libSTATICLIB.a: current ar archive random library
复制代码
我们创建的动态库和系统的动态库有什么区别呢?
- 我们导入到项目中的我们自己创建的动态库是在我们自己应用的 .app 目录里面,只能自己的 App Extension 和 APP 使用。
- 我们导入到项目中的系统的动态库是在系统目录里面,所有的程序都能使用。
(我们在模拟器上运行的时候用 NSBundle *bundel = [[NSBundle mainBundle] bundlePath];
就能得到 .app 的路径,在第一篇中我们有详细讲解 .ipa 和 .app 目录中的内容,这里不再展开。)
我们自己创建的动态库就在 .app 目录下的 Framework 文件夹里,对 Test_ipa_Simple 进行 Archive,导出并解压 Test_ipa_Simple.ipa,进入 Test_ipa_Simple.app 文件夹:
下面我们可以通过 MachOView 来验证一下 Test_ipa_Simple.app 文件夹中的 Test_ipa_Simple 可执行文件中的动态库(WebKit 和 DYLID)的链接地址。(@rpth 表示的其实就是 .app 下的 Framework 文件夹。)
系统在加载动态库时,会检查 framework 的签名,签名中必须包含 Team Identifier 并且 framework 和 host app 的 Team Identifier 必须一致。可以使用 codesign -dv Test_ipa_Simple.app
和 codesign -dv DYLIB.framework
来进行验证。
- .framework 为什么既是静态库又是动态库 ?
系统的 .framework 是动态库,我们自己建立的.framework 一般都是静态库。但是现在你用 xcode 创建 Framework 的时候默认是动态库(Mach-O Type 默认是 Dynamic Library),一般打包成 SDK 给别人用的话都使用的是静态库,可以修改 Build Settings 的 Mach-O Type 为 Static Library。
- 什么是 framework ?
Framework 是 Cocoa/Cocoa Touch 程序中使用的一种资源打包方式,可以将代码文件、头文件、资源文件、说明文档等集中在一起,方便开发者使用。一般如果是静态 Framework 的话,资源打包进 Framework 是读取不了的。静态 Framework 和 .a 文件都是编译进可执行文件里面的。只有动态 Framework 能在 .app 下面的 Framework 文件夹下看到,并读取 .framework 里的资源文件。
Cocoa/Cocoa Touch 开发框架本身提供了大量的 Framework,比如 Foundation.framework / UIKit.framework / AppKit.framework 等。需要注意的是,这些 framework 无一例外都是动态库。
平时我们用的第三方 SDK 的 framework 都是静态库,真正的动态库是上不了 AppStore 的(iOS 8 之后能上 AppStore,因为有个 App Extension,需要动态库支持)。
我们用 use_frameworks! 生成的 pod 里面,pods 这个 PROJECT 下面会为每一个 pod 生成一个 target,比如有一个 pod 叫做 AFNetworking,那么就会有一个叫 AFNetworking 的 target,最后这个 target 生成的就是 AFNetworking.framework。
关于 use_frameworks!
在使用 CocoaPods 的时候在 Podfile 里加入 use_frameworks! ,那么在编译的时候就会默认生成动态库,我们能看到每个源码 Pod 都会在 Pods 工程下面生成一个对应的动态库 Framework 的 target,我们能在这个 target 的 Build Settings -> Mach-O Type 看到默认设置是 Dynamic Library,也就是会生成一个动态 Framework,我们能在 Products 下面看到每一个 Pod 对应生成的动态库。
这些生成的动态库将链接到主项目给主工程使用,但是我们上面说过动态库需要在主工程 target 的 General -> Frameworks, Libraries, and Embedded Content 添加这个动态库并设置其 Embed 为 Embed & Sign 才能使用,而我们并没有在 Frameworks, Libraries, and Embedded Content 中看到这些动态库。那这是怎么回事呢,其实是 cocoapods 已经执行了脚本把这些动态库嵌入到了 .app 的 Framework 目录下,相当于在 Frameworks, Libraries, and Embedded Content 加入了这些动态库,我们能在主工程 target 的 Build Phase -> [CP]Embed Pods Frameworks 里看到执行的脚本。(”${PODS_ROOT}/Target Support Files/Pods-Test_ipa_Simple/Pods-Test_ipa_Simple-frameworks.sh”)
所以 Pod 默认是生成动态库,然后嵌入到 .app 下面的 Framework 文件夹里。我们去 Pods 工程的 target 里把 Build Settings -> Mach-O Type 设置为 Static Library。那么生成的就是静态库,但是 cocoapods 也会把它嵌入到 .app 的 Framework 目录下,而因为它是静态库,所以会报错:unrecognized selector sent to instanceunrecognized selector sent to instance 。iOS里的动态库和静态库
动态库和静态的知识我们就延伸到这里吧,下面我们继续学习 链接器 相关的内容。
一组函数的执行顺序
// main.m 代码如下:
__attribute__((constructor)) void main_front() {
printf("??? %s 执行 \n", __func__);
}
__attribute__((destructor)) void main_back() {
printf("??? %s 执行 \n", __func__);
}
int main(int argc, char * argv[]) {
NSLog(@"??? %s 执行", __func__);
// NSString * appDelegateClassName;
// @autoreleasepool {
// // Setup code that might create autoreleased objects goes here.
// appDelegateClassName = NSStringFromClass([AppDelegate class]);
// }
// return UIApplicationMain(argc, argv, nil, appDelegateClassName);
return 0;
}
// ViewController.m 代码如下:
@implementation ViewController
+ (void)load {
NSLog(@"??? %s 执行", __func__);
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
}
@end
// 运行后控制台打印如下:
2021-05-07 14:46:45.238651+0800 Test_ipa_Simple[43277:456220] ??? +[ViewController load] 执行
??? main_front 执行
2021-05-07 14:46:45.242218+0800 Test_ipa_Simple[43277:456220] ??? main 执行
??? main_back 执行
复制代码
根据控制台打印,可以看到 load 函数最先执行,然后是 constructor 属性修饰的 main_front 函数执行,然后是 main 函数执行,最后是 destructor 属性修饰的 main_back 函数执行。
__attribute__ 可以设置函数属性(Function Attribute)、变量属性(Variable Attribute)和类型属性(Type Attribute)。__attribute__ 前后都有两个下划线,并且后面会紧跟一对原括弧,括弧里面是相应的 __attribute__ 参数,__attribute__ 语法格式为:__attribute__((attribute-list))
。
若函数被设定为 constructor
属性,则该函数会在 main 函数执行之前被自动的执行。类似的,若函数被设定为 destructor
属性,则该函数会在 main 函数执行之后或者 exit 被调用后被自动的执行。
我们知道 .h、.m 的类在程序运行时先进行预编译,之后进行编译,编译完成后会进行汇编,在汇编结束后会进入一个阶段叫链接(把所有的代码链接到我们的程序中),最后会生成一个可执行文件。
下面我们将了解 App 运行需要加载依赖库,需要加载 .h、.m 文件,那么谁来决定加载这些东西的先后顺序呢?这就是我们今天要说的主角 dyld(链接器)。就是由它来决定加载内容的先后顺序。
app:images(镜像文件)-> dyld:读到内存(也就是加表里),启动主程序 – 进行 link – 一些必要对象的初始化(runtime,libsysteminit,OS_init 的初始化)。
下面我们的目光聚焦在两个点上:链接器本身和链接过程的解读。
Dyld 探索
macOS 的 dyld 程序位置在 /usr/lib/dyld
hmc@bogon Simple % file dyld
dyld: Mach-O universal binary with 3 architectures: [x86_64:Mach-O 64-bit dynamic linker x86_64] [i386:Mach-O dynamic linker i386] [arm64e]
dyld (for architecture x86_64): Mach-O 64-bit dynamic linker x86_64
dyld (for architecture i386): Mach-O dynamic linker i386
dyld (for architecture arm64e): Mach-O 64-bit dynamic linker arm64e
复制代码
可以看到我电脑里面的 dyld 是一个 fat Mach-O 文件,同时集合了三个平台 x86_64、i386、arm64e。
dyld 是英文 the dynamic link editor 的简写,翻译过来就是动态链接器,是苹果操作系统的一个重要的组成部分。在 iOS/macOS 系统中,仅有很少量的进程只需要内核就能完成加载,基本上所有的进程都是动态链接的,所以 Mach-O 镜像文件中会有很多对外部的库和符号的引用,但是这些引用并不能直接用,在启动时还必须要通过这些引用进行内容的填补,这个填补工作就是由动态链接器 dyld 来完成的,也就是符号绑定。系统内核在加载 Mach-O 文件时,都需要用 dyld 链接程序,将程序加载到内存中。
在编写项目时,我们大概最先接触到的可执行的代码是 main 和 load 函数,当我们不重写某个类的 load 函数时,大概会觉得 main 是我们 APP 的入口函数,当我们重写了某个类的 load 函数后,我们又已知的 load 函数是在 main 之前执行的。(上一小节我们也有说过 __attribute__((constructor)) 修饰的 C 函数也会在 main 之前执行)那么从这里可以看出到我们的 APP 真的执行到 main 函数之前其实已经做了一些 APP 的 加载操作,那具体都有哪些呢,我们可以在 load 函数中打断点,然后打印出函数调用堆栈来发现一些端倪。如下图所示:
在模拟器下的截图,其中的 sim 表示当前是在 TARGET_OS_SIMULATOR 环境下:
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x0000000100a769c7 Test_ipa_Simple`+[ViewController load](self=ViewController, _cmd="load") at ViewController.m:17:5
frame #1: 0x00007fff201804e3 libobjc.A.dylib`load_images + 1442
frame #2: 0x0000000108cb5e54 dyld_sim`dyld::notifySingle(dyld_image_states, ImageLoader const*, ImageLoader::InitializerTimingList*) + 425
frame #3: 0x0000000108cc4887 dyld_sim`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 437
frame #4: 0x0000000108cc2bb0 dyld_sim`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 188
frame #5: 0x0000000108cc2c50 dyld_sim`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 82
frame #6: 0x0000000108cb62a9 dyld_sim`dyld::initializeMainExecutable() + 199
frame #7: 0x0000000108cbad50 dyld_sim`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 4431
frame #8: 0x0000000108cb51c7 dyld_sim`start_sim + 122
frame #9: 0x0000000200dea57a dyld`dyld::useSimulatorDyld(int, macho_header const*, char const*, int, char const**, char const**, char const**, unsigned long*, unsigned long*) + 2093
frame #10: 0x0000000200de7df3 dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 1199
frame #11: 0x0000000200de222b dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) + 457
* frame #12: 0x0000000200de2025 dyld`_dyld_start + 37
(lldb)
复制代码
在真机下的截图,相比较与模拟器环境看到是少了 dyld`dyld::useSimulatorDyld 和 dyld_sim`start_sim 调用(切换到模拟器环境),后序的函数调用基本都是一样的,除了运行环境不同外(dyld_sim / dyld)。
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x00000001043f19c0 Test_ipa_Simple`+[ViewController load](self=ViewController, _cmd="load") at ViewController.m:17:5
frame #1: 0x00000001a2bc925c libobjc.A.dylib`load_images + 944
frame #2: 0x00000001046ea21c dyld`dyld::notifySingle(dyld_image_states, ImageLoader const*, ImageLoader::InitializerTimingList*) + 464
frame #3: 0x00000001046fb5e8 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 512
frame #4: 0x00000001046f9878 dyld`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 184
frame #5: 0x00000001046f9940 dyld`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 92
frame #6: 0x00000001046ea6d8 dyld`dyld::initializeMainExecutable() + 216
frame #7: 0x00000001046ef928 dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 5216
frame #8: 0x00000001046e9208 dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) + 396
frame #9: 0x00000001046e9038 dyld`_dyld_start + 56
(lldb)
复制代码
可以看到从 _dyld_start 函数开始直到 +[ViewController load] 函数,中间的函数调用栈都集中在了 dyld/dyld_sim。(最后的 libobjc.A.dylib`load_images 调用,后面我们会详细分析)下面我们可以通过 dyld 的源码 来一一分析上面函数调用堆栈中出现的函数。
_dyld_start
_dyld_start 是汇编函数,这里我们只看 __arm64__ && !TARGET_OS_SIMULATOR 平台下的。
#if __arm64__ && !TARGET_OS_SIMULATOR
.text
.align 2
.globl __dyld_start
__dyld_start:
mov x28, sp // mov 数据传送指令 x28 -> sp
and sp, x28, #~15 // force 16-byte alignment of stack and 逻辑与指令 ((x28 & #~15) & sp) -> sp
mov x0, #0
mov x1, #0
stp x1, x0, [sp, #-16]! // make aligned terminating frame
mov fp, sp // set up fp to point to terminating frame
sub sp, sp, #16 // make room for local variables sub 减法指令
#if __LP64__
ldr x0, [x28] // get app's mh into x0
ldr x1, [x28, #8] // get argc into x1 (kernel passes 32-bit int argc as 64-bits on stack to keep alignment)
add x2, x28, #16 // get argv into x2
#else
ldr w0, [x28] // get app's mh into x0
ldr w1, [x28, #4] // get argc into x1 (kernel passes 32-bit int argc as 64-bits on stack to keep alignment)
add w2, w28, #8 // get argv into x2
#endif
adrp x3,___dso_handle@page
add x3,x3,___dso_handle@pageoff // get dyld's mh in to x4
mov x4,sp // x5 has &startGlue
// ⬇️⬇️⬇️⬇️⬇️ 这里调用 dyldbootstrap::start 是一个入口
// call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
bl __ZN13dyldbootstrap5startEPKN5dyld311MachOLoadedEiPPKcS3_Pm
mov x16,x0 // save entry point address in x16
#if __LP64__
ldr x1, [sp]
#else
ldr w1, [sp]
#endif
cmp x1, #0
b.ne Lnew
// LC_UNIXTHREAD way, clean up stack and jump to result
#if __LP64__
add sp, x28, #8 // restore unaligned stack pointer without app mh
#else
add sp, x28, #4 // restore unaligned stack pointer without app mh
#endif
#if __arm64e__
braaz x16 // jump to the program's entry point
#else
br x16 // jump to the program's entry point
#endif
// LC_MAIN case, set up stack for call to main()
Lnew: mov lr, x1 // simulate return address into _start in libdyld.dylib
#if __LP64__
ldr x0, [x28, #8] // main param1 = argc
add x1, x28, #16 // main param2 = argv
add x2, x1, x0, lsl #3
add x2, x2, #8 // main param3 = &env[0]
mov x3, x2
Lapple: ldr x4, [x3]
add x3, x3, #8
#else
ldr w0, [x28, #4] // main param1 = argc
add x1, x28, #8 // main param2 = argv
add x2, x1, x0, lsl #2
add x2, x2, #4 // main param3 = &env[0]
mov x3, x2
Lapple: ldr w4, [x3]
add x3, x3, #4
#endif
cmp x4, #0
b.ne Lapple // main param4 = apple
#if __arm64e__
braaz x16
#else
br x16
#endif
#endif // __arm64__ && !TARGET_OS_SIMULATOR
复制代码
dyldbootstrap::start
然后看到汇编函数 __dyld_start 内部调用了 dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
函数,即 dyldbootstrap 命名空间中的 start 函数,namespace dyldbootstrap 定义在 dyldInitialization.cpp 中,它的内容超简单,内部就定义了 start 和 rebaseDyld 两个函数,从命名空间的名字中我们已经能猜到一些它的作用:用来进行 dyld 的初始化,将 dyld 引导到可运行状态(Code to bootstrap dyld into a runnable state)。下面我们一起看下其中的 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>
// 发出 kdebug tracepoint 以指示 dyld bootstrap 已启动
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
// 内核将 env 指针设置为刚好超出 agv 数组的末尾
const char** envp = &argv[argc+1];
// kernel sets up apple pointer to be just past end of envp array
// 内核将 apple 指针设置为刚好超出 envp 数组的末尾
const char** apple = envp;
while(*apple != NULL) { ++apple; }
++apple;
// set up random value for stack canary
// 为 stack canary 设置随机值
__guard_setup(apple);
#if DYLD_INITIALIZER_SUPPORT // 前面 DYLD_INITIALIZER_SUPPORT 宏的值是 0,所以这里 #if 内部的内容并不会执行
// run all C++ initializers inside dyld
// 在 dyld 中运行所有 C++ 初始化器
//(这里可以参考 《Hook static initializers》:https://blog.csdn.net/majiakun1/article/details/99413403)
//(帮助我们了解学习 C++ initializers)
runDyldInitializers(argc, argv, envp, apple);
#endif
_subsystem_init(apple);
// now that we are done bootstrapping dyld, call dyld's main
// 现在我们完成了 bootstrapping dyld,调用 dyld 的 main(进入 dyld 的主函数)
uintptr_t appsSlide = appsMachHeader->getSlide();
return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}
复制代码
在 start
函数中 appsMachHeader
和 dyldsMachHeader
两个参数的类型是 const dyld3::MachOLoaded*
(它们两个参数可以理解为我们程序的可执行文件和 dyld 程序的 header 的地址),在 dyld/dyld3/MachOLoaded.h 文件中可看到命名空间 dyld3 中定义的 struct VIS_HIDDEN MachOLoaded : public MachOFile
,即 MachOLoaded
结构体公开继承自 MachOFile
结构体,在 dyld/dyld3/MachOFile.h 文件中可看到命名空间 dyld3 中定义的 struct VIS_HIDDEN MachOFile : mach_header
,即 MachOFile
结构体继承自 mach_header
结构体。
在 return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
中,我们看到 appsMachHeader
被强转为了 macho_header*
,那我们接着则看下 macho_header
的定义。在 dyld/src/ImageLoader.h 中可看到在 __LP64__ 下 macho_header
公开继承自 mach_header_64
其他平台则是继承自 mach_header
(它们的名字仅差一个 0
),mach_header
在前一篇 《iOS APP 启动优化(一):ipa(iPhone application archive) 包和 Mach-O(Mach Object file format) 概述》中我们有详细分析过:
#if __LP64__
struct macho_header : public mach_header_64 {};
struct macho_nlist : public nlist_64 {};
#else
struct macho_header : public mach_header {};
struct macho_nlist : public nlist {};
#endif
复制代码
Mach-O 文件的 Header 部分对应的数据结构定义在 darwin-xnu/EXTERNAL_HEADERS/mach-o/loader.h 中,
struct mach_header
和struct mach_header_64
分别对应 32-bit architectures 和 64-bit architectures。(对于 32/64-bit architectures,32/64 位的 mach header 都出现在 Mach-O 文件的最开头。)
struct mach_header_64 {
uint32_t magic; /* mach magic number identifier */
cpu_type_t cputype; /* cpu specifier */
cpu_subtype_t cpusubtype; /* machine specifier */
uint32_t filetype; /* type of file */
uint32_t ncmds; /* number of load commands */
uint32_t sizeofcmds; /* the size of all the load commands */
uint32_t flags; /* flags */
uint32_t reserved; /* reserved */
};
复制代码
综上,MachOLoaded -> MachOFile -> mach_header
。MachOFile 继承 mach_header 使其拥有 mach_header 结构体中所有的成员变量,然后 MachOFile 定义中则声明了一大组针对 Mach-O 的 Header 的函数,例如获取架构名、CPU 类型等。MachOLoaded 继承自 MachOFile 其定义中则声明了一组加载 Mach-O 的 Header 的函数。
dyld::_main
下面我们接着看 dyld::_main
函数。首先是根据函数调用方式可以看到 _main 函数是属于 dyld 命名空间的,在 dyld/src/dyld2.cpp 中可看到 namespace dyld 的定义,在 dyld2.h 和 dyld2.cpp 中可看到分别进行了 uintptr_t _main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, int argc, const char* argv[], const char* envp[], const char* apple[], uintptr_t* startGlue)
的声明和定义。
首先是 _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 todyld 的入口点。内核加载 dyld 并跳到 __dyld_start 设置一些寄存器并调用此函数。返回目标程序中的 main() 地址,__dyld_start 跳到该地址。
下面我们沿着 _main 函数的定义,来分析 _main 函数,由于该函数定义内部根据不同的平台、不同的架构作了不同的定义和调用,所以函数定义超长,总共有 800 多行,这里只对必要的代码段进行摘录分析,其中最重要的部分则是分析函数返回值 uintptr_t result
在函数内部的赋值情况。
在 dyld/src/dyld2.cpp 的 7117 行和 7127 行看看到如下代码:
...
// find entry point for main executable
result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();
...
// main executable uses LC_UNIXTHREAD, dyld needs to let "start" in program set up for main()
result = (uintptr_t)sMainExecutable->getEntryFromLC_UNIXTHREAD();
...
复制代码
sMainExecutable
是一个全局变量:static ImageLoaderMachO* sMainExecutable = NULL;
,在 dyld/src/dyld2.cpp 的 6861 行可看到对其进行实例化。
// instantiate ImageLoader for main executable
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
gLinkContext.mainExecutable = sMainExecutable;
gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);
复制代码
ImageLoaderMachO
这里我们首先看一下 ImageLoaderMachO
类(ImageLoaderMachO is a subclass of ImageLoader which loads mach-o format files.)的定义,instantiateFromLoadedImage
函数返回一个 ImageLoaderMachO
指针,在 dyld/src/ImageLoaderMachO.h 中可看到 class ImageLoaderMachO : public ImageLoader
的定义,ImageLoaderMachO
类公开继承自 ImageLoader
类。ImageLoader
是一个抽象基类。为了支持加载特定的可执行文件格式,可以创建 ImageLoader
的一个具体子类。对于使用中的每个可执行文件(dynamic shared object),将实例化一个 ImageLoader
。ImageLoader
基类负责将 images 链接在一起,但它对任何特定的文件格式一无所知,主要由其特定子类来实现。如 ImageLoaderMachO
是 ImageLoader
的特定子类,可加载 mach-o 格式的文件。(例如还有 class ImageLoaderMegaDylib : public ImageLoader
ImageLoaderMegaDylib is the concrete subclass of ImageLoader which represents all dylibs in the shared cache.)
instantiateFromLoadedImage
下面我们接着看 instantiateFromLoadedImage
函数实现,根据入参 const macho_header* mh
它内部直接调用 ImageLoaderMachO
的 instantiateMainExecutable
函数进行主可执行文件的实例化(即创建 ImageLoader 对象)。对于程序中需要的依赖库、插入库,会创建一个对应的 image 对象,对这些 image 进行链接,调用各 image 的初始化方法等等,包括对 runtime 的初始化。然后将 image 加载到 imagelist 中,所以我们在 xcode 中使用 image list 命令查看的第一个便是我们的 mach-o,最后返回根据我们的主可执行文件创建的 ImageLoader 对象的地址,即这里 sMainExecutable
就是创建后的主程序。
// 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
// isCompatibleMachO 是检查 mach-o 的 subtype 是否支持当前的 cpu
// if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
// 将 image 加载到 imagelist 中,所以我们在 xcode 中使用 image list 命令查看的第一个便是我们的 mach-o
addImage(image);
return (ImageLoaderMachO*)image;
// }
// throw "main executable not a known format";
}
复制代码
ImageLoaderMachO::instantiateMainExecutable
下面我们看一下 ImageLoaderMachO::instantiateMainExecutable
函数的定义,它的功能便是为 main executable 创建 image。
// 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(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
函数,它也是 ImageLoaderMachO
类的一个函数。
class ImageLoaderMachOCompressed : public ImageLoaderMachO
即 ImageLoaderMachOCompressed
是 ImageLoaderMachO
的子类:ImageLoaderMachOCompressed is the concrete subclass of ImageLoader which loads mach-o files that use the compressed LINKEDIT format。
sniffLoadCommands
下面我们看一下 sniffLoadCommands
函数的定义,此函数过长,我们只看它的部分内容。
// determine if this mach-o file has classic or compressed LINKEDIT and number of segments it has
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)
{
*compressed = false;
*segCount = 0;
*libCount = 0;
*codeSigCmd = NULL;
*encryptCmd = NULL;
const uint32_t cmd_count = mh->ncmds;
const uint32_t sizeofcmds = mh->sizeofcmds;
...
复制代码
确定此 mach-o 文件是 classic 或者 compressed LINKEDIT 且确定 mach-o 可执行文件的 segments 的数量。然后我们可以看到对 mach-o 文件中的 Load Commands 中各个段的确认,如 LC_DYLD_INFO、LC_DYLD_INFO_ONLY、LC_LOAD_DYLIB、LC_SEGMENT_64、LC_CODE_SIGNATURE 等等。
...
switch (cmd->cmd) {
case LC_DYLD_INFO:
case LC_DYLD_INFO_ONLY:
if ( cmd->cmdsize != sizeof(dyld_info_command) )
throw "malformed mach-o image: LC_DYLD_INFO size wrong";
dyldInfoCmd = (struct dyld_info_command*)cmd;
*compressed = true;
break;
case LC_DYLD_CHAINED_FIXUPS:
if ( cmd->cmdsize != sizeof(linkedit_data_command) )
throw "malformed mach-o image: LC_DYLD_CHAINED_FIXUPS size wrong";
chainedFixupsCmd = (struct linkedit_data_command*)cmd;
*compressed = true;
break;
case LC_DYLD_EXPORTS_TRIE:
if ( cmd->cmdsize != sizeof(linkedit_data_command) )
throw "malformed mach-o image: LC_DYLD_EXPORTS_TRIE size wrong";
exportsTrieCmd = (struct linkedit_data_command*)cmd;
break;
case LC_SEGMENT_COMMAND:
segCmd = (struct macho_segment_command*)cmd;
...
复制代码
sniffLoadCommands(mh, path, false, &compressed, &segCount, &libCount, context, &codeSigCmd, &encryptCmd);
函数调用我们就看到这里,然后下面的 return ImageLoaderMachOCompressed::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
和 return ImageLoaderMachOClassic::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
则都是调用 ImageLoaderMachO 的构造函数,创建 ImageLoaderMachO 对象。
sMainExecutable
创建完毕,我们接着分析 dyld::_main
函数。
getHostInfo
调用 getHostInfo(mainExecutableMH, mainExecutableSlide); 函数来获取 Mach-O 头部信息中的当前运行架构信息,仅是为了给 sHostCPU 和 sHostCPUsubtype 两个全局变量赋值。getHostInfo 函数虽然有两个参数 mainExecutableMH 和 mainExecutableSlide 但是实际都只是为了在 x86_64 && !TARGET_OS_SIMULATOR 下使用的。
static void getHostInfo(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide)
{
#if CPU_SUBTYPES_SUPPORTED
#if __ARM_ARCH_7K__
sHostCPU = CPU_TYPE_ARM;
sHostCPUsubtype = CPU_SUBTYPE_ARM_V7K;
#elif __ARM_ARCH_7A__
sHostCPU = CPU_TYPE_ARM;
sHostCPUsubtype = CPU_SUBTYPE_ARM_V7;
#elif __ARM_ARCH_6K__
sHostCPU = CPU_TYPE_ARM;
sHostCPUsubtype = CPU_SUBTYPE_ARM_V6;
#elif __ARM_ARCH_7F__
sHostCPU = CPU_TYPE_ARM;
sHostCPUsubtype = CPU_SUBTYPE_ARM_V7F;
#elif __ARM_ARCH_7S__
sHostCPU = CPU_TYPE_ARM;
sHostCPUsubtype = CPU_SUBTYPE_ARM_V7S;
#elif __ARM64_ARCH_8_32__
sHostCPU = CPU_TYPE_ARM64_32;
sHostCPUsubtype = CPU_SUBTYPE_ARM64_32_V8;
#elif __arm64e__
sHostCPU = CPU_TYPE_ARM64;
sHostCPUsubtype = CPU_SUBTYPE_ARM64E;
#elif __arm64__
sHostCPU = CPU_TYPE_ARM64;
sHostCPUsubtype = CPU_SUBTYPE_ARM64_V8;
#else
struct host_basic_info info;
mach_msg_type_number_t count = HOST_BASIC_INFO_COUNT;
mach_port_t hostPort = mach_host_self();
kern_return_t result = host_info(hostPort, HOST_BASIC_INFO, (host_info_t)&info, &count);
if ( result != KERN_SUCCESS )
throw "host_info() failed";
sHostCPU = info.cpu_type;
sHostCPUsubtype = info.cpu_subtype;
mach_port_deallocate(mach_task_self(), hostPort);
#if __x86_64__
// host_info returns CPU_TYPE_I386 even for x86_64. Override that here so that
// we don't need to mask the cpu type later.
sHostCPU = CPU_TYPE_X86_64;
#if !TARGET_OS_SIMULATOR
sHaswell = (sHostCPUsubtype == CPU_SUBTYPE_X86_64_H);
// <rdar://problem/18528074> x86_64h: Fall back to the x86_64 slice if an app requires GC.
if ( sHaswell ) {
if ( isGCProgram(mainExecutableMH, mainExecutableSlide) ) {
// When running a GC program on a haswell machine, don't use and 'h slices
sHostCPUsubtype = CPU_SUBTYPE_X86_64_ALL;
sHaswell = false;
gLinkContext.sharedRegionMode = ImageLoader::kDontUseSharedRegion;
}
}
#endif
#endif
#endif
#endif
}
复制代码
forEachSupportedPlatform
在此块区域中我们看到了我们的老朋友 block 在 C/C++ 函数中的使用。
判断 mainExecutableMH
支持的平台以及当前的版本信息等。
// Set the platform ID in the all image infos so debuggers can tell the process type
// FIXME: This can all be removed once we make the kernel handle it in rdar://43369446
// The host may not have the platform field in its struct, but there's space for it in the padding, so always set it
{
__block bool platformFound = false;
((dyld3::MachOFile*)mainExecutableMH)->forEachSupportedPlatform(^(dyld3::Platform platform, uint32_t minOS, uint32_t sdk) {
if (platformFound) {
halt("MH_EXECUTE binaries may only specify one platform");
}
gProcessInfo->platform = (uint32_t)platform;
platformFound = true;
});
if (gProcessInfo->platform == (uint32_t)dyld3::Platform::unknown) {
// There were no platforms found in the binary. This may occur on macOS for alternate toolchains and old binaries.
// It should never occur on any of our embedded platforms.
#if TARGET_OS_OSX
gProcessInfo->platform = (uint32_t)dyld3::Platform::macOS;
#else
halt("MH_EXECUTE binaries must specify a minimum supported OS version");
#endif
}
}
...
复制代码
setContext
setContext
是一个静态全局函数,主要为 ImageLoader::LinkContext gLinkContext;
这个全局变量的各项属性以及函数指针赋值。设置 crash 以及 log 地址,设置上下文信息。
CRSetCrashLogMessage("dyld: launch started");
setContext(mainExecutableMH, argc, argv, envp, apple);
复制代码
configureProcessRestrictions
设置环境变量,envp 就是 _main 函数的参数,它是所有环境变量的数组,就是将环境变量插入进去。
configureProcessRestrictions(mainExecutableMH, envp);
复制代码
checkSharedRegionDisable
对共享缓存进行处理。
// load shared cache
checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
复制代码
instantiateFromLoadedImage
主程序的初始化。
// instantiate ImageLoader for main executable
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
gLinkContext.mainExecutable = sMainExecutable;
gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);
复制代码
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);
}
复制代码
link
link 主程序。
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
sMainExecutable->setNeverUnloadRecursive();
复制代码
link 我们所有的 image(通过上面两个可以知道,必须先 link 主程序,然后在 link 所有的 image)
// 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);
}
}
}
复制代码
weakBind
绑定弱符号。
// <rdar://problem/12186933> do weak binding only after all inserted images linked
sMainExecutable->weakBind(gLinkContext);
复制代码
initializeMainExecutable
执行所有的初始化方法。
// run all initializers
initializeMainExecutable();
复制代码
notifyMonitoringDyldMain
查找 main 函数入口
// notify any montoring proccesses that this process is about to enter main()
notifyMonitoringDyldMain();
复制代码
设置运行环境 -> 加载共享缓存 -> 实例化主程序 -> 插入加载动态库 -> 连接主程序 -> 链接插入的动态库 -> 执行弱符号绑定 -> 执行初始化方法 -> 查找入口并返回()
initializeMainExecutable
上面我们分析了 main 的总体流程,其中 initializeMainExecutable
函数进行了所有的 initializers,下面我们详细学习一下 initializeMainExecutable
函数。
void initializeMainExecutable()
{
// record that we've reached this step(记录,我们已经达到了这一步)
gLinkContext.startedInitializingMainExecutable = true;
// run initialzers for any inserted dylibs(为任何插入的 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]);
}
复制代码
gLinkContext
是一个 ImageLoader::LinkContext gLinkContext;
类型的全局变量,LinkContext 是在 class ImageLoader 中定义的一个结构体,其中定义了很多函数指针和成员变量,来记录和处理 Link 的上下文。其中 bool startedInitializingMainExecutable;
则是用来记录标记 MainExecutable 开始进行 Initializing 了,这里是直接把它的值置为 true。
InitializerTimingList
也是在 class ImageLoader 中定义的一个挺简单的结构体。用来记录 Initializer 所花费的时间。
struct InitializerTimingList
{
uintptr_t count;
struct {
const char* shortName;
uint64_t initTime;
} images[1];
void addTime(const char* name, uint64_t time);
};
void ImageLoader::InitializerTimingList::addTime(const char* name, uint64_t time)
{
for (int i=0; i < count; ++i) {
if ( strcmp(images[i].shortName, name) == 0 ) {
images[i].initTime += time;
return;
}
}
images[count].initTime = time;
images[count].shortName = name;
++count;
}
复制代码
下面是 runInitializers
函数,同样是 class ImageLoader 中定义的一个函数。
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); // ⬅️ deallocate 任务
uint64_t t2 = mach_absolute_time(); // ⬅️ 计时结束
fgTotalInitTime += (t2 - t1);
}
复制代码
在 runInitializers 中我们看到了两个老面孔,在学习 GCD 源码时见过的 mach_absolute_time
和 mach_thread_self
一个用来统计初始化时间,一个用来记录当前线程。
UninitedUpwards
也是 class ImageLoader 中定义的一个超简单的结构体,其中的成员变量 std::pair<ImageLoader*, const char*> imagesAndPaths[1];
一个值记录 ImageLoader 的地址,另一个值记录该 ImageLoader 的路径。
struct UninitedUpwards
{
uintptr_t count;
std::pair<ImageLoader*, const char*> imagesAndPaths[1];
};
复制代码
可看到 processInitializers(context, thisThread, timingInfo, up);
是其中最重要的函数,下面来一起看看。
processInitializers
处理初始化过程,看到 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.
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);
}
复制代码
images.imagesAndPaths[i].first
是 ImageLoader 指针(ImageLoader *
),即调用 class ImageLoader 的 recursiveInitialization
函数,下面我们看一下 recursiveInitialization
函数的定义。
recursiveInitialization
recursiveInitialization
函数定义较长,其中比较重要的是 // initialize lower level libraries first 下的 for 循环,进行循环判断是否都加载过,没有的话就再执行 dependentImage->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
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();
}
复制代码
然后再往下是 // let objc know we are about to initialize this image 部分的的内容,它们才是 recursiveInitialization
函数的核心,我们首先看一下 context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
函数的调用,首先这里我们一直往上追溯的话可发现 context 参数即在 initializeMainExecutable
函数中传入的 ImageLoader::LinkContext gLinkContext;
这个全局变量,然后在 dyld/src/dyld2.cpp 文件中的 static void setContext(const macho_header* mainExecutableMH, int argc, const char* argv[], const char* envp[], const char* apple[])
静态全局函数中,gLinkContext.notifySingle = ¬ifySingle;
即 recursiveInitialization
函数中调用的 context.notifySingle
即 gLinkContext.notifySingle
即 dyld/src/dyld2.cpp 中的 ¬ifySingle
函数。
然后我们直接在 dyld2.cpp 中搜索 notifySingle
函数, 它是一个静态全局函数,由于该函数实现过长,那么我们只看其中的核心:
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
复制代码
sNotifyObjCInit 是一个静态全局变量,是一个名字是 _dyld_objc_notify_init
的函数指针,_dyld_objc_notify_init
是一个返回值为 void 两个参数分别是 const char * 和 const struct mach_header * 的函数指针:
typedef void (*_dyld_objc_notify_init)(const char* path, const struct mach_header* mh);
static _dyld_objc_notify_init sNotifyObjCInit;
复制代码
然后同样在 dyld2.cpp 文件中搜索,可看到在 registerObjCNotifiers
函数中,有对 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());
}
}
}
复制代码
typedef void (*_dyld_objc_notify_mapped)(unsigned count, const char* const paths[], const struct mach_header* const mh[]);
typedef void (*_dyld_objc_notify_init)(const char* path, const struct mach_header* mh);
typedef void (*_dyld_objc_notify_unmapped)(const char* path, const struct mach_header* mh);
复制代码
我们看到 registerObjCNotifiers
函数的 _dyld_objc_notify_init init
参数会直接赋值给 sNotifyObjCInit
,并在下面的 for 循环中进行调用,那么什么时候调用 registerObjCNotifiers
函数呢?_dyld_objc_notify_init init
的实参又是什么呢?我们全局搜索 registerObjCNotifiers
函数。(其实看到这里,看到 registerObjCNotifiers 函数的形参我们可能会有一点印象了,之前看 objc 的源码时的 _objc_init 函数中涉及到 image 部分。)
在 dyld/src/dyldAPIs.cpp 中,_dyld_objc_notify_register
函数内部调用了 registerObjCNotifiers
函数(属于 namespace dyld)。
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
函数在 objc4 源码中也有调用过,并且就在 _objc_init
函数中。下面我们先看一下 _dyld_objc_notify_register
函数的声明。
//
// Note: only for use by objc runtime
// Register handlers to be called when objc images are mapped, unmapped, and initialized.
// Dyld will call back the "mapped" function with an array of images that contain an objc-image-info section.
// Those images that are dylibs will have the ref-counts automatically bumped, so objc will no longer need to call dlopen() on them to keep them from being unloaded.
// During the call to _dyld_objc_notify_register(), dyld will call the "mapped" function with already loaded objc images.
// During any later dlopen() call, dyld will also call the "mapped" function.
// Dyld will call the "init" function when dyld would be called initializers in that image.
// This is when objc calls any +load methods in that image.
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped);
复制代码
_dyld_objc_notify_register
函数仅供 objc runtime 使用。注册在 mapped、unmapped 和 initialized objc images 时要调用的处理程序。Dyld 将使用包含 objc-image-info section 的 images 数组回调 mapped 函数。那些 dylib 的 images 将自动增加引用计数,因此 objc 将不再需要对它们调用 dlopen() 以防止它们被卸载。在调用 _dyld_objc_notify_register() 期间,dyld 将使用已加载的 objc images 调用 mapped 函数。在以后的任何 dlopen() 调用中,dyld 还将调用 mapped 函数。当 dyld 在该 image 中调用 initializers 时,Dyld 将调用 init 函数。这是当 objc 调用 image 中的任何 +load 方法时。
下面我们在 objc4-781 中搜一下 _dyld_objc_notify_register
函数,在 _objc_init
中我们看到了它的身影。
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_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}
复制代码
那么追了这么久,上面的 init 形数对应的实参便是 load_images
函数。总结下:_dyld_objc_notify_register
相当于一个回调函数,也就是该方法的调用是在 _objc_init
调用的方法里调用,类推就是调用初始化方法才回去调用 _dyld_objc_notify_register
。下面我们回到前面的 bool hasInitializers = this->doInitialization(context);
调用。
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(context);
调用。
void ImageLoaderMachO::doImageInit(const LinkContext& context)
{
if ( fHasDashInit ) {
const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds; // ⬅️ load command 的数量
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);
}
}
}
复制代码
看看到其中就是遍历 load command 找到其中 LC_ROUTINES_COMMAND 的 load command 然后通过内存地址偏移得到要执行的方法并执行。(Initializer func = (Initializer)(((struct macho_routines_command*)cmd)->init_address + fSlide);
) 其中的 if ( ! dyld::gProcessInfo->libSystemInitialized )
是判断 libSystem 必须先初始化,否则就直接抛错。总结:上面我们研究了初始化的过程,最后是由内存地址不断平移拿到初始化方法进行调用。
这样我们最开始的 bt 指令的截图中出现的函数就都浏览一遍了:_dyld_start
-> dyldbootstrap::start
-> dyld::_main
-> dyld::initializeMainExecutable
-> ImageLoader::runInitializers
-> ImageLoader::processInitializers
-> ImageLoader::recursiveInitialization
-> dyld::notifySingle
-> libobjc.A.dylib \
load_images
。
参考链接
参考链接:?