在程序启动的时候,我们总是认为程序的入口是从main函数开始的,但是在此之前,
dyld
已经干了很多事情了, 那什么是dyld
呢? 它能够干什么, 它又有什么作用呢? 接下来, 就来研究下这个神奇的东西…
Mach-O
我们先来看下Mach-O
, 在OS X 与 iOS 系统上的可执行文件格式是Mach-O
, 我们编译过程产生的.O文件,以及程序的可执行文件, 动态库等都是Mach-O
文件. 我们来看看下Mach-O
结构:
Mach-o包含三个基本区域:
- 头部(header structure)保存了一些基本信息,包括了该文件运行的平台、文件类型、LoadCommands的个数等.
- 加载命令(load commands) 在加载Mach-O文件时会使用这里的数据来确定内存的分布以及相关的加载命令, 如main函数的加载地址, 程序所需的dyld的文件路径, 以及相关依赖库的文件路径
- Data(数据) 包含多个段(segment),每个段可以拥有零个或多个区域(section)每一个段(segment)都拥有一段虚拟地址映射到进程的地址空间
我们可以从loader.h
里面找到header的结构:
1 | struct mach_header_64 { |
magic:
魔数,用于确认该文件是用64位还是32位
cputype:
CPU类型
cpusubtype:
机器类型
filetype:
文件类型
ncmds:
加载的指令条数
sizeofcmds:
所有加载的指令大小
flags:
标志位
reserved:
保留字段 (32位无此字段)
接下来我们在来看下load_command:
1 | struct load_command { |
这个很好理解, cmd
是加载指令的类型, cmdsize
是指令的总大小
64位下在segment_command
结构:
1 | struct segment_command_64 { /* for 64-bit architectures */ |
cmd:
就是加载指令的类型, #define LC_SEGMENT_64 0x19 /* 64-bit segment of this file to be mapped */
这里LC_SEGMENT_64
意思是将文件中64位的段映射到进程的地址空间
vmaddr:
段虚拟内存的地址
vmsize:
虚拟内存大小
fileoff:
段在文件中的偏移
filesize:
文件大小
nsects:
该段里有多少个section
64位下的section
结构:
1 | struct section_64 { /* for 64-bit architectures */ |
sectname:
section的名称
segname:
该section所属的segment
addr:
该section的内存地址
size:
该section的大小
offset:
该section的文件偏移
align:
该section的内存对齐
reloff:
重定位入口的文件偏移
nreloc:
需要重定位的数量
flags:
标志位 (section类型和属性)
reserved1
保留字段(偏移和索引)
reserved2:
保留字段 (数量和大小)
reserved3:
保留字段
1 |
在头文件中, 我们还找到了一些用于存放不同类型数据的段:
"__text":
源代码编译后的机器指令就放在这个段中, 也称为代码段"__data":
初始化全局变量和静态变量就放在这个段"__bss":
未初始化的全局变量和静态变量放在这个段"__OBJC":
runtime 段"__symbol_table":
符号表"__selector_strs":
方法名字符串表"__ICON":
图标段
什么是dyld
The dynamic loader for Darwin/OS X is called dyld, and it is responsible for loading all frameworks, dynamic libraries, and bundles (plug-ins) needed by a process.
苹果文档是这么解释的, 就是动态加载器, 它负责加载程序所需要的所有的frameworks
, 动态库以及插件. 更多详情
由于dyld是开源的, 所以我们可以下载源码,探其究竟. 这里我用的是 dyld-97.1.tar.gz
我们都知道iOS的系统frameworks都是动态链接的,系统内核做好启动程序的初始准备后, 就会将控制权交给 dyld
负责剩下的工作, (dyld
是运行在用户态
的, 这里由内核态
切到了用户态
)
系统使用动态链接好处:
- 代码共用:多个程序都动态链接了相同的
lib
, 但它们在内存和磁盘中中只有一份 - 易于维护:由于被依赖的
lib
是程序执行时才链接的, 所以更新这些lib
很方便 - 减少可执行文件体积:相比静态链接, 动态链接在编译时不需要打包进去, 所以可执行文件的体积要小很多
dyld接手
当dyld接手后, 它干了什么?
我们看到了这样一段注释
1 | // Entry point for dyld. The kernel loads dyld and jumps to __dyld_start which |
uintptr_t _main(const struct mach_header* mainExecutableMH, uintptr_t mainExecutableSlide, int argc, const char* argv[], const char* envp[], const char* apple[])
意思是说这个是dyld
入口点, 内核加载dyld
并跳去执行__dyld_start(复杂初始化一些寄存器然后执行_main函数)
最后返回main()
函数的地址
1 | uintptr_t _main(const struct mach_header* mainExecutableMH, uintptr_t mainExecutableSlide, int argc, const char* argv[], const char* envp[], const char* apple[]) { |
这里主要负责主要负责初始化程序环境, 将可执行文件以及相应的依赖库与插入库加载进内存生成对应的ImageLoader
类的image(镜像文件)
对象,对这些image进行链接,调用各image的初始化方法等(注:这里多数事情都是递归的,从底向上的方法调用),其中runtime
也是在这个过程中被初始化
这里对ImageLoader
的解释是:
1 | // ImageLoader is an abstract base class. To support loading a particular executable |
对上面生成的Image
进行进行链接, 其主要做的事有对image
进行load(加载)
,rebase(基地址复位)
,bind(外部符号绑定)
1 | void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, const RPathChain& loaderRPaths) |
最后执行initializeMainExecutable ()
, 调用所有image
的Initalizer
方法进行初始化
由于 runtime
向 dyld
绑定了回调,当 image
加载到内存后, dyld
会通知 runtime
. runtime
的初始化方法是_objc_init
, 我们可以在程序中打一个符号断点, 这个断点会先与main()
函数断点断下, 说明这些方法都是在main函数之前执行
从调用栈我们可以看到dyld
递归调用ImageLoader
初始化方法之后, 就会调用libSystem_initializer
, 之后调用libdipatch_init
, 最后才会走到_objc_init
也就是runtime
的初始化方法, runtime
的初始化之后, 接下来 load_images
中调用 call_load_methods
方法,遍历所有加载进来的 Class, 调用 类的load
方法, 并初始化相应依赖库里的类结构.
到这里, 我们依然还是在main()
函数之前, 当所有依赖库的Initializer
都调用完后, dyld::_main()
函数会返回程序的main
函数地址, 然后main
函数被调用.
总结
系统先读取程序的可执行文件(Mach-O), 从里面获得dyld
的路径,然后加载dyld
,dyld
调用_main(const struct mach_header* mainExecutableMH, uintptr_t mainExecutableSlide, int argc, const char* argv[], const char* envp[], const char* apple[])
去初始化运行环境,开启缓存策略,加载程序相关依赖库(其中也包含我们的可执行文件),并对这些库进行链接,最后递归调用每个依赖库的初始化方法,在这一步,runtime
被初始化, 当所有依赖库的初始化后,轮到最后一位(程序可执行文件)进行初始化,在这时runtime
会对项目中所有类进行类结构初始化,然后调用所有类的load
方法, 最后dyld::_main()
返回main
函数地址,最终main函数被调用, 之后便来到了熟悉的程序入口.
最后更新: 2023年03月25日 22:39:55
本文链接: http://aeronxie.github.io/post/dd841de.html
版权声明: 本作品采用 CC BY-NC-SA 4.0 许可协议进行许可,转载请注明出处!