美文网首页
带你走入苹果的世界

带你走入苹果的世界

作者: __Adan__ | 来源:发表于2018-07-15 21:49 被阅读0次

    带你走入苹果的世界

    • 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

    相关文章

      网友评论

          本文标题:带你走入苹果的世界

          本文链接:https://www.haomeiwen.com/subject/flkmpftx.html