理解iOS app的编译,运行过程

语言: CN / TW / HK

theme: channing-cyan highlight: arduino-light


前言

一年都没怎么写iOS的文章。最近有一些知识学习了完以后总觉得零零散散的,打算以这一篇文章作为开篇,系统地记录一些所学习的iOS知识。

本篇文章的基础框架已经初步确定了,不会提及到特别深入的知识点。本篇文章以APP是一个app的编译和运行过程的一个概述,为后续的文章提供一个基本的分析思路。之后时不时会随着研究内容的时候补充一些细节。

一、 系统架构

在谈iOS的运行过程前,我们首先需要了解iOS的系统。其实iOS和MacOS一脉相承,总体的结构设计都如下所示

底层都是Darwin,是一个操作系统。Darwin结构如下

其中Darwin的内核是XNU。然后XNU里面使用了BSD和Mach。

到这里可以基本理解为MacOS和iOS是一个类Unix系统,BSD也兼容Posix这个标准,所以我们能在iOS开发中使用pthread,在Mac上终端处理unix命令的原因。 这里关于iOS和MAC OS架构的描述就先说这么多,如果有兴趣了解更深一些的话,可以查看《深入了解MACOS&iOS操作系统》一书。

先介绍这个系统架构,主要是为了让大家了解,在后续的文章中用到的方法有些是Mach这一层的提供的。

二、 Mach-O

Mach-O是什么?

我们在打开了一个App,可以理解在手机的操作系统上运行了一个App进程。而进程是特殊文件在内存中加载得到的结果,这种特殊文件必须是操作系统可理解的。

在unix中,可运行的程序会编译成一个二进制文件,unix对于二进制文件,有一个标准的格式叫ElF。苹果爸爸偏偏不依,定制了一个属于苹果的格式,这个就是Mach-O格式,全称Mach-Object。

但是Mach-O不仅仅是只是可执行文件的格式标准,还是macOS和iOS中一些其他文件的标准格式。常见的Mach-O格式有下面几种 * 可执行文件 * 库文件(.a .dylib .framework等) * dyld(动态连接器) * dsym文件(符号表) * 目标文件(编译输出的.0文件)

Mach-O总的来说可以说是苹果的一种文件标准的格式。

以下以可执行文件为重点去讲述Mach-O

可执行文件

我们开发的app,编译打包后,就会生成一个可执行的,Mach-O格式的二进制文件。

对于macOS和iOS中的App,其生成可执行二进制文件从支持的架构分为两种 1. 单架构的二进制文件 2. 胖二进制文件

单架构的二进制文件就是运行于单个架构(如x86,ARM64等)的二进制文件。而胖二进制就是一个二进制文件里面包含多个单架构的二进制文件。

Mach-O内容

以mac中的计算器App为例,用MachOView查看如下

截屏2022-01-03 上午12.09.33.png

可以看到这是一个Fat Binary类型的可执行文件,也就是我们平常说的胖二进制文件。里面包含了两个Executable二进制文件,每一个Excutable里面的格式可以参照单个架构的Mach-O的格式。当我们点击app的时候,就会从中选合适的Excutable加载到内存中。

单个Excutable的格式总体如下

对于上面的图片,单架构的可执行文件Mach-O文件格式的内部结构总体如下

从图中可知,Mach-O大概分为三个部分 * mach-header * load commands * Data

下面简单的说下各个部分的作用是什么。

1. mach_header

mach_header 位于 Mach-O 文件的头部,有以下作用 * 标明Mach-O 的格式 * 标明文件类型,CPU 架构等信息 * 标明load commands数量等 * 其他

对于计算器App,在MachOView中整体如下

截屏2022-01-03 上午12.01.28.png

2. load commands

这一部分存储的是加载指令,直到操作系统该如何把可执行文件加载到内存中。有以下作用 * 指导data部分的数据应该怎么加载 * 指导dyld加载哪些库 * 指明符号表地址 * 其他

对于计算器App,在MachOView中整体如下

3. Data

这一部分存储的数据区域,包含程序运行的一切数据,表明数据加载到什么位置。部分数据如 * 代码数据 * 运行时的类数据 * 符号表,动态符号表 * 其他数据

对于计算器App,在MachOView中整体如下

对于Data区域的文档,可以按照Segment和section划分。Segment和section的关系有点像操作系统中的段和页的概念。 这里section可以简单为Segment的子节点,而Data中的数据是以Segment为单位划分的。常见的Segment有 1. __PAGEZERO。一段随机大小,不可访问的空间 2. __TEXT。代码区 3. __DATA。数据区域 4. __LINKEDIT;包含了方法和变量的元数据,代码签名等信息

三、App编译

当我们在Xcode上编写代码,如何生成一个可执行的文件呢?对于一个通用编译流程,如下

编译流程.jpg

在Xcode中使用LLVM架构流程化处理上面的步骤,我们无需操心。但是作为开发者,我们可以理解一下LLVM的基本的流程。

LLVM的总体架构如下所示,分为前端,中间层,后端等

3008243-b517c768f5a97607.png

在iOS中,结合编译流程与LLVM,各个步骤如下。

1. 预处理

在预处理的阶段中,根据语言选择编译器前端。如对于OC,会调用编译器前端Clang首先预处理我们代码。这一步会进行将宏替换到代码中、删除注释、处理预编译命令等工作

2. 编译、优化

在这个阶段,在编译器前前端处理以下步骤 1. 词法分析 2. 语法分析 3. 静态分析 4. 为每一个文件生成中间代码IR

3. 汇编

拿到第二个步骤的IR文件后,LLVM中间层LLVM Optimizer就会对每一个IR文件进行一些优化,如尾递归优化、循环优化、全局变量优化。 优化完成后,LLVM会调用汇编生成器将每一个IR文件转化成汇编代码。再根据部署的平台选择对应的编译器后端,将汇编转变生成产物,也就是一个个.o文件了(二进制文件)。

4. 链接程序

在上面步骤中,生成了一个个.o文件。在我们正常开发中,一个文件通常会引用别的文件或是库,链接过程会将多个目标文以及所需的库文件链接成最终的可执行文件,也就是Mach-O的文件。对于动态库,链接构成中不会把库加载进来,而是会把动态库的相关信息写入到可执行文件中。

经过上述步骤,我们开发的代码会生成一个Mach-O格式的可执行文件。

四、 App运行过程

1. 加载到内存

当我们点击App的时候,操作系统就会在手机上开辟一段虚拟内存空间,然后加载Mach-O格式的二进制文件。如下

截屏2022-01-03 上午12.58.47.png

其中图中类似于管道的部分可以理解为系统为程序分配的内存空间。然后把我们的可执行文件加载到内存中。

注意图中有一个PAGEZERO的区域,这个是苹果的ASLR技术,也就是地址空间布局随机化。从【App编译】小节我们可以知道,当我们编译出一个App的时候,生成的Mach-O文件的内容地址是固定的。

ASLR技术在每次运行App的时候,都会加上一个PAGEZERO,PAGEZERO的大小都是随机的。加上PAGEZERO以后,Mach-O里面的地址都会需要加上相对的偏移,这样可以增加App的攻击难度。

2. 调用dyld进程加载dylib

把Mach-O当作一个image加载到虚拟内存后,会启动dyld进程,根据load commonds里面的加载命令,加载需要的动态库。以Mac上的计算器app为例,加载命令在Mach-O如下

截屏2022-01-03 下午9.15.04.png

dyld根据这些加载命令把这些第三方库加载到虚拟内存中,加载后如下

截屏2022-01-03 下午9.13.53.png

3. 修复处理

Rebase

在编译过程中,Mach-O文件的文件引用。由于签名和ASLR技术加载了PAGEZERO区域,所以文件里面的内容都会有偏移。这时候就需要修正文件里面的指针引用。Rebase过程就是对于文件里面的指针一个修正过程。

Bind

在编译过程,可执行文件对动态库的方法调用是只声明了符号。在调用dyld把这些动态库在加载到内存后,需要去相应的动态库中链接对应的方法,找到其指针,然后对可执行文件中的指针执行修复。Bind过程中就是把这些指向动态库的指针进行修复。

Objc

大部分修复操作都已经在Rebase和Bind中做了,这一部分的工作主要是做Objc运行时的处理。如加载category里面的方法到类中,这一部分在苹果开源的objc库中可以找到相关内容,在笔者前面的文章也提及到过(ps:早期文章写的比较随意,大家可以结合文章里面的注释看源码)。

4.Initalizers

这一个阶段基本就是执行c++的初始化,OC load方法。执行完以后。由dyld调用程序中的main函数

5.执行main函数

执行main函数,开始一个RunLoop,保持程序的不断运行,然后开始执行我们程序中AppDelegate中的代码。

按照苹果官方的PPT,加载可程序后执行的总体步骤如下

截屏2022-01-03 下午10.05.44.png

小结

笔者才疏学浅,这一篇文章是参考了其他博客,结合自身的理解,得出的一篇关于iOS的编译和运行过程的一个概述,如果有纰漏的地方,望各位读者指正。

参考链接

Mach-O文件结构

iOS App - 从编译到运行

深入浅出让你理解什么是LLVM

优化启动时间

clang常用命令学习