请大家认真看完
前言
自从抖音团队分享了这篇 抖音研发实践:基于二进制文件重排的解决方案 APP启动速度提升超15% 启动优化文章后 , 二进制重排优化
pre-main
阶段的启动时间自此被大家广为流传 .本篇文章首先讲述下二进制重排的原理 , ( 因为抖音团队在上述文章中原理部分大多是点到即止 , 多数朋友看完并没有什么实际收获 ) . 然后将结合
clang
插桩的方式 来实际讲述和演练一下如何解决抖音团队遗留下来的这一问题 :
hook Objc_msgSend 无法解决的 纯swift , block , c++ 方法
.来达到完美的二进制重排方案 .
( 本篇文章由于会从原理角度讲解 , 有些已经比较熟悉的同学可能会觉得节奏偏啰嗦 , 为了照顾大部分同学 , 大家自行根据目录跳过即可 . )
了解二进制重排之前 , 我们需要了解一些前导知识 , 以及二进制重排是为了解决什么问题 .
作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:651612063 进群密码111,不管你是小白还是大牛欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!
虚拟内存与物理内存
在本篇文章里 , 笔者就不通过教科书或者大多数资料的方式来讲述这个概念了 . 我们通过实际问题和其对应的解决方式来看这个技术 or
概念 .
在计算机领域 , 任何一个技术 or
概念 , 都是为了解决实际的问题而诞生的 .
在早期的计算机中 , 并没有虚拟内存的概念 , 任何应用被从磁盘中加载到运行内存中时 , 都是完整加载和按序排列的 .
那么因此 , 就会出现两个问题 :
使用物理内存时遗留的问题
安全问题
: 由于在内存条中使用的都是真实物理地址 , 而且内存条中各个应用进程都是按顺序依次排列的 . 那么在进程1
中通过地址偏移就可以访问到其他进程
的内存 .效率问题
: 随着软件的发展 , 一个软件运行时需要占用的内存越来越多 , 但往往用户并不会用到这个应用的所有功能 , 造成很大的内存浪费 , 而后面打开的进程往往需要排队等待 .
为了解决上述两个问题 , 虚拟内存应运而生 .
虚拟内存工作原理
引用了虚拟内存后 , 在我们进程中认为自己有一大片连续的内存空间实际上是虚拟的 , 也就是说从 0x000000
~ 0xffffff
我们是都可以访问的 . 但是实际上这个内存地址只是一个虚拟地址 , 而这个虚拟地址通过一张映射表映射后才可以获取到真实的物理地址 .
什么意思呢 ?
- 实际上我们可以理解为 , 系统对真实物理内存访问做了一层限制 , 只有被写到映射表中的地址才是被认可可以访问的 .
- 例如 , 虚拟地址
0x000000
~0xffffff
这个范围内的任意地址我们都可以访问 , 但是这个虚拟地址对应的实际物理地址是计算机来随机分配到内存页上的 .- 这里提到了实际物理内存分页的概念 , 下面会详细讲述 .
可能大家也有注意到 , 我们在一个工程中获取的地址 , 同时在另一个工程中去访问 , 并不能访问到数据 , 其原理就是虚拟内存 .
整个虚拟内存的工作原理这里用一张图来展示 :
作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:651612063 进群密码111,不管你是小白还是大牛欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!
虚拟内存与物理内存
虚拟内存解决进程间安全问题原理
显然 , 引用虚拟内存后就不存在通过偏移可以访问到其他进程的地址空间的问题了 .
因为每个进程的映射表是单独的 , 在你的进程中随便你怎么访问 , 这些地址都是受映射表限制的 , 其真实物理地址永远在规定范围内 , 也就不存在通过偏移获取到其他进程的内存空间的问题了 .
而且实际上 , 每次应用被加载到内存中 , 实际分配的物理内存并不一定是固定或者连续的 , 这是因为内存分页以及懒加载以及 ASLR
所解决的安全问题 .
cpu 寻址过程
引入虚拟内存后 , cpu
在通过虚拟内存地址访问数据的过程如下 :
- 通过虚拟内存地址 , 找到对应进程的映射表 .
- 通过映射表找到其对应的真实物理地址 , 进而找到数据 .
这个过程被称为 地址翻译 , 这个过程是由操作系统以及 cpu
上集成的一个 硬件单元 MMU
协同来完成的 .
那么安全问题解决了以后 , 效率问题如何解决呢 ?
虚拟内存解决效率问题
刚刚提到虚拟内存和物理内存通过映射表进行映射 , 但是这个映射并不可能是一一对应的 , 那样就太过浪费内存了 . 为了解决效率问题 , 实际上真实物理内存是分页的 . 而映射表同样是以页为单位的 .
换句话说 , 映射表只会映射到一页 , 并不会映射到具体每一个地址 .
在 linux
系统中 , 一页内存大小为 4KB
, 在不同平台可能各有不同 .
Mac OS
系统中 , 一页为4KB
,iOS
系统中 , 一页为16KB
.
我们可以使用 pagesize
命令直接查看 .
那么为什么说内存分页就可以解决内存浪费的效率问题呢 ?
内存分页原理
假设当前有两个进程正在运行 , 其状态就如下图所示 :
( 上图中我们也看出 , 实际物理内存并不是连续以及某个进程完整的 ) .
映射表左侧的 0
和 1
代表当前地址有没有在物理内存中 . 为什么这么说呢 ?
当应用被加载到内存中时 , 并不会将整个应用加载到内存中 . 只会放用到的那一部分 . 也就是懒加载的概念 , 换句话说就是应用使用多少 , 实际物理内存就实际存储多少 .
当应用访问到某个地址 , 映射表中为
0
, 也就是说并没有被加载到物理内存中时 , 系统就会立刻阻塞整个进程 , 触发一个我们所熟知的缺页中断 - Page Fault
.当一个缺页中断被触发 , 操作系统会从磁盘中重新读取这页数据到物理内存上 , 然后将映射表中虚拟内存指向对应 ( 如果当前内存已满 , 操作系统会通过置换页算法 找一页数据进行覆盖 , 这也是为什么开再多的应用也不会崩掉 , 但是之前开的应用再打开时 , 就重新启动了的根本原因 ).
通过这种分页和覆盖机制 , 就完美的解决了内存浪费和效率问题 .
但是此时 , 又出现了一个问题 .
问 : 当应用开发完成以后由于采用了虚拟内存 , 那么其中一个函数无论如何运行 , 运行多少次 , 都会是虚拟内存中的固定地址 .
什么意思呢 ?
假设应用有一个函数 , 基于首地址偏移量为
0x00a000
, 那么虚拟地址从0x000000 ~ 0xffffff
, 基于这个 , 那么这个函数我无论如何只需要通过0x00a000
这个虚拟地址就可以拿到其真实实现地址 .而这种机制就给了很多黑客可操作性的空间 , 他们可以很轻易的提前写好程序获取固定函数的实现进行修改
hook
操作 .
为了解决这个问题 , ASLR
应运而生 . 其原理就是 每次 虚拟地址在映射真实地址之前 , 增加一个随机偏移值 , 以此来解决我们刚刚所提到的这个问题 .
( Android 4.0
, Apple iOS4.3
, OS X Mountain Lion10.8
开始全民引入 ASLR
技术 , 而实际上自从引入 ASLR
后 , 黑客的门槛也自此被拉高 . 不再是人人都可做黑客的年代了 ) .
至此 , 有关物理内存 , 虚拟内存 , 内存分页的完整流程和原理 , 我们已经讲述完毕了 , 那么接下来来到重点 , 二进制重排 .
二进制重排
概述
在了解了内存分页会触发中断异常 Page Fault
会阻塞进程后 , 我们就知道了这个问题是会对性能产生影响的 .
实际上在 iOS
系统中 , 对于生产环境的应用 , 当产生缺页中断进行重新加载时 , iOS
系统还会对其做一次签名验证 . 因此 iOS
生产环境的应用 page fault
所产生的耗时要更多 .
抖