这篇文章讲的是OS启动优化相关的理论部分。
Mach-O术语
Mach-O是运行时可执行文件的文件类型,这些文件类型包括:
- 可执行文件:应用中最重要的二进制文件,也是应用扩展文件的主二进制文件。
- Dylib:动态链接库(又称DSO或DLL)。动态链接库包括:iOS中用到的所有系统framework,加载OC runtime方法的libobjc,系统级别的libSystem,例如libdispatch(GCD)和libsystem_blocks(Block)。
- Bundle:捆绑包,不能被链接的Dylib,只能在运行时使用dlopen()加载,macOS的插件会用到它。
Image:指的是任意这三种类型。
Framework:这里指的是一个特殊的Dylib,它有一个目录结构用来存储该Dylib所需的文件。
所有动态链接库和静态库.a和所有类文件.o文件最终都由Dyld(apple的动态链接器)加载到内存中。
Mach-O镜像文件
Mach-O的镜像文件被分割成一些segment(段),segment名字都是大写的。所有的segments都是page(页)大小的整数倍。
页的大小跟硬件有关:
-
arm64上是16KB
-
其余的为4KB
一个Segment包含若干个sections(分区),所以section名字都是小写的,分区不遵循页面大小,分区间是不重叠的。
几乎所有的二进制文件都包含这三个段(segment):__TEXT,__DATA和__LINKEDIT: -
TEXT段是文件的开头,包含了Mach的头文件、被执行的代码和只读常量(如C字符串)。只读可执行(r-x)。
-
DATA段是重写段,包含所有的:全局变量,静态变量等。可读写(rw-)。
-
LINKEDIT段包含加载程序的元数据,比如函数的名称和地址。只读(r-)。
Mach-O的通用文件
假设你生成一个64位的iOS应用,那么你就会有一个Mach-O文件,如果你也想让它在32位的机器上运行,Xcode里会发生什么变化呢?
Xcode会重新生成一个Mach-O文件,这个是为32位生成的armv7s,之后Xcode会将这两个文件合并成第三个文件,这个文件就是Mach-O的通用文件。
这个文件前端有一个头文件,所有的头文件都有一个所有体系的列表,它们的偏移值也在文件里,该文件占用一页空间大小。
虚拟内存
虚拟内存是一个间接层。每个进程都是一个逻辑地址空间,映射到RAM(随机存取存储器)的某个物理页, 这种映射不一定是一对一的。
特点如下:
-
如果有一个逻辑地址不映射任何物理RAM,当进程访问该地址时,就会触发page defult,内核会停止该线程,并试图找出解决方案。
-
当多个逻辑地址映射到同一物理RAM上时,会造成多进程共享内存。
-
基于文件的映射
- 不用一次性将整个文件读入物理RAM,可以使用分页映射(mmap()函数)的方式读取。也就是把文件某个段映射到进程逻辑内存的某个页上。
- lazy reading(懒加载)。
总结:Dylib或者imgae的TEXT段可以映射到多个进程,将会造成读取迟缓,而所有这些页面可以在进程间共享。
-
Copy-On-Write(写入时复制),简称COW。也就是多个进程共享一页内存空间时,一旦有进程要做写操作,它会将这页内存内容复制一份出来,然后重新映射逻辑地址到新的RAM页上。也就是这个进程自己拥有了那份内存的拷贝。
-
Dirty vs. clean pages:上面复制出来的副本被认为是脏页面,脏页面是指含有进程特定信息的页面。干净页面是指内核可以按照需要重新建立的页面,比如重新读取磁盘。脏页面比干净页面要昂贵的多,
-
Permissions(权限界限):这里指可以标记一个页面,可读,可写或可执行,或者是它们的任意组合。
Mach-O镜像加载
在多个进程加载Mach-O镜像时 __TEXT和__LINKEDIT是只读的,都是可以共享内存的。而__DATA是可读写的,会产生dirty page。当dyld执行结束后,__LINKEDIT就没用了,对应的内存页就会被回收。
安全
两点安全问题会影响Dyld:
- ASLR:地址空间布局随机化。镜像会在随机的地址上加载。
- 代码签名:为了在运行时验证Mach-O文件的签名,并不是每次重复读入整个文件,而是把每页的内容都生成一个单独的加密散列值,并存储在__LINKEDIT中。这使得文件每页的内容在读取时都能及时校验并确保不被篡改。
下期预告:iOS启动优化之从exec()到main()
关注公众号,提前获取更多文章