带你走入苹果的世界

带你走入苹果的世界

  • 1.从iOS-Beta说起
  • 2.背后隐藏的问题
    2.1 是否所有手机都能够升级到最新的iOS系统
    2.2为什么我们的beta系统升级完成之后,手机里大部分软件还是能正常运行?
    2.3 什么原因导致某些软件不能正常运行
  • 3.更深层次的问题-软件的运行的本质
    3.1 软件运行的目的
    3.2 可执行程序是(数据+CPU指令)
    3.3 操作系统读懂可执行程序
    3.4 文件格式到底长成什么样?
    3.5 细看segment

#######1.从iOS-Beta说起
iOS-Beta版本一发布,开发者一般都会抢先下载体验,这有三个主要目的:
1.看看自家已上架的App在新系统里会不会出现crash,好尽早修复问题.免得苹果iOS版本发布后,用户遭殃.
2.App工程和新iOS-SDK能否build Success.充分把握这个buffer时间,适配新的编译环境.
3.基于新iOS-SDK开发新功能.


image.png

为了达到目的,我们首先需要把手机更新到beta版本,同时升级改造升级我们开发人员的IDE工具,以支持开发.

image.png

好像,讲到这里,我们要说的事情就结束了.... 这只是一次相对并不复杂的配置改动.
然鹅,聪明的你,肯定知道这才是刚刚开始.
#######2.背后隐藏的问题
你是否想过,是否所有手机都能够升级到最新的iOS系统.
为什么我们的beta系统升级完成之后,手机里大部分软件还是能正常运行?
是什么原因导致某些软件不能正常运行?

今天我们将一步步剖析,围绕beta版本的体验问题,回顾历史,不断深入,让你了解到软件运行的本质.


image.png

########2.1 是否所有手机都能够升级到最新的iOS系统
有时候苹果推出新的iOS新系统,有些老旧iOS设备出现掉队,不支持运行新系统.iOS11更是一个分水岭,彻底和硬件层面是32位架构的"SoC"说再见.(System on a Chip系统级芯片是CPU、GPU、音频芯片、无线芯片、电池管理等等的集合体,CPU,GPU是它的重要组成部分)
具体可以参考 https://en.wikipedia.org/wiki/System_on_a_chip
下图应该是比较形象地展示了SoC,注意中间的 ARM Cortex M3, 这是Micro Computer Unit, 属于一种微处理器类型.

image.png

32-bit架构的"系统级芯片"如果想运行64-bit的操作系统,这是不可能的.恩,你应该会问为什么?这个问题如果要深究展开技术细节,估计要展开好久.不过我觉得一句话可以比较形象地解释:

应用运行于系统之上,必然受到OS系统的限制,
而OS系统是对硬件资源的操作管理,那它暴露出去的能力也需要建立在硬件能力之上.

用一张图来解释,基于别人的能力,那你总不能比别人牛吧,硬件说我CPU和内存之间只有32根线,你寻址只能32-bit,结果管理硬件资源的OS说,老哥我OS的寻址能力是64-bit,这就有点扯了......

image.png

但是如果硬件层面是64-bit,系统层面和软件层面是32-bit倒是有可能.不过就有点浪费了自己的天赋了.
看起来能否影响到手机是否可以升级主要有俩个基础概念:

  • 系统级芯片SoC的指令架构是"32-bit"还是"64-bit"
  • iOS系统版本是"32-bit"还是"64-bit"

大逻辑就是:系统级芯片SoC架构的指令集(32-bit or 64-bit) >= iOS系统版本(32-bit or 64-bit)

#########2.1.1 iPhone手机的SoC & iOS系统历史
从上面我们可以看到,SoC和OS系统是紧密关联的,我们下面以苹果为案例,来看看他是如何处理俩者之间的关系.
##########2.1.1.1 苹果的SoC历史
时间回溯到2005年,当时的Intel CEO 保罗·欧德宁把Intel带向了辉煌.Mac也从PowerPC转移到Intel x86,乔布斯干脆咔嚓掉早年给Mac搭建的芯片设计团队,寄希望于Intel的Atom处理器.然而当时因为iPod而成为苹果二号人物的Tony Fadell却大力反对,支持更简单、更省电的ARM架构.苹果说SoC不仅仅是一个处理器的事情,还要其他类型芯片来配合,Intel只擅长处理器,绘图芯片做得烂,改进动作太慢;Intel则说钱没给够.总之这俩最后在移动端就这样分手了.于是苹果就转头三星设计的ARM架构应用处理器.


image.png

2007年,初代iPhone发布之后,乔布斯发现芯片是个基础中的基础,需要完全自控.但是俩年前自个干掉了芯片团队,这个坑,只能自己填上.一方面和三星签订SoC开发协议.另一方面招兵买马,收购芯片公司.于是乎,2010年,诞生了自行设计的第一款SoC,Apple A4芯片.这个芯片后来被用在iPhone4上.苹果在芯片之路越走越远,从A4,A5一直到2017年发布的A11.其中一个重要的分水A7芯片,这个芯片宣布了64-bit SoC 时代的到来.这款产品被用在iPhone5s上.
当然后来有一天,你会发现....

image.png

台积电的故事也是相当复杂,剧情跌宕起伏,总之感兴趣的自己可以网上去看看,这里只提供关键词:

台积电  叛徒 三星 中芯国际 大陆  张忠谋

##########2.1.1.2 SoC的重要特征-指令集支持
A4,A5,A6芯片都是32-bit的指令架构(Instruction Set Arch)
A7,A8,A9,A10是 32-bit/64-bit 都支持的指令架构
到了A11 只支持64-bit的指令架构(这里注意了, A11是64-bit only)

image.png

A7芯片让苹果迈入了一个新的时代,继续领跑.在股价体现上,少不了A7的功劳.

image.png

#########2.1.2 iOS版本历史
运行于硬件上的就是操作系统.由于A7以前是只支持32-bit指令架构,运行在这些机器上对应的系统要与之匹配.所以在iOS7的时候,是分俩个大版本,一个是32-bit,一个是64-bit.
当时的iPhone5s采用64-bit的A7芯片,所以对应的iOS7就是64-bit的系统,但是iPhone5由于是32-bit的A6芯片,所以它运行的是32-bit的iOS7系统.

image.png

从iOS7到iOS10,每一个系列,都有32-bit,也有64-bit对应的系统.
而到了iOS11的时候,苹果一刀切,系统不再出32-bit的系统了.真正和那些只支持32-bit指令架构的硬件Say GoodBye.
########2.2 为什么我们的beta系统升级完成之后,手机里大部分软件还是能正常运行?
这个问题主要涉及到程序是如何在系统里运行的.不知道此刻,你还能不能想起来课本说的大概的原理.我们这里先讲俩个关键点:动态库链接和指令架构兼容
#########2.2.1 动态库链接
我们前面提到,从iOS7到iOS10,每一个系列,都有32-bit,也有64-bit对应的系统.比如说iPhone5由于他是32-bit的A6芯片,那运行的就是32-bit的iOS,iPhone5s是64-bit的A7芯片,那么就是64-bit的iOS系统.

  • 第一种情况, 如果我们升级了iPhone5的操作系统,手机上的软件还是可以运行的,虽然内置在系统中的动态库发生了升级,可能增加了新的功能,但是由于动态链接技术,所以实现了老APP在升级后的系统中,run起来之后,还是能够链接到他需要的内容.动态链接是怎么做到的呢.我先看看这篇文章会不会太长,再决定要不要在后面加入对应的内容.
    后面我们会讲一下到底动态链接是怎么样做到的.

  • 第二种情况,由于iPhone5s刚刚发布的时候,开发者都还没做好适配工作,总不能让用户没有APP可以下载吧.于是,苹果在64-bit的操作系统中塞入了32-bit的动态链接库,没错,这意味同一个动态链接库,其实在64-bit的系统中是存在俩份的,一份是32-bit的,一份是64-bit,如果你的程序是32-bit的APP,那么OK,系统就加载对应32-bit的库,如果是64-bit的app就加载64-bit的动态链接库.这里边有个问题,不知道你发现了没? 就是系统在内存里加载了俩份动态库,打个比方32-bit的APP-A加载了一份用于GPS定位的动态库,64-bit的App-B则加载了另一份.这会拖慢系统运行.


    image.png

#########2.2.2 指令架构兼容
假如说仅仅iOS系统中多了一份32-bit的动态库,是不是真的就能够运行,并不是这样.如果指令集架构不支持32-bit,那也是白折腾啊.我们的程序最后是机器码的执行.
所以前面 A7,A8,A9,A10是 32-bit/64-bit 都支持的指令架构,就是为了兼容老的32-bit软件.让他们能够跑起来.


image.png

当然这是一把双刃剑,为了兼容,让不同指令架构的软件能够运行,我们丢失了设计上的优雅和更高的性能.在那个时间点,苹果选择了这么一个平衡点,用三四年的时间,让开发者过渡到64-bit软件上来.从A6-A10的指令集都是支持32-bit和64-bit,向下兼容老的软件.

image.png

到了A11,苹果彻底甩掉历史包袱,这个芯片只支持64-bit指令架构,除了本身自己硬件上的常规升级,单独一款指令支持,不用考虑兼容,跑起来自然嗖嗖快.


image.png

我们平时在评价一个芯片的时候,总是看相关的硬件指标,特别的是频率.但是其实一个简洁的设计,没有历史包袱的芯片,在同样频率指标下,其实是更加优秀的.我们就拿喜欢晒参数的小米手机来看,单独说一下snapdragon 845, 小米8采用的这款芯片.


image.png

当你去官网找到他的spec之后,你会发现这个属于 ARM Cortex-A75 微架构,结合最新发布的ARM Cortex-A76(这家伙比米8的 ARM Cortex-A75 更强)


image.png

即使是 ARM Cortex-A76 依然支持 32-bit的 A32&T32指令架构(为了让老旧的32-bit软件运行),在内核代码则只支持64-bit, 那ARM Cortex-A75你可想而知.
总之,即使 ARM Cortex-A76 也是背负着一定的历史债务.这也和Android生态的自由生长有很大关系.
苹果为了让开发者支持64-bit指令架构,主要从三个方面下手

  • 不断升级苹果自家IDE Xcode,让开发者很容易就升级支持64-bit指令架构.
  • 从提交上给了一个deadline,拒绝only 32-bit的APP提交审核
  • 在应用上给出了很强的用户提示,让用户知道这个APP拖慢系统运行
    于是,为了兼容老指令架构同时支持新指令架构.苹果搞了一个fat binary(也就是multiarchitecture binary)


    image.png

#########2.2.2.1 fat binary
fat binary 说白了把不同指令架构下的可执行文件,打包到一起,对应平台跑对应的二进制文件.系统会从你APP里挑选最佳的可执行文件进行运行,以最好地发挥性能. 比如说你是iPhone5,由于这个芯片是32-bit的SoC,那好,运行的时候系统就取出来是32-bit指令架构的二进制程序进行运行;如果是iPhone5s,那就从里边选64-bit指令架构的二进制程序运行.
这导致Appstore中APP的size增大了不少.
关于fat binary 这个技法,其实老早前,苹果已经在PC时代用上了.
最开始苹果采用的的架构是 Motorola 68K, 94年后之后开始转 PowerPC,中间还出现了 PowerPC (G3, G4) version和 PowerPC 64 (G5) version,后面使用X86,具体可以看 http://hohle.net/scrap_post.php?post=197,有详细的介绍.
#########2.2.2.2 bit code
考虑到这个问题,苹果想,开发者老铁们,要不你们提交给我审核的时候,别提交二进制文件了,你们提交一些中间形式的代码(bitcode),我在后台帮你们编译,然后用户从Appstore下载的时候,苹果后台跟进用户手机的芯片型号,下发对应的二进制安装包.一来用户下载的时候包不会那么大,二来我后台机器牛啊,我可以帮你们做足编译链接优化,三来哪天我推出新的指令架构硬件,我只要在云端再重新编译一次,就可以完美支持新硬件.之前为了过渡用户可是花了三四年的时间,才把你们这些开发者和用户赶上架,真是心累.

image.png

注意图中红色部分,按照这种方式,以后完全是存在完美兼容的方案,开发者提交的是中间代码,苹果后台编译器进行重编,完美适配最新的系统,同时在最新的设备上运行.

########2.3 什么原因导致某些软件不能正常运行
#########2.3.1 32-bit app run on only 64-bit OS
有了前面的铺垫,这个问题就很好回答了, 前面我们说到从iOS7到iOS10,每一个系列,都有32-bit,也有64-bit对应的系统.在64bit系统里还多放了一份32-bit的动态链接库,到了iOS11,32-bit的OS被干掉了,苹果也不想维护32-bit的动态链接库.于是运行iOS11的系统里,动态依赖库只有64-bit版本.你手机上的老APP,如果是32-bit,自然就再也跑不起来了....


image.png

#########2.3.2API接口废弃
某个API接口被彻底废弃了...动态链接的时候没调到.那肯定就挂了...
#######3.更深层次的问题-软件运行的本质
我感觉我应该是讲清楚这些问题背后隐藏的原理.但是还是不够系统.我只是从表象去追寻原理.接下来我们就正式开讲:软件运行的本质.App是如何运行起来的.


image.png

其实你看到这里,才是真正的开始.虽然前面说了那么多,不过没关系,当你看到这里的时候,我相信你已经学到很多之前没接触过的知识.
虽然前面我们一直在以iPhone手机这个具体实例来阐述,但是当你把他应用到其他芯片或者操作系统上,他们也是类似的.所以当你去看硬件(PC时代的CPU或者移动时代的芯片)或者是操作系统变迁(Windows或者Android),沿用上面的方式去思考追寻答案,最终也是大同小异.
在前面大知识基础之上,我们来仔细分析一下运行于iOS操作系统之上的App.

image.png

########3.1 软件运行的目的
我们都知道软件存在的意义就是"帮人类解决问题".
如果美图秀秀不能帮我们美颜,那我们还要它干嘛?
用户启动软件之后,用户自己根据主观判断,在软件里做出了操作,我们的软件获取到用户的意图,执行对应的指令,对图片像素点的某个数值进行修改,从而实现用户目的.
下面是一个带褶皱的老图片的修复.就是通过用户指定了特定区域(x,y,width,height),然后让计算机执行对应指令,从而实现修复.


image.png

########3.2 可执行程序是(数据+CPU指令)
为了实现上述"帮人类解决问题"这个美好愿景.App作为可执行程序,由数据和指令共同构成.可执行程序启动时,将对应指令和最初的数据(包括一些初始化和未初始化的数据,以及一些调用地址等)加载到存储设备(内存和寄存器)中.这一系列加载工作准备完成后,操作系统还要根据已加载入内存的可执行程序进行一些环境准备(例如一些动态库绑定,这种属于none-lazy),最后再把控制权交给我们熟悉的main方法.接下来CPU 把这些已经加载到内存中的指令 load 到指令流中一条一条执行,这些指令会获取他们关联到的数据。


image.png

接下来的主要内容,会专注在控制权转交给main方法之前(上图中,标红的2部分),通过这部分,你会明白计算机操作系统是如何理解并加载可执行文件,为它准备好运行前的一切工作.

  • 上图中1:主要涉及到源码接变成机器码,这个过程主要涉及编译器和链接器.包括了语法分析,语义分析,优化等等流程.这也是一个很大的分支.
  • 上图中2:会是我们本文接下来的主要内容,涉及到iOS系统如何读懂加载可执行程序.这个在其他系统也是大同小异.
  • 上图中3:主要是这些加载到内存中的指令,在CPU中是如何被执行.
    不同的 CPU 体系结构的指令集是不一样的,指令的长度和组成都有区别。我们拿三个具体的芯片来看看.
    iPhone5搭载的A6芯片,采用的是ARMv7-A 32-bit ,支持的指令集有 ARM, Thumb-2
    iPhone5s搭载的A7芯片,采用的是ARMv8-A (32/64-bit),支持的指令集有A64,A32, T32
    iPhone8搭载的是A11芯片,采用的是ARMv8‑A compatible,支持的指令集有 A64
    所以当年为了兼容iPhone4,iPhone4s,5,5s,6等等机器,我们的App里是包含了多份二进制程序,不同平台执行不同二进制程序,这就是fat-binary,我们前面已经讲到.
    image.png

#######3.3 操作系统读懂可执行程序
操作系统要完成某一个任务,其实就是执行一系列的指令,并操作对应的数据,如上面所说他们组成了可执行文件.操作系统为了读懂这个文件(可执行程序说白了就是个文件),制定了对应的文件格式,这样它才能读懂这个可执行程序中的指令和数据.这也就说明,为啥在某个OS下可成功运行的程序,在另一个操作系统下却无法执行,即使这俩操作系统跑在同样的硬件配置下.在启动执行前,操作系统要读懂这个可执行程序的内容,加载进入内存中.
所以,如下图,同样的MacBook Pro硬件,可以跑不同的操作系统,但是OSX的软件无法在Windows系统下运行,即使他们基于同样的硬件.


image.png

当然,上面说的这个只是其中的一个原因,还有其他原因导致二进制文件,不能跨OS运行(比如可执行文件调用了特定OS接口).
如果各家操作系统都统一格式那多好....但是商业竞争,专利等等又怎么可能让他们形成统一的标准呢?


image.png

还好,虽然没有统一的标准,但是套路还是很像.下图是几种最流行的文件格式以及他们被哪些平台使用.


image.png

我们看到苹果的可执行文件采用的主要俩种格式
fat-binaries和Mach-O,我觉得你可以把问题简单化,理解fat-binaries就是多个Mach-O的数组文件.接下来我们重点关注Mach-O文件就够了.
虽然上图中写着OS X系统,其实iOS也是一样的.
#######3.4 文件格式到底长成什么样?
我们用MachOView这个工具在mac下面就可以看清楚二进制文件格式到底什么样.
用一个最简单的代码,来生成一个可执行程序,然后用这个工具窥探它.

#include <stdio.h>

int main(int argc, const char * argv[]) {
    // insert code here...
    printf("Hello, World!\n");
    printf("2Hello, World!\n");
    return 0;
}

我们执行一下 gcc -g main.c 最后会生成出来 a.out 就是对应的二进制可执行文件.
接下来要干的事情就是MachOView打开这个二进制文件,呈现在你眼前的是


image.png

是不是有点熟悉的感觉,熟悉的Text,Data,SymbolTable,StringTable,FunctionStarts
每一个单词看起来都很熟悉,组合到一块就不知所措了....
不过没关系,且听我从浅入深,慢慢道来.
上面这个格式,我们可以分成大的三部分,1.header, 2.Load commands, 3. payload;是不是有点TCP/IP协议的感觉.说白了,我们是要让OS懂我们这个文件,那就需要制定规则(标准),让文件遵守,操作系统才能懂,你要是乱来,那就没有规矩,怎么可能有方圆.


image.png

好了,我们先来说header部分,header包含了这个文件的一些概要信息,比如说CPU类型(X86,arm或者其他),大小端;
Load commands说的是接下来要如何加载每个段的内容,每个段是不同的类型,所以采用的加载命令是不一样的,你可以看到这是一个数组.
第三个部分payload,存放的数据也是由一个数组组成,每个里边你大概可以简化理解是一个个segment,比如说_Text_Segment, _Data_Segment等等, segment里边又存放着一个个section.
直接说,payload(对应下图就是Data,我个人感觉用payload不会和其他名字冲突)其实就是个二维数组.我们上一张图,你就可以很好理解了.


image.png

总之一句话:header和load command说明了系统fork进程后,如何加载后面的内容到内存中,让这个进程得以执行对应的程序.
有了前面的bird-view,我们再拉近看一下,我们就慢慢展开这三个部分
########3.4.1 Mach-O文件格式的Header

image.png

具体的字段内容,这里不做展开介绍,网上有很多,总之这个header说明了,这个文件是32位还是64位,支持什么CPU架构,本文件包含了多少个加载命令需要执行等等.
########3.4.2 Mach-O文件格式的Load Commands
header中已经注明总共有多少个load command需要被加载,这个地方就详细地说明了各个command都是什么.比如说有一些是 加载Text,有一些是加载Data的,有一些是加载动态链接器的等等.

image.png

上图只是展示了一些常见的load command类型,如果你需要更加仔细的类型文档,可以到这个地方查看
https://opensource.apple.com/source/xnu/xnu-2050.18.24/EXTERNAL_HEADERS/mach-o/loader.h

下面这个参考了网上,罗列了大部分命令的用途

Command 用途
LC_SEGMENT/LC_SEGMENT_64 将对应的段中的数据加载并映射到进程的内存空间去
LC_SYMTAB 符号表信息
LC_DYSYMTAB 动态符号表信息
LC_LOAD_DYLINKER 启动动态加载连接器/usr/lib/dyld程序
LC_UUID 唯一的 UUID,标示该二进制文件,128bit
LC_THREAD 开启一个MACH线程,但是不分配栈空间
LC_UNIXTHREAD 开启一个UNIX线程
LC_VERSION_MIN_IPHONEOS/MACOSX LC_VERSION_MIN_IPHONEOS/MACOSX
LC_MAIN 设置程序主线程的入口地址和栈大小
LC_ENCRYPTION_INFO 加密信息
LC_LOAD_DYLIB 加载的动态库,包括动态库地址、名称、版本号等
LC_FUNCTION_STARTS 函数地址起始表
LC_CODE_SIGNATURE 代码签名信息

########3.4.3 Mach-O文件格式的Data
到这儿的时候,你是不是发现有点问题,我们之前提到的这张图有点问题,


image.png

LoadCommand并不仅仅是Segment Command啊,还包括其他的,而且下图,绿色部分也不是Segment啊!!!!


image.png

他们怎么和text段,data段,长得那么不像?
哦,是的,刚刚为了不一次性带入那么多信息给你,确实做了简化.其实他们和segment是有共性的,他们也是等待着对应loadcommand来加载他们.
所以更加完整的理解姿势是如下


image.png

这张图的关键信息是command是如何指向对应的segment和section,也就是那些箭头.
这个关系的映射全依赖load command里的信息,例如下面这张图,是LG_SEGMENT_64这个command的具体内容,当中有一个 Number of Sections说明这个segment下面有多少个section


接下来看LoadCommand下具体某个section header的时候, offset&Size恰好指定了他要加载segment所处文件的位置


image.png

至此,我们的整个文件,看起来结构应该是如下,其中的大致关联关系你应该也懂了:
1.文件头 mach64 Header
2.加载命令 Load Commands
3.Data区域(主要由一系列的segment&section组成)
3.1文本段 __TEXT
3.2数据段 __Data
3.3动态库加载信息 Dynamic Loader Info
3.4入口函数 Function Starts
3.5符号表 Symbol Table
3.6动态库符号表 Dynamic Symbol Table
3.7字符串表 String Table

#######3.5 细看segment
我们再拉近看,把重点关注到"段"上,Text段和Data段
前面说到LC_SEGMENT(32-bit架构)/LC_SEGMENT_64(64-bit架构) 将对应的段中的数据加载并映射到进程的内存空间去.
#######3.5.1 如何根据load commands把segment&section映射到内存
我们通过MachOView一样可以看到他具体是如何load的.但是为了简化,我们通过执行
dwarfdump -R a.out 就可以看到对应的映射情况

image.png

当我们执行一个可执行文件,虚拟内存系统会将segment映射到进程的地址空间中。上图看起来是把整个文件都load进去,但是实际上虚拟内存系统做了优化,用一些技巧来规避一次性load这种低效操作.在这里我们先简单的假设VM会将整个文件加载进内存,虽然在实际上这不会发生。
当虚拟内存系统进行映射时,数据段和可执行段会以不同的参数和权限被映射。这些参数和权限在load command已经被规定好.
__TEXT段包含了可执行的代码。它们被以只读和可执行的方式映射。进程被允许执行这些代码,但是不能修改。这些代码也不能改变它们自己,并且这些页从来不会被污染。
__DATA段以可读写和不可执行的方式映射。它包含了可以被更改的数据。
当你仔细看这个的时候,你会发现__TEXT这个segment其实是从fileoff 0的位置开始.从wwdc2016 406 session可以看到,确实也是如此.不过对于大部分读者来说,你可以忽略这个细节.只需要大概知道这些segment会按照规则load到内存里.
#######3.5.2 聊聊这些segment
既然这些segment都会被加载到内存里,那我们就来看看他们都是干嘛的
########3.5.2.1 __PAGEZERO
第一个段是__PAGEZERO。__PAGEZERO段不包含任何section,该段被称为空指针陷阱段,映射到虚拟内存空间的第一页,用于捕捉对NULL指针的引用.在64-bit架构中,这个有4GB大小。这4GB并不是文件的真实大小,但是说明了进程的前4GB地址空间将会被映射为,不能执行,不能读,不能写。这就是为什么在去写NULL指针或者一些低位的指针的时候,你会得到一个EXC_BAD_ACCESS错误。这是操作系统在尝试防止你引起系统崩溃。这是一个全用0填充的段,用于抓取空指针引用。通常不会占用磁盘空间 (或内存空间),因为它运行时映射为一群0.

########3.5.2.2 __TEXT
__TEXT:本段只有可执行代码和其他只读数据。
我们要注意一下,大小写问题,__TEXT表示的是段, 小写__text则是段中的区(section)
__text:本section是编译后得到的可执行机器码。
_stubs和_stub_helper是给动态链接器用的。这允许动态链接的代码延迟链接。
__cstring:表示代码里的字符串常量。链接器在生成最终产品时会清除重复语句。


image.png

而你在__text中用到这边常量,其实是链接了指向这个内容的一个指针.
__stubs:间接符号存根。该区存放的是二进制文件中未定义符号的占位符.正如他的名字"桩",他只是预先拿来占位的,内容指向了别的地方.
__stub_helper:这算是一个工具,可以简单理解成另一个程序,他能够告诉你,"桩"最后是指向了哪儿.
__unwind_info:一个紧凑格式,为了存储堆栈展开信息供处理异常。此节由链接器生成,通过“__eh_frame”里供OS X异常处理的信息。
__eh_frame: 一个标准的节,用于异常处理,它提供堆栈展开信息,以DWARF格式。

########3.5.2.3 __DATA
_DATA段包含了可读写数据,可读可写的特性让动态链接成为可能.链接有几种方式,其中最主要有俩种:lazy binding & none-lazy binding
lazy binding顾名思义,就是在代码在runtime的时候,才去寻找对应的调用地址,解析出来,然后再执行.
none-lazy binding则是在加载程序的时候,就把这些调用地址绑定后,后续run time直接调用.
在这个例子中,我们看到的就是:
__nl_symbol_ptr:非延迟导入符号指针表。在编译的时候,这里的指针们指向解析助手。
__la_symbol_ptr:延迟导入符号指针表。
到这里,结合我们之前的代码,我们来说一下代码是如何动态链接到printf这个函数.
我们先从 _text代码开始,可以看到有俩条跳转指令


image.png

这俩条跳转指令都跳转到 0x100000f76,那我们就到这个地址看看


image.png

image.png

我们通过打断点,调试可以看到,这是让跳转到"Lazy Symbol Pointers"指向的地方(请仔细品位这句话),于是乎我们跑去看Lazy Symbol Pointers到底存放了什么东西,我靠, 里边居然指向了_stub_helper,因为Lazy Symbol Pointers首次运行到这个动态链接的方法,他自己也不知道要去哪儿执行,就告诉你说,你去找助手吧,让助手告诉你. 于是助手做了一下参数压栈,通过binder去查找这个动态库的地址.


image.png

在完成这个流程后,把Lazy Symbol Pointers里对应的实际方法调用地址给修改了,后续执行到这个方法再也不用去找助手问地址了.
看到这儿我估计你还有点迷糊,我们通过下面这张图会清晰一些


image.png

具体的细节,可以参考这篇文章<Dynamic Linking of Imported Functions in Mach-O> ,这篇文章写得真心好.
https://www.codeproject.com/Articles/187181/Dynamic-Linking-of-Imported-Functions-in-Mach-O
到了这儿,和linux下面的动态链接延迟绑定基本上差不多.具体可参考
《程序员的自我修养》第200页
#######3.5.3 这些部件是如何组装在一起工作

  1. 系统内核为这个二进制文件先fork一个进程
  2. 调用execve让系统内核加载并执行这个二进制文件
    2.1 这个时候内核根据Mach-O的mach_header进行合法性校验.
    2.2 根据load command加载此二进制文件
    2.3 根据load command中的LC_LOAD_DYLINKER命令加载动态链接器(dyld)
    2.4 系统内核 控制权转移到 动态链接器(dyld)
  3. dyld接手控制权
    3.1 dyld加载system framework以及一些dylib到内存中.其实这些动态库也是Mach-O文件.
    3.2 把动态库链接到一起(这个也是个大话题,这里展开的话,这篇文章基本说不完) fix-ups 地址修正,让各个部件关联起来.确定地址被置于“_nl_symbol_ptr”和“__got”中.
    3.3 初始化方法 (这里多数事情都是递归的,从底向上的方法调用,因为初始化的时候要确保,他所依赖的已经初始化)
  4. 完成以上这个工作之后,就把控制权交给main方法.

#######4. End
如果你看到这,可能觉得好像这些底层的东西我们现在都不需要关注了.其实并不是这样.当你碰到问题,体会到书到用时方恨少时,你就知道有时候问题解决并不是简单耍机灵就可以.比如怎么样让应用程序启动加载更快;居然有坏人调用了APP端的加密算法来探测后台接口等等.
写到这,已经有点体力不支了.所以就来个简单结束吧.谢谢你看到这,希望有所收获.不足请指正.


image.png

最后这里附上refer的内容链接:
http://49be7714.wiz03.com/share/s/19LDsk0qDkfy2pziXv12a6L72jQmC90z5QVJ2GlBAh1uMBcF

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容