OC底层原理32:启动优化(一)基本概念

张建 lol

前言

早期的 数据访问 是直接通过 物理地址 访问 物理内存,而物理内存是有 固定大小的,这种方式有以下两个问题:

  1. 内存不够用
  2. 内存数据的安全问题

内存不够用的解决方案:虚拟内存

针对问题1,我们在 进程和物理内存 之间增加一个 中间层,这个中间层就是所谓的 虚拟内存,主要用于解决当多个进程同时存在时,对物理内存的管理,提高了CPU的利用率,使多个进程可以同时、按需加载,所以虚拟内存其本质就是 一张虚拟地址和物理地址对应关系的映射表

  • 每个进程都有一个独立的 虚拟内存,其地址都是 从0开始,到最后结束,大小是 4G 固定的,每个虚拟内存又划分为一个一个的 ,所有页放在一起组成 页表,统称 虚拟内存分页管理,页的大小在 iOS中是16K,Linux、MacOS等是4K,每次加载都是以为单位加载的,进程间是无法互相访问的,保证了进程间数据的安全性。

  • 一个进程中,只有部分功能是活跃的,所以只需要 将进程中活跃的部分放入物理内存,避免物理内存的浪费

  • CPU 需要 访问数据 时,首先是访问虚拟内存,然后通过虚拟内存地址寻址,这个虚拟地址在被送到内存条之前先转换为物理地址,这个转换的过程叫 地址翻译(需要CPU的硬件MMU和操作系统配合),即可以理解为在 虚拟地址和物理地址对应表 中找到对应的 物理地址,然后对相应的物理地址进行访问

  • 如果在访问时,虚拟地址的内容未加载到物理内存,会发生 缺页异常(pagefault),将当前进程阻塞掉,此时操作系统需要先将数据载入到物理内存中,这个过程很快用户感知不到(每页16K),然后再寻址,进行读取。这样就避免了内存浪费。

  • 如果在访问时,物理内存满了,操作系统会将 新数据将旧数据覆盖

如下图所示,虚拟内存与物理内存 间的关系:

内存数据安全问题的解决方案:ASLR技术

在上面解释的虚拟内存中,我们提到了 虚拟内存的起始地址与大小都是固定的,这意味着,当我们访问时,其数据的地址也是固定的,这会导致我们的数据非常容易被破解,为了解决这个问题,所以苹果为了解决这个问题,在iOS4.3开始引入了 ASLR 技术。

ASLR 的概念:(Address Space Layout Randomization)地址空间配置随机加载,是一种 针对缓冲区溢出安全保护技术,通过对 堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的的一种技术。

其目的通过 利用随机方式配置数据地址空间,使某些敏感数据(例如App登录注册、支付相关代码)配置到一个恶意程序无法事先获知的地址,令攻击者难以进行攻击

由于 ASLR 的存在,导致 可执行文件和动态链接库 在虚拟内存中的 记载地址每次启动都不固定 ,所以需要在编译时来修复镜像中的资源指针,来指向正确的地址,即 正确的内存地址 = ASLR地址 + 偏移值

可执行文件

不同的操作系统,其可执行文件的格式也不同,系统内核可执行文件 读取到 内存,然后根据可执行文件的 头签名(magic魔数) 判断二进制文件的格式。

可执行文件 魔数 用途
PE32/PE32+ MZ Windows的可执行文件
ELF \x7FFLF Linux和大部分UNIX的可执行文件和库文件
脚本 #! 主要用于shell脚本,也有一些解释器脚本使用这个格式,这是一种特殊的二进制文件格式,#!后面指向真正的可执行文件(比如python),而脚本其他内容,都被当做输入传递给这个命令
通用二进制格式(胖二进制格式) Oxcafebabe(小端) 包含多种架构支持的Mach-O格式,iOS和OS X支持的格式
Mach-O 0xfeedface(32位) 0xfeedfacf(64位) iOS和OS X支持的格式

其中 PE、ELF、Mach-O 这三种可执行文件格式都是 COFF(Command file format) 格式的变种,COFF的主要贡献是目标文件里面 引入了段的机制,不同的目标文件可以拥有不同数量和不同类型的

通用二进制文件

因为不同的 CPU 平台支持的指令不同,比如 arm64x86,苹果中的通用二进制格式就是 将多种架构的Mach-O文件打包在一起,然后系统根据自己的CPU平台,选择合适的 Mach-O,所以 通用二进制格式 又被称为 胖二进制格式,如下图所示:

通用二进制格式的定义在 <Mach-O/fat.h>中,可以下载xnu ,然后根据 xnu ->EXTERNAL_HEADERS ->mach-o中找到该文件,通用二进制文件开始的 Fat Headerfat_header 结构体,而 Fat Archs 是表示通用二进制文件中有多少个 Mach-O,单个 Mach-O 的描述是通过 fat_arch 结构体。两个结构体的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
- magic:可以让系统内核读取该文件时知道是通用二进制文件
- nfat_arch:表明下面有多个fat_arch结构体,即通用二进制文件包含多少个Mach-O
*/
struct fat_header {
uint32_t magic; /* FAT_MAGIC */
uint32_t nfat_arch; /* number of structs that follow */
};

/*
fat_arch是描述Mach-O
- cputype 和 cpusubtype:说明Mach-O适用的平台
- offset(偏移)、size(大小)、align(页对齐)描述了Mach-O二进制位于通用二进制文件的位置
*/
struct fat_arch {
cpu_type_t cputype; /* cpu specifier (int) */
cpu_subtype_t cpusubtype; /* machine specifier (int) */
uint32_t offset; /* file offset to this object file */
uint32_t size; /* size of this object file */
uint32_t align; /* alignment as a power of 2 */
};

所以,综上所述

  • 通用二进制文件是苹果公司提出的一种新的二进制文件的存储结构,可以 同时存储多种架构的二进制指令,使CPU在读取该二进制文件时可以自动检测并选用合适的架构,以最理想的方式进行读取。

  • 由于通用二进制文件会同时存储多种架构,所以比单一架构的二进制文件大很多,会占用大量的磁盘空间,但由于系统会自动选择最合适的,不相关的架构代码不会占用内存空间,且执行效率高

  • 还可以通过指令来进行Mach-O的合并与拆分

    • 查看当前 Mach-O 的架构:lipo -info MachO文件
    • 合并:lipo -create MachO1 MachO2 -output 输出文件路径
    • 拆分:lipo MachO文件 –thin 架构 –output 输出文件路径

Mach-O文件

Mach-O 文件是 Mach Object 文件格式的缩写,它是用于可执行文件、动态库、目标代码的文件格式。作为 a.out 格式的替代,Mach-O 格式提供了更强的扩展性,以及更快的符号表信息访问速度

熟悉Mach-O文件格式,有助于更好的理解苹果底层的运行机制,更好的掌握 dyld 加载 Mach-O 的步骤。

查看Mach-O文件

如果想要查看具体的 Mach-O 文件信息,可以通过以下 两种 方式,推荐 使用 第二种 方式,更直观:

  • 【方式一】otool 终端命令:otool -l Mach-O文件名

  • 【方式二】MachOView 工具(推荐):将Mach-O可执行文件拖动到 MachOView 工具打开:

Mach-O文件格式

对于OS X 和 iOS来说,Mach-O 是其 可执行文件的格式,主要包括以下几种文件类型

  • Excutable:可执行文件
  • Dylib:动态链接库
  • Bundle:无法被链接的动态库,只能在运行时使用dlopen加载
  • Image:指的是Excutable、Dylib和Bundle的一种
  • Framework:包含Dylib、资源文件和头文件的集合

下面图示是Mach-O镜像文件格式:

以上是Mach-O文件的格式,一个完整的Mach-O文件主要分为三大部分:

  • Header Mach-O头部:主要是Mach-O的cpu架构,文件类型以及加载命令等信息

  • Load Commands 加载命令:描述了文件中数据的具体组织结构,不同的数据类型使用不同的加载命令表示

  • Data 数据:数据中的每个段(segment)的数据都保存在这里,段的概念与ELF文件中段的概念类似。每个段都有一个或多个部分,它们放置了具体的数据与代码,主要包含代码,数据,例如符号表,动态符号表等等

Header

Mach-O的 Header 包含了 整个Mach-O文件的关键信息,使得CPU能快速知道Mac-O的基本信息,其在 Mach.h(路径同前文的fat.h一致)针对 32 位和 64 位架构的cpu,分别使用了mach_headermach_header_64 结构体来 描述Mach-O头部mach_header 是连接器加载时最先读取的内容,决定了一些基础架构、系统类型、指令条数等信息,这里查看64位架构的mach_header_64 结构体定义,相比于32位架构的 mach_header ,只是多了一个 reserved 保留字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
- magic:0xfeedface(32位) 0xfeedfacf(64位),系统内核用来判断是否是mach-o格式
- cputype:CPU类型,比如ARM
- cpusubtype:CPU的具体类型,例如arm64、armv7
- filetype:由于可执行文件、目标文件、静态库和动态库等都是mach-o格式,所以需要filetype来说明mach-o文件是属于哪种文件
- ncmds:sizeofcmds:LoadCommands加载命令的条数(加载命令紧跟header之后)
- sizeofcmds:LoadCommands加载命令的大小
- flags:标志位标识二进制文件支持的功能,主要是和系统加载、链接有关
- reserved:保留字段
*/
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 */
};

其中filetype主要记录Mach-O的文件类型,常用的有以下几种:

1
2
3
4
5
#define MH_OBJECT   0x1     /* 目标文件*/
#define MH_EXECUTE 0x2 /* 可执行文件*/
#define MH_DYLIB 0x6 /* 动态库*/
#define MH_DYLINKER 0x7 /* 动态链接器*/
#define MH_DSYM 0xa /* 存储二进制文件符号信息,用于debug分析*/

相对应的,Header在MachOView中的展示如下:

Load Commands

在Mach-O文件中,Load Commands 主要是用于 加载指令,其大小和数目在Header中已经被提供,其在Mach.h中的定义如下:

1
2
3
4
5
6
7
8
9
/*
load_command用于加载指令
- cmd 加载命令的类型
- cmdsize 加载命令的大小
*/
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};

我们在MachOView中查看Load Commands,其中记录了很多信息,例如动态链接器的位置、程序的入口、依赖库的信息、代码的位置、符号表的位置等等,如下所示:

其中LC_SEGMENT_64的类型segment_command_64定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/*
segment_command 段加载命令
- cmd:表示加载命令类型,
- cmdsize:表示加载命令大小(还包括了紧跟其后的nsects个section的大小)
- segname:16个字节的段名字
- vmaddr:段的虚拟内存起始地址
- vmsize:段的虚拟内存大小
- fileoff:段在文件中的偏移量
- filesize:段在文件中的大小
- maxprot:段页面所需要的最高内存保护(4 = r,2 = w,1 = x)
- initprot:段页面初始的内存保护
- nsects:段中section数量
- flags:其他杂项标志位

- 从fileoff(偏移)处,取filesize字节的二进制数据,放到内存的vmaddr处的vmsize字节。(fileoff处到filesize字节的二进制数据,就是“段”)
- 每一个段的权限相同(或者说,编译时候,编译器把相同权限的数据放在一起,成为段),其权限根据initprot初始化。initprot指定了如何通过读/写/执行位初始化页面的保护级别
- 段的保护设置可以动态改变,但是不能超过maxprot中指定的值(在iOS中,+x和+w是互斥的)
*/
struct segment_command_64 { /* for 64-bit architectures */
uint32_t cmd; /* LC_SEGMENT_64 */
uint32_t cmdsize; /* includes sizeof section_64 structs */
char segname[16]; /* segment name */
uint64_t vmaddr; /* memory address of this segment */
uint64_t vmsize; /* memory size of this segment */
uint64_t fileoff; /* file offset of this segment */
uint64_t filesize; /* amount to map from the file */
vm_prot_t maxprot; /* maximum VM protection */
vm_prot_t initprot; /* initial VM protection */
uint32_t nsects; /* number of sections in segment */
uint32_t flags; /* flags */
};

Data

Load Commands 后就是 Data 区域,这个区域 存储了具体的只读、可读写代码,例如方法、符号表、字符表、代码数据、连接器所需的数据(重定向、符号绑定等)。主要是存储具体的数据。其中大多数的Mach-O文件均包含以下三个段:

  • __TEXT 代码段:只读,包括函数,和只读的字符串
  • __DATA 数据段:读写,包括可读写的全局变量等
  • __LINKEDIT: __LINKEDIT包含了方法和变量的元数据(位置,偏移量),以及代码签名等信息。

Data 区中,Section 占了很大的比例,SectionMach.h 中是以结构体section_64(在arm64架构下)表示,其定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*
Section节在MachO中集中体现在TEXT和DATA两段里.
- sectname:当前section的名称
- segname:section所在的segment名称
- addr:内存中起始位置
- size:section大小
- offset:section的文件偏移
- align:字节大小对齐
- reloff:重定位入口的文件偏移
- nreloc:重定位入口数量
- flags:标志,section的类型和属性
- reserved1:保留(用于偏移量或索引)
- reserved2:保留(用于count或sizeof)
- reserved3:保留
*/

struct section_64 { /* for 64-bit architectures */
char sectname[16]; /* name of this section */
char segname[16]; /* segment this section goes in */
uint64_t addr; /* memory address of this section */
uint64_t size; /* size in bytes of this section */
uint32_t offset; /* file offset of this section */
uint32_t align; /* section alignment (power of 2) */
uint32_t reloff; /* file offset of relocation entries */
uint32_t nreloc; /* number of relocation entries */
uint32_t flags; /* flags (section type and attributes)*/
uint32_t reserved1; /* reserved (for offset or index) */
uint32_t reserved2; /* reserved (for count or sizeof) */
uint32_t reserved3; /* reserved */
};

SectionMachOView 中可以看出,主要集中体现在 TEXTDATA 两段里,如下所示:

其中常见的section,主要有以下一些:

所以,综上所述,Mach-O的格式如下:

  • Post title:OC底层原理32:启动优化(一)基本概念
  • Post author:张建
  • Create time:2021-05-04 14:44:11
  • Post link:https://redefine.ohevan.com/2021/05/04/OC底层原理/OC底层原理32:启动优化(一)基本概念/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.