一、前言

1.1 程序和进程

广义上的程序就是一个静态的可执行文件,是由一个已经编译好的指令和数据集合的一个文件。就像通过 Xcode 编译好的 Mach-O 文件。而进程则是一个动态的概念,是程序的运行时的一个过程。

1.2 虚拟内存

每个进程内部都是使用的逻辑地址空间,这个逻辑地址与物理 RAM 之间存在着映射关系,这个映射是以 page 为单位的。这种映射关系不一定是 1 对 1 的,有可能某个逻辑地址不对应任何的物理 RAM,也可以多个逻辑地址共同对应一个物理 RAM 地址。虚拟内存主要有以下几个用处:

  1. 当某个逻辑地址没有对应的物理内存地址的时候,内核会中断当前线程以执行对应的策略;
  2. 多个逻辑地址映射到同一个物理内存地址的时候,可以允许不同的进程共享同一块物理内存;
  3. 允许在读取文件的时候不用读取整个文件直接到内存,而是通过调用 mmap 将文件的某个部分读取到进程的某段逻辑地址,这样当你第一次读取文件的某个部分的时候,因为它还没有与真实的物理地址对应,也就是上面的第一种情况,此时系统会仅仅读取该部分,也就是 page 大小的内容到内存中,实现了读取文件的懒加载

每个进程运行的时候都有自己独立的虚拟地址空间,这个空间的大小是由计算机的硬件决定的,比如在 32 位硬件平台上,它的寻址空间大小是 232 - 1,64 位的寻址空间为 264 - 1。

1.3 以 page 为单位的操作

基于上面的特性,任何 Mach-O 镜像的 TEXT segment 部分都可以被映射到不同的进程内,被懒加载读取,而且以 page 为单位被多个进程共享。

而对于 DATA 部分,因为它是可读写的,因此有对应的 Copy-On-Write 策略,当某进程在去读取 DATA 部分而另外一个进程需要修改的时候,内核会将需要被写入的 page 大小部分拷贝到另外一个物理内存地址中,然后将该线程内它的虚拟地址映射到新的内存地址。此时系统成有 dirty 和 clean 两份 page,被 copy 出来的那份为 dirty page,同时脏数据页还包含了进程相关的信息,这部分数据页是比较耗性能的。

最后,权限的设置也是以 page 为单位的,可以对单独的 page 数据设置可读或可写等权限。

1.4 安全性

  1. ASLR:物理内存的分配是随机的,镜像被夹在到随机的内存地址当中;
  2. 代码签名:为了保证在运行的时候保证文件内容的正确性,需要对文件进行签名,但是因为文件内存的物理地址是随机的而且是以 page 为单位的,为了快速地判断文件内容是否被修改过,需要对每个 page 签名,所有这些签名信息被存储在 LINKEDIT。

一、Mack-O 格式

Mack-O 包含如下几种格式的文件:

  1. Executable:应用以及 extension 的可执行二进制文件
  2. Dylib:动态链接库,如其他平台上的 DSO 和 DLL
  3. Bundle:不能被链接,只能在运行时使用 dlopen 加载
  4. Image:镜像,可以用来表示任何一种类型的可执行文件,如 Executable、Dylib 和 Bundle
  5. Framework:苹果特有的一种可执行文件,里面包含了 Dylib 以及相关的头文件和资源

1.1 Mack-O 镜像文件

  1. 镜像文件被划分为 segments,所有的 segments 名字都是大写的;
  2. 每个 segment 具有一个或多个 page size 大小,page size 大小由硬件决定,如 arm64 是 16kb,其他的是 4kb;
  3. 几乎每一个镜像文件都包含的 segment 有 TEXT、DATA 和 LINKEDIT;
  4. TEXT 处于文件的开头,它包含了 Mack Header(指定文件的目标体系结构,如 PPC、PPC64、IA-32 或 x86-64),被执行的机器指令以及一些只读常量,如 c 字符串;
  5. DATA 部分是可读写的,包含了全局变量以及静态变量;
  6. LINKEDIT 是原数据部分,如函数的名字和地址。

Mack-O 通用文件

针对多个硬件平台的Mack-O文件的合体
文件头部为Fat Header segment,里面记录了所有架构以及他们的TEXT segement在文件内的偏移量
这个Fat Header只有一个 page 的大小
Mack-O 通用文件内容
这里所有的segment的大小都需要是page大小的整数倍,主要原因与接下来要将的虚拟内存有关

虚拟内存
每个进程内部都是使用的逻辑地址空间,这个逻辑地址与物理RAM之间存在着映射关系,这个映射是以page为单位的。这种映射关系不一定是1对1的,有可能某个逻辑地址不对应任何的物理RAM,也可以多个逻辑地址共同对应一个物理RAM地址。虚拟内存主要有以下几个用处:

当某个逻辑地址没有对应的物理内存地址的时候,内核会中断当前线程以执行对应的策略
多个逻辑地址映射到同一个物理内存地址的时候,可以允许不同的进程共享同一块物理内存
允许在读取文件的时候不用读取整个文件直接到内存,而是通过调用mmap将文件的某个部分读取到我这个进程的某段逻辑地址,这样当你第一次读取文件的某个部分的时候,因为它还没有与真实的物理地址对应,也就是上面的第一种情况,此时系统会紧紧读取该部分,也就是page大小的内容到内存中,实现了读取文件的懒加载
以page为单位的操作
基于上面的特性,任何Mach-O镜像的TEXT segment部分都可以被映射到不同的进程内,被懒加载读取,而且以page为单位被多个进程共享。

而对于DATA部分,因为它是可读写的,因此有对应的Copy-On-Write策略,当某进程在去读取DATA部分而另外一个进程需要修改的时候,内核会将需要被写入的page大小部分拷贝到另外一个物理内存地址中,然后将该线程内它的虚拟地址映射到新的内存地址。此时系统成有dirty和clean两份page,被copy出来的那份为dirty page,同时脏数据页还包含了进程相关的信息,这部分数据页是比较耗性能的

最后,权限的设置也是以page为单位的,可以对单独的page数据设置可读或可写等权限

安全性
ASLR:物理内存的分配是随机的,镜像被夹在到随机的内存地址当中
代码签名:为了保证在运行的时候保证文件内容的正确性,需要对文件进行签名,但是因为文件内存的物理地址是随机的而且是以page为单位的,为了快速地判断文件内容是否被修改过,需要对每个page签名,所有这些签名信息被存储在LINKEDIT
Mack-O 镜像的加载
首先在加载一个dyld的时候,我们将它进行虚拟地址映射而不是直接读取到物理内存中,文件的开头映射到虚拟内存的起始位置
系统首先读取文件的Mach Header,发现没有对应的物理内存,此时开始上文提到的懒加载,将一个page的数据读取到物理内存中,并做好与虚拟内存的映射
然后系统用同样的方式继续往后读取Mach Header,当发现某些信息需要从LINKEDIT中获取的时候,就开始用同样的方式读取LINKEDIT中的部分
但是在进程中,LINKEDIT会告诉dyld,你需要对DATA部分做一些修正才能让dyld正确运行,于是又开始用同样的懒加载方式读取了DATA部分的数据库到内存中
但是此时进程是会修改DATA部分的数据的,因此被修改的部分被标记为脏数据页
此时如果有另外一个进程使用此dyld的时候,TEXT和LINKEDIT部分是不会被重新读取的,可以直接使用内存中已有的内容,对于DATA部分的数据,如果内存中有对应的脏数据页,则重新读取,如果没有那么就服用内存中现有的数据,如果同一段数据都被两个进程修改了,那么此时内存中会有两个对应的脏数据页
镜像文件的加载
exec() to main()
exec()
exec是一个系统调用。

当内核启动一个应用的时候,会随机地在无用的内存地址内找个地方加载你的应用。
加载应用的起始点到内存的开头被标记为不可读取、不可写、不可执行
当需要使用到动态库的时候,由dyld来加载动态库,因此此时系统也将Dyld夹在到内存中的一个随机的地址,有dyld来结束剩下的加载,它的主要职责就是加载所有使用到的动态库,并使它们准备好运行
动态库加载
dyld的运行主要有以下几个步骤:

首先,dyld映射所有以来到的dylibs,通过从主运行程序中的头部中可以读取到所有依赖的dylibs
查找到所有的dylibs
打开并开始读取每个Mack-O文件
验证Mack-O文件,并将验证信息注册到内核当中
这样就可以对每个page块调用mmap()
如此依赖应用的所有动态库也被加载到内存中,一般情况下一个应用会加载1-400个dylibs,当然大部分是系统的动态库,在加载的时候系统已经做过优化

但是此时所有的动态库都是各自为政的,它们之间存在着依赖关系,所以我们还需要将它们串联起来,也就是修正引用的地址。这里就涉及到一个问题,因为有代码签名的原因,我们不能直接修改指令,那么我们又该如何修正动态库之间的互相调用呢?

现代的code-gen代码生成器是一种动态的PIC(Position Independent Code),位置独立表示代码可以被加载到任意的地址,动态表示指令不是直接被指向的,也就是说当需要调用其他库中的指令的时候,code-gen会在DATA块中创建一个指针,这个指针指向了真实的指令,然后本库中的调用加载这个被创建的指针并跳转到真正被调用的指令去。

dyld在这阶段的主要任务就是修正这些指针和数据。这里分为两种情况:rebasing和binding。

rebasing是修正指向本镜像内部的指针;在iOS4.3前会把dylib加载到指定地址,所有指针和数据对于代码来说都是固定的,dyld 就无需做rebase/binding了,iOS4.3后引入了 ASLR ,dylib会被加载到随机地址,这个随机的地址跟代码和数据指向的旧地址会有偏差,dyld 需要修正这个偏差,做法就是将 dylib 内部的指针地址都加上这个偏移量。然后就是重复不断地对LINK 段中需要 rebase 的指针加上这个偏移量。这可能会产生 I/O 瓶颈,但因为 rebase 的顺序是按地址排列的,所以从内核的角度来看这是个有次序的任务,它会预先读入数据,减少 I/O 消耗
binding是修正指向镜像外部的指针。这部分指针实际上是通过字符串来表示的,在运行的时候dyld需要找到这个符号所表示的真实实现在哪里,这里就需要通过查找符号表,因此会有大量的计算。这一步所需要的io很少,因为link段中的数据在之前的rebasing阶段已经被读取过了。
地址修正
修正信息
接下来的步骤就是Objc运行时初始化。

Objc在DATA段中有大量对应的数据信息,这部分在rebasing和binding阶段基本上已经完成
因为Objc是一种动态语言,因此需要注册类名与类相关信息的一张注册表
对在其他dylib中定义的category,也需要通过rebasing和binding来修正扩展方法的地址
保证selector的唯一性
到这里我们基本完成了DATA段中静态数据的修正,接下来就是对一些动态数据的修正:

C++被静态初始化的对象方法,如用attribute((constructor))修饰
Objc对应的load方法(官方不推荐使用load方法,而推荐使用initialize,如果有自定义load方法的话,那么将会在这个时刻执行)
按照引用层级,所有上面的方法从底向上调用执行

iOS App启动时间优化原理

iOS MachO的更多相关文章

  1. 由App的启动说起(转)

    The two most important days in your life are the day you are born and the day you find out why. -- M ...

  2. iOS系统分析(二)Mach-O二进制文件解析

    ➠更多技术干货请戳:听云博客 0x01  Mach-O格式简单介绍 Mach-O文件格式是 OS X 与 iOS 系统上的可执行文件格式,类似于windows的 PE 文件 与 Linux(其他 Un ...

  3. 了解iOS上的可执行文件和Mach-O格式

    http://www.cocoachina.com/mac/20150122/10988.html http://www.reinterpretcast.com/hello-world-mach-o ...

  4. iOS逆向系列-Mach-O文件

    概述 Mach-O是Mach object的缩写,是Mac\iOS上用于存储程序.库的标准格式. 常见的Mach-O文件 属于Mach-O格式的文件类型有. 可以在xnu源码中,查看到Mach-O格式 ...

  5. 【iOS】Apple Mach-O Linker Error Linker command failed with exit code 1

    又遇到了这个问题,貌似之前遇到过……如图所示: 解决方法寻找中………… 在 Stack Overflow 找到了解决方法,如下: 参考链接:Apple Mach-O Linker Error

  6. iOS开发之App间账号共享与SDK封装

    上篇博客<iOS逆向工程之KeyChain与Snoop-it>中已经提到了,App间的数据共享可以使用KeyChian来实现.本篇博客就实战一下呢.开门见山,本篇博客会封装一个登录用的SD ...

  7. Mach-O 的动态链接(Lazy Bind 机制)

    ➠更多技术干货请戳:听云博客 动态链接 要解决空间浪费和更新困难这两个问题最简单的方法就是把程序的模块相互分割开来,形成独立的文件,而不再将它们静态的链接在一起.简单地讲,就是不对那些组成程序的目标文 ...

  8. 【腾讯Bugly干货分享】移动App入侵与逆向破解技术-iOS篇

    本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/577e0acc896e9ebb6865f321 如果您有耐心看完这篇文章,您将懂 ...

  9. dyld 加载 Mach-O

    ➠更多技术干货请戳:听云博客 前言 最近看 ObjC的runtime 是怎么实现 +load 钩子函数的实现.进而引申分析了 dyld 处理 Mach-O 的这部分机制. 1.简单分析 Mach-O ...

随机推荐

  1. Java - 常见的算法

    二分法查找 private static int binarySearch(int[] list,int target) { ; ; //直到low>high时还没找到关键字就结束查找,返回-1 ...

  2. PHPRAP v1.0.6 发布,修复因php7.1版本遗弃mcrypt扩展造成安装失败的BUG

    PHPRAP,是一个PHP轻量级开源API接口文档管理系统,致力于减少前后端沟通成本,提高团队协作开发效率,打造PHP版的RAP. 更新记录 [修复]修复因php7.1版本遗弃mcrypt扩展造成安装 ...

  3. 前端每日实战:31# 视频演示如何利用 CSS 的动画原理,创作一个乒乓球对打动画

    效果预览 按下右侧的"点击预览"按钮可以在当前页面预览,点击链接可以全屏预览. https://codepen.io/comehope/pen/rvgLzK 可交互视频教程 此视频 ...

  4. 小程序的数据存储,与Django等服务发送请求

    目录 官方文档 快速归纳 存取改删 1.wx存储数据到本地以及本地获取数 1.1 wx.setStorageSync(string key, any data) 存(同步) 1.2 wx.setSto ...

  5. openwrt 外挂usb 网卡 RTL8188CU 及添加 RT5572 kernel支持

    RT5572 原来叫 Ralink雷凌 现在被 MTK 收购了,淘宝上买的很便宜50块邮,2.4 5G 双频.在 win10 上插了试试,果然是支持 5G.这上面写着 飞荣 是什么牌子,有知道的和我说 ...

  6. Python之locust踩坑指北

    坑点1:locust安装报错 其中一种情况:error:Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visua ...

  7. C#制作密码文本框

    2020-03-14 每日一例第7天 1.新建窗体windowform,修改text值: 2.两个按钮后台代码: private void button1_Click(object sender, E ...

  8. app之---豆果美食

    1.抓包 2.代码 抓取: #!/usr/bin/env python # -*- coding: utf-8 -*- #author tom import requests from multipr ...

  9. 03.文件I/O

    UNIX系统中的大多数文件I/O只需用到5个函数:open.read.write.lseek和close. 本章所说明的函数称为不带缓冲的I/O.不带缓冲指的是每个read和write都调用内核中的一 ...

  10. react / config\webpack.config.js 编译后去掉map 减小体积 shouldUseSourceMap = false

    react / config\webpack.config.js 编译后去掉map 减小体积 shouldUseSourceMap = false