编译过程
iOS应用程序的编译过程涉及多个步骤,从源代码到最终生成可运行的应用程序。
1. 源代码编写
开发者在Xcode中编写应用程序的源代码。源代码通常包括Objective-C、Swift、C或C++代码文件,以及资源文件(如图像、声音文件等)。
2. 预处理
对于C和Objective-C代码,预处理器(cpp)会处理指令,例如#include、#define等。预处理器生成预处理后的源代码,主要包括以下内容:
- 处理宏定义(
#define) - 展开头文件(
#include) - 条件编译(
#if、#ifdef)
3. 编译
编译器(clang)将预处理后的源代码转换为中间表示(Intermediate Representation, IR)。这一步主要包括以下内容:
- 语法分析(Lexical Analysis):将源代码分解成标记(tokens)
- 语法分析(Syntax Analysis):将标记转换为抽象语法树(AST)
- 语义分析(Semantic Analysis):检查代码的语义正确性,例如类型检查
- 生成中间表示(IR)
4. 优化
编译器对中间表示进行各种优化,生成优化后的中间表示。这些优化可以包括:
- 常量折叠(Constant Folding)
- 死代码消除(Dead Code Elimination)
- 循环优化(Loop Optimization)
- 内联函数展开(Function Inlining)
5. 生成目标代码
编译器将优化后的中间表示转换为目标代码(机器代码)。这一步生成的是特定于目标架构(如arm64)的汇编代码。
6. 汇编
汇编器(as)将汇编代码转换为目标文件(Object File),这是机器可执行的二进制文件。每个源代码文件都会生成一个对应的目标文件。
7. 链接
链接器(ld)将多个目标文件和库文件链接在一起,生成最终的可执行文件。链接器的主要任务包括:
- 符号解析:将所有的符号(如函数和变量)解析为具体的内存地址
- 重定位:调整目标文件中的地址,以便在最终的可执行文件中正确引用
- 合并:将多个目标文件和库文件合并成一个单一的可执行文件或动态库
8. 生成Mach-O文件
在iOS中,最终生成的可执行文件是Mach-O文件格式(Mach Object)。Mach-O文件包含以下部分:
- Header:文件头,包含文件类型、架构类型等信息
- Load Commands:描述文件的布局和如何加载
- Segments和Sections:包含代码、数据、符号表等
- Symbol Table:符号表,包含所有符号的信息
9. 代码签名
为了确保应用程序的完整性和来源的可信性,Xcode会对生成的应用程序进行代码签名。代码签名的过程包括:
- 生成应用程序的哈希值
- 使用开发者的私钥对哈希值进行加密,生成签名
- 将签名附加到应用程序的Mach-O文件中
10. 打包成IPA
Xcode将签名后的应用程序打包成IPA(iOS App Archive)文件。IPA文件是一个压缩包,包含应用程序的可执行文件、资源文件、Info.plist文件等。
11. 部署和运行
最终生成的IPA文件可以部署到iOS设备或上传到App Store。用户从App Store下载并安装应用程序,iOS设备会解压IPA文件,并通过动态链接器(dyld)加载和运行应用程序。
iOS应用程序的编译过程包括从源代码编写到最终生成可执行文件的多个步骤。每个步骤都有特定的工具和任务,从预处理、编译、优化、生成目标代码、链接、生成Mach-O文件、代码签名到打包成IPA文件。理解这些步骤有助于开发者更好地调试和优化他们的应用程序。
Mach-O
是iOS/macOS上可执行文件的格式,常见Mach-O格式的文件:.a/.dyib/.framework/可执行文件/dyld(动态链接器)/.dsym(符号文件) 等
Mach-O 内部结构
若是把MachO整个文件看成是一本书的话。Header相当于书的序;Load commands相当于书的目录;Data相当于书的具体内容。当某个执行文件是由多个MachO文件组成时,内部是一个个MachO叠在一起的。
- Data段里面会有两张表-懒加载表和非懒加载表
- 懒加载表:存放系统函数指针
- 非懒加载表:自己写的函数指针
- 在程序启动的时候 Mach-O 文件会被 DYLD (动态加载器)加载进内存。加载完 Mach-O 后,DYLD接着会去加载 Mach-O 所依赖的动态库。
Mach-O 文件结构
Mach-O文件由多个部分组成,每个部分都承载着特定类型的信息:
-
Header:文件的开头部分,包含文件类型(可执行文件、动态库、目标文件等)、架构类型(例如x86_64、arm64)和加载命令的数量等信息。
-
Load Commands:一系列指令,描述了文件的布局和如何加载文件。它们包括段信息、动态库依赖、符号表位置等。
-
Segments and Sections:
- Segments:文件的主要部分,每个段包含一个或多个部分(section)。常见的段包括
__TEXT(包含代码)、__DATA(包含可变数据)、__LINKEDIT(包含符号和动态链接信息)。 - Sections:更细粒度的部分,属于某个段。比如,
__TEXT段中的__text部分包含可执行代码,__DATA段中的__data部分包含初始化的数据。
- Segments:文件的主要部分,每个段包含一个或多个部分(section)。常见的段包括
-
Symbol Table:包含符号信息,用于链接和调试。符号可以是函数名、全局变量名等。
-
String Table:包含符号名称的字符串,用于符号解析。
Mach-O 文件的加载过程
- **加载器(Loader)**读取Mach-O文件的Header和Load Commands,以确定文件的布局和依赖关系。
- **动态链接器(dyld)**根据Load Commands加载所需的动态库,并处理符号解析和重定位。
- dyld将可执行文件和动态库映射到进程的虚拟内存中,并进行必要的初始化。
- dyld将控制权交给应用程序的入口点(通常是
main函数)。
优点和特点
- 模块化:通过Segments和Sections的划分,Mach-O文件格式非常灵活,可以轻松扩展和修改。
- 动态链接:支持动态库,使得多个应用程序可以共享相同的代码库,减少内存占用和磁盘空间。
- 跨平台:支持多种架构(例如x86_64、arm64),适用于不同的硬件平台。
Mach-O工具
- otool:用于显示Mach-O文件的结构和内容信息。
- nm:用于列出Mach-O文件中的符号表。
- dwarfdump:用于显示DWARF调试信息。
- ld:链接器,用于生成Mach-O可执行文件或动态库。
- dyld:动态链接器,负责加载和链接Mach-O文件。
示例
使用otool查看一个Mach-O文件的基本信息:
sh
复制代码
otool -hV /path/to/executable
此命令会显示Mach-O文件的Header信息和Load Commands。
总结来说,Mach-O文件格式是macOS和iOS操作系统中的核心文件格式,支撑了应用程序和系统组件的加载和执行。通过其模块化和灵活性,Mach-O文件格式能够高效地管理和加载各种类型的二进制文件。
dyld
在iOS中,dyld(Dynamic Link Editor)是负责动态链接和加载可执行文件及其依赖的动态库的系统组件。它是一个动态链接器,类似于macOS上的dyld,也是从macOS移植到iOS上的关键部分。以下是一些有关dyld的关键点:
- 加载可执行文件:当用户启动一个应用程序时,dyld负责加载应用程序的可执行文件到内存中,并解析其依赖的所有动态库。这些动态库可以是系统提供的库,也可以是应用程序自带的库。
- 符号解析:dyld在加载过程中会解析符号,这包括全局变量和函数。它会确保每个符号都能被正确解析和链接,这样在运行时,程序可以正确地调用这些符号。
- 重定位:为了让代码能够在内存中任何地方运行,dyld会进行重定位。重定位是指将代码中用到的符号地址更新为实际内存中的地址。
- 依赖管理:dyld会处理动态库之间的依赖关系,确保所有依赖的库都被正确加载。它会根据库的依赖树递归地加载所有需要的库。
- 环境变量:dyld可以通过一些环境变量进行配置,例如
DYLD_LIBRARY_PATH、DYLD_FRAMEWORK_PATH等,这些环境变量可以影响库的搜索路径和加载行为。 - 安全性:为了确保系统和应用程序的安全,dyld有许多安全机制。例如,它会验证动态库的签名以防止加载未经授权的代码,还会进行地址空间布局随机化(ASLR)以增加攻击难度。
- 性能优化:dyld使用多种技术来优化应用程序的启动时间。例如,它会缓存一些已解析的符号信息,以减少重复解析的开销,还会利用共享缓存(shared cache)来加快常用系统库的加载速度。
APP启动过程
iOS应用程序的启动过程可以分为几个关键阶段,每个阶段都有特定的任务和目标。以下是一个简化的描述:
-
加载可执行文件:
- 当用户点击应用程序图标时,系统会创建一个进程,并启动应用程序的可执行文件。
dyld(动态链接器)会加载可执行文件和应用程序所依赖的动态库。
-
dyld 处理:
dyld负责解析可执行文件和动态库中的符号。- 进行重定位,将符号地址更新为实际内存地址。
- 处理依赖关系,确保所有依赖的动态库都被正确加载。
加载共享缓存(UIKit、Foundation) -> 插入库:即将app本身的库加载(比如:逆向自己生成的framework动态注入就是在这个时候) -> 内部调用
_dyld_objc_notify_register(&map_images, load_images, unmap_image); ->load_images`这个方法。内部就是load方法的加载,先主类,后分类; -
执行应用程序入口:
- 加载过程完成后,
dyld将控制权交给应用程序的main函数。 main函数是应用程序的入口点,它是由Xcode生成的标准main.m文件中的UIApplicationMain函数调用。
- 加载过程完成后,
-
UIApplicationMain 函数:
UIApplicationMain函数创建应用程序对象(UIApplication或其子类)和应用程序委托(App Delegate)。- 设置应用程序的主运行循环(Run Loop),开始处理事件。
-
App Delegate:
- App Delegate是应用程序的代理,它处理应用程序的生命周期事件,如启动、进入前台、进入后台等。
application:didFinishLaunchingWithOptions:是App Delegate中的一个关键方法,当应用程序启动完成并准备好运行时,会调用此方法。
-
设置用户界面:
- 在
application:didFinishLaunchingWithOptions:方法中,开发者通常会配置初始的用户界面,例如创建和设置窗口(UIWindow)和根视图控制器(Root View Controller)。
- 在
-
显示窗口:
- 一旦设置了初始界面,应用程序会将窗口显示在屏幕上,并开始响应用户输入和其他系统事件。
整个过程通常在短短几百毫秒内完成,以确保用户在点击应用程序图标后能够迅速进入应用程序。这个启动过程的每个阶段都至关重要,任何一个阶段出现问题都可能导致应用程序无法正常启动。
程序加载其他点
-map_images 之 _read_images 之 static Class realizeClassWithoutSwift(Class cls)
- 这一步会读取class的data(),将ro写入到rw,即将method_list_t、property_list_t、protocol_list_t写进去;
realizeClassWithoutSwift(Class cls)这步是递归调用,父类和元类
-
methodizeClass 之 attachLists()以二维数组的方式倒序插入,即后加的在前面 -
懒加载类和非懒加载类
- 实现了load方法就是非懒加载类
- 未实现了load方法就是懒加载类
共享缓存机制
苹果为了节约内存以及提高加载速度,将系统的动态库(UIKit,UIFoundation...)放在内存的一块特殊位置,然后将这块内存共享给其他的应用。这块区域就是动态库共享缓存(dyld shared cache)
PIC技术:位置代码独立
编译的时候,符号地址。dyld加载的时候,将符号地址和真实地址绑定。
- 由于共享缓存的原因,系统函数地址无法在编译时期确定
- 所以APPLE使用PIC技术在MachO文件DATA段建立两张表,懒加载表(包含系统的函数)和非懒加载表(自己工程里面写的函数)
- 首次调用懒加载表内的函数时,DYLD会对该函数进行绑定:符号表绑定
- 而非懒加载表内部是自己代码里写的函数指针,编译时期就确定了
共享缓存机制和PIC技术的原因,
fishhook可以hook系统自带的函数,因为系统的的函数是存放在懒加载表中的,在dyld去进行绑定的时候,fishhook可以修改符号表的绑定从而hook函数。而工程自己写的函数是在非懒加载的表中的,在编译时期就确定了,所以无法被hook
ASLR技术(地址空间布局随机化):
MachO文件加载的时候会对首地址加上一个随机的偏移值,防止被逆向。
rebase & bind
由于ASLR技术(地址空间布局随机化),在iOS或macOS应用程序的加载过程中,dyld会依次进行rebase和bind操作:
- Rebase:首先,dyld会调整所有基于编译时地址的指针,使它们适应ASLR带来的地址变化。
函数的会先放在__Data -> __stub_helper,等待进程被加载 - Bind:接下来,dyld会解析所有符号引用,并将它们绑定到实际的内存地址。当进程被加载后函数从__stub_helper中转移到了
__Data -> __la_symbol_prt
rebase用于调整内存地址以适应ASLR,bind用于解析符号并绑定实际地址。这两个操作共同确保了应用程序在不同运行环境中的正确加载和执行。
相关问题
- 1.Mach-O是如何找到系统的函数地址?或者说Mach-O文件是如何链接外部函数的呢?
- Moch-O文件会先在__DATA段建立一个空指针(符号地址)
- 在DYLD的时候,会将上面的空指针指向具体某个系统函数(外部函数):即将符号地址和真实地址绑定
- 2.fishook原理 将上面懒加载表里面指向系统函数的指针重新绑定(rebind_symbols符号表重绑定);而自己写方法函数指针在编译时期就已经写在了非懒加载表内所以无法hook