dyld动态链接

生成可执行文件后,就是在启动时进行动态链接了,进行符号和地址的绑定。首先会加载所依赖的dylibs,修正地址偏移,因为iOS会用 ASLR 来做地址偏移避免攻击,确定Non-Lazy Pointer地址进行符号地址绑定,加载所有类,最后执行load方法和clang attributeconstructor修饰函数。

每个函数、全局变量和类都是通过符号的形式来定义和使用的,当把目标文件链接为一个执行文件时,链接器在目标文件和动态库之间对符号做解析处理

查看一个可链接文件的符号表
xcrun nm -nm SayHi.o

                 (undefined) external _OBJC_CLASS_$_Foo
                 (undefined) external _objc_autoreleasePoolPop
                 (undefined) external _objc_autoreleasePoolPush
                 (undefined) external _objc_msgSend
0000000000000000 (__TEXT,__text) external _main
  • OBJC_CLASS$_Foo表示FooOC符号
  • (undefined) external表示未实现非私有,如果是私有就是non-external
  • external _main 表示main()函数,处理0地址,将要到 TEXT,text section
xcrun nm -nm Foo.o
                 (undefined) external _NSLog
                 (undefined) external _OBJC_CLASS_$_NSObject
                 (undefined) external _OBJC_METACLASS_$_NSObject
                 (undefined) external ___CFConstantStringClassReference
                 (undefined) external __objc_empty_cache
0000000000000000 (__TEXT,__text) non-external -[Foo say]
0000000000000060 (__DATA,__objc_const) non-external l_OBJC_METACLASS_RO_$_Foo
00000000000000a8 (__DATA,__objc_const) non-external l_OBJC_$_INSTANCE_METHODS_Foo
00000000000000c8 (__DATA,__objc_const) non-external l_OBJC_CLASS_RO_$_Foo
0000000000000110 (__DATA,__objc_data) external _OBJC_METACLASS_$_Foo
0000000000000138 (__DATA,__objc_data) external _OBJC_CLASS_$_Foo

undefine符号表示该文件类未实现的,所以在目标文件和Function framework动态库做链接处理时,链接器尝试解析所有的undefined符号

链接器通过动态库解析成符号会记录是通过哪个动态库解析的,路径也会一起记录。对比下a.out符号表,看看可执行文件是怎么解析符号的

xcrun nm -nm a.out
                 (undefined) external _NSLog (from Foundation)
                 (undefined) external _OBJC_CLASS_$_NSObject (from CoreFoundation)
                 (undefined) external _OBJC_METACLASS_$_NSObject (from CoreFoundation)
                 (undefined) external ___CFConstantStringClassReference (from CoreFoundation)
                 (undefined) external __objc_empty_cache (from libobjc)
                 (undefined) external _objc_autoreleasePoolPop (from libobjc)
                 (undefined) external _objc_autoreleasePoolPush (from libobjc)
                 (undefined) external _objc_msgSend (from libobjc)
                 (undefined) external dyld_stub_binder (from libSystem)
0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
0000000100000e90 (__TEXT,__text) external _main
0000000100000f10 (__TEXT,__text) non-external -[Foo say]
0000000100001130 (__DATA,__objc_data) external _OBJC_METACLASS_$_Foo
0000000100001158 (__DATA,__objc_data) external _OBJC_CLASS_$_Foo

undefined 符号 有了更多信息,可以知道在哪个库能够找到
通过otool可以找到所需要的库在哪

xcrun otool -L a.out
a.out:
    /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation (compatibility version 300.0.0, current version 1349.25.0)
    /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1238.0.0)
    /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 1348.28.0)
    /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)

dylib这种格式表示是动态链接的,即动态库,编译时不会被编译到执行文件中,在程序执行时才link,这样就不用算到包的大小中,而且也能够不更新执行程序就能更新库

打印什么库被加载了

(export DYLD_PRINT_LIBRARIES=; ./a.out )
dyld: loaded: /Users/didi/Downloads/./a.out
dyld: loaded: /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation
dyld: loaded: /usr/lib/libSystem.B.dylib
dyld: loaded: /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation
…

因为Fundation库依赖一些其他库,其他库还会依赖更多,因此为了减少处理的时间,系统上的动态链接器会共享缓存,共享的缓存在/var/db/dyld/,这样,当加载Mach-O文件时动态链接器会先检查共享内存是否有。每个进程都会有自己地址空间映射这些共享缓存,可以优化启动速度

dyld做了什么事情

  • kernel做启动程序初始准备,开始由dyld负责
  • 基于非常简单的原始栈为kernel设置进程来启动自身
  • 使用共享缓存来处理递归依赖带来的性能问题,ImageLoader会读取二进制文件,其中包含了我们的类,方法等各种符号
  • 立即绑定non-lazy的符号并设置用于lazy bind的必要表,将这些库link到执行文件里
  • 为可执行文件运行静态初始化
  • 设置参数到可执行文件的 main 函数并调用它
  • 在执行期间,通过绑定符号处理对lazily-bound符号存根的调用提供runtime动态加载服务(通过 dl*() 这个 API ),并为gdb和其它调试器提供钩子以获得关键信息。runtime会调用map_images做解析和处理,load_images来调用call_load_methods方法遍历所有加载了的Class,按照继承层级依次调用+load方法;
  • 在main函数返回后运行 static terminator
  • 在某些情况下,一旦main函数返回就需要调用libSystem的_exit

查看运行时的调用 map_images 和 调用 +load 方法的相关 runtime 处理可以通过 RetVal 的可debug 的 objc/runtimeRetVal/objc-runtime: objc runtime 706 来进行断点查看调用的runtime方法具体实现。在 debug-objc 下创建一个类,在 +load 方法里断点查看走到这里调用的堆栈如下:

0 +[someclass load]
1  call_class_loads()
2  ::call_load_methods
3  ::load_images(const char *path __unused, const struct mach_header *mh)
4  dyld::notifySingle(dyld_image_states, ImageLoader const*, ImageLoader::InitializerTimingList*)
11 _dyld_start

load_images方法里断点 p path 可以打印出所有加载的动态链接库,这个方法的hasLoadMethods用于快速判断是否有+load方法。

prepare_load_methods这个方法会获取所有类的列表然后收集其中的+load方法,在代码里可以发现Class+load是先执行的,然后执行Category+load方法。为什么这样做,原因可以通过prepare_load_methods这个方法看出,在遍历Class+load方法时会执行schedule_class_load这个方法,这个方法会递归到根节点来满足Class收集完整关系树的需求。
最后call_load_methods会创建一个autoreleasePool使用函数指针来动态调用类和Category+load方法

如果想了解CocoaFundation库可以通过GNUStep源码来学习。比如 NSNotificationCenter 发送通知是按什么顺序发送的可以查看 NSNotificationCenter.m 里的 addObserver 方法和 postNotification 方法,看看观察者是怎么添加的和怎么被遍历通知到的

dyld 是开源的: GitHub - opensource-apple/dyld