1、虚拟内存 & ASLR
在早期计算机中数据是直接通过物理地址访问的
,这就造成了下面两个问题
- 1、内存不够用
- 2、数据安全问题
内存不够 --- > 虚拟内存
虚拟内存就是通过创建一张物理地址和虚拟地址的映射表
来管理内存,提高了CPU利用率,使多个进程可以同时/按需加载
- 在iOS中,每个进程都有独立的
虚拟内存
,存放物理内存中,其地址是从0开始的
,大小固定4G
,每个虚拟内存又会按页划分,每页16K
,以页为单位加载
,每个进程是相互独立的,保证进程间的数据安全 - 当一个进程只有部分功能使用时,系统会自动将使用部分加载到物理内存中
- CPU在进行数据访问时,会先访问虚拟内存,通过虚拟内存和物理内存的映射关系,去对应的物理内存中查找,在CPU上有专门处理映射的硬件
- 当CPU访问数据时,如果虚拟内存中的数据没有物理内存绑定,会发生
缺页异常(pagefault)
,会阻塞当前进程
,直到数据加载到物理内存中,虚拟内存和物理内存绑定 - 当内存条中的内存全部用完后,系统会将新的数据
覆盖
到很久没使用的内存上
数据安全 --- > ASLR
因为虚拟内存的起始地址(0x000000)和大小(4G)是固定的,这就意味着我们的数据地址也是固定的
,所以在iOS4.3引进了ASLR
ASLR:英文全称Address Space Layout Randomization,也叫地址空间配置随机加载
,是一种针对缓冲区溢出
的安全保护技术
。通过对堆、栈、共享库映射等线性区布局的随机化
,增强了攻击者找到目标物理内存的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的的一种技术。
由于ASLR的存在,导致可执行文件和动态链接库在虚拟内存中的地址每次都是不固定的,所以需要在编译时来修复镜像中的资源指针,来指向正确的地址。即正确的内存地址 = ASLR地址 + 偏移值
2、Mach-O
Mach-O
文件是Mach Object
文件格式的缩写,它是用于可执行文件、动态库、目标代码的文件格式。作为a.out格式的替代,Mach-O格式提供了更强的扩展性,以及更快的符号表信息访问速度
我们可以通过工具MachOView
来查看Mach-O文件具体信息
对于OS X 和iOS来说,Mach-O是其可执行文件的格式
,主要包括以下几种文件类型
-
Executable
:可执行文件 -
Dylib
:动态链接库 -
Bundle
:无法被链接的动态库,只能在运行时使用dlopen加载 -
Image
:指的是Executable、Dylib和Bundle的一种 -
Framework
:包含Dylib、资源文件和头文件的集合
Mach-O文件格式
一个完整的Mach-O文件主要分为三大部分:
-
Header
:Mach-O的cpu架构,文件类型以及加载命令等信息 -
Load Comands
:文件中数据的具体组织结构,不同数据使用不同的加载命令 -
Data
:数据中的每个段(Segment)的数据保存在这里,每个段有一个或多个部分,存放具体的数据、代码、符号表、动态符号表等
Header
Header
中包含了整个Mach-O文件中的关键信息
,可以使CPU快速知道整个Mach-O的基本信息,决定了一些基础架构、系统类型、指令条数等信息。针对32位和64位架构的cpu,分别使用了mach_header
和mach_header_64
结构体来描述Mach-O头部,mach_header_64
结构体相比于mach_header
只是多了一个reserved
保留字段
/*
- 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的文件类型,常用的有以下几种
#define MH_OBJECT 0x1 /* 目标文件*/
#define MH_EXECUTE 0x2 /* 可执行文件*/
#define MH_DYLIB 0x6 /* 动态库*/
#define MH_DYLINKER 0x7 /* 动态链接器*/
#define MH_DSYM 0xa /* 存储二进制文件符号信息,用于debug分析*/
Load Commands
Load Commands
主要用于加载指令
,例如动态链接器的位置、程序的入口、依赖库的信息、代码的位置、符号表的位置
等等。其大小和数目在Header中已确定,在Mach.h中的定义如下
/*
load_command用于加载指令
- cmd 加载命令的类型
- cmdsize 加载命令的大小
*/
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};
其中LC_SEGMENT_64
的类型segment_command_64
定义如下
/*
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
Data
主要用于存储具体的只读、可读写代码
,例如方法、符号表、字符表、代码数据、重定向/符号绑定等。其中大多数的Mach-O文件均包含以下三个段:
-
__TEXT
:代码段:只读,包括函数和只读字符串 -
__DATA
:数据段:读写,可读写的全局变量等 -
__LINKEDIT
:包含方法和变量的元数据(位置、偏移量),代码签名等信息
在Data
区中,section
占很大比例,Section在Mach.h中是以结构体section_64
(在arm64架构下)表示,其定义如下
/*
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 */
};
3、优化建议
启动的过程一般是指点击App图标
开始到AppDelegate 的didFinishLaunching
-
冷启动
:内存中不包含App相关数据,一般可以通过重启手机来实现冷启动 -
热启动
:杀掉App进程后,数据依然存在内存中时启动
启动优化一般指在冷启动情况
下主要分为两部分:
-
main函数开始之前
(pre-main): 操作系统加载App可执行文件到内存,执行一系列的加载和链接操作,也就是dyld加载过程
-
main函数开始
:从main函数开始到AppDelegate 的didFinishLaunching
方法执行为止,加载渲染第一个界面
main函数之前
在之前的文章已经了解过dyld加载流程
我们可以通过Edit Scheme -> Run -> Arguments ->Environment Variables->点击+添加环境变量DYLD_PRINT_STATISTICS == 1
上图中pre-main阶段总共用时1.7s
dylib loading
:动态库加载耗时-
rebase/binding
:偏移修正和符号绑定-
rebase
:每个app的二进制文件内部的方法、函数调用,都有一个偏移地址
,每次app启动,系统会分配一个ALSR
,例如,二进制文件中有一个 test方法,偏移值是0x0001,而随机分配的ASLR是0x1f00,如果想访问test方法,其内存地址(即真实地址)变为 ASLR+偏移值 = 运行时确定的内存地址(即0x1f00+0x0001 = 0x1f01) -
binding
:绑定就是给符号赋值的过程
。方法、函数在编译时期会创建一个对应的符号
指向一个随机地址,在运行时(磁盘加载到内存中,是一个镜像文件),会将真正的内存地址和符号进行绑定
(即dyld
将内存地址和符号绑定,也称动态库符号绑定
)
-
Objc setup
:oc类注册的耗时,类越多,耗时越长,swift
没有这个initializer
:执行load和构造函数的耗时
优化
少用外部动态库
,苹果官方建议自定义动态库数量不要超过6个
,如果超过需要进行动态库合并减少OC类使用
将不必须在
+load
方法中做的事情延迟到+initialize
中,尽量不要用C++虚函数
如果是
swift
,尽量使用struct
main函数开始后
减少启动初始化的流程
:能懒加载的懒加载,能延迟的延迟,能放后台初始化的放后台,尽量不要占用主线程的启动时间优化代码逻辑,去除非必要的代码逻辑
启动阶段能使用
多线程来初始化
的,就使用多线程主UI框架尽量使用
纯代码构建
删除废弃的类和方法