iOS编译链接原理

前言:

计算机语言分为机器语言,汇编语言,高级语言。
可以将高级语言分为两种:1,编译语言和解释型语言(直译式语言)。


编译链接概述
编译型语言(一次性翻译)

编译型语言的程序只要经过编译器编译之后,每次运行程序都可以直接运行,不需要再次'翻译',如oc,swift等
  优点:执行速度快。
  缺点:可移植性差,因为编译需要对操作系统的库做出链接,所以程序运行时需要用到特定的系统库

解释型语言(逐步进行解释执行)

解释语言编写的程序在每次运行时都需要通过解释器对程序进行动态解释和执行,如php,javascript等
即解释一条代码,执行一条
  优点:可移植好,因为只需要各种系统有解释器便可运行,不需要乱七八糟的系统库支持
  缺点:执行速度慢,因为相比直接执行,多了一个翻译过程。
而java比较特殊,java既可以说成是编译型语言也可以说成是解释型语言,因为是先经过编译成.class在执行生成可行性文件,运行时经过逐行解释成机器语言。

编译链接过程:

1,预处理:macro 宏, import 头文件替换及处理其他的预编译指令,产生.i文件。(都是以#号开头)

2,编译:把预处理完的一系列文件进行一系列词法、语法、语义分析,并且优化后生成相应的汇编代码,产生.s文件;

3,汇编:汇编器将汇编代码生成机器指令,输出目标文件,产生.o文件(根据汇编指令和机器指令的对照表一一翻译就可以了);

4,链接:在一个文件中可能会到其他文件,因此,还需要将编译生成的目标文件和系统提供的文件组合到一起,这个过程就是链接。经过链接,最后生成可执行文件。

经过编译和链接,才会把写的代码转换成计算机能识别的二进制指令。

iOS编译

编译器:用来把源文件转换为更低级的语言,xcode自带的编译器自动去完成这部分的实现,xcode对于oc和swift使用的编译器前端不同,本文只对oc进行阐述,而xcode使用的clang前端编译器的作用就是把源代码转化为更为低级的LLVM IR,然后这个LLVM IR是操作系统无关的,然后后端编译器LLVM通过这个中间语言来进行下一步二进制文件的产出。这要得益于LLVM的三层架构,假如你需要增加一种语言,只需要增加一种前端,如过你需要增加一种处理器架构,也只需要增加一种后端。

编译器概述

在 xcode 中可以通过 Build phases,Build settings以及 Build rules来进行控制。

Build phases主要是用来控制从源文件到可执行文件的整个过程的,应该说是面向源文件的,包括编译哪些文件,以及在编译过程中执行一些自定义的脚本。

Build rules主要是用来控制如何编译某种类型的源文件,假如说相对某种类型的源文件进行特定的编译,那么就应该在这里进行编辑了。同时这里也会大量的运用一些 xcode 中的环境变量,完整的官方文档在这里:Build Settings Reference

Build settings则是对编译工作的细节进行设定,在这个窗口里可以看见大量的设置选项,从编译到打包再到代码签名的设置。

编译器前端:

Clang编译器前端的任务是进行:语法分析,语义分析,生成中间代码(intermediate representation )。在这个过程中,会进行类型检查,如果发现错误或者警告会标注出来在哪一行。Clang 是一个C、C++、Objective-C和Objective-C++编程语言的编译器前端。


编译器前端

公用优化器:将生成的中间文件进行优化,去除冗余代码,进行结构优化。

编译器后端:

后端将优化后的中间代码再次转换,变成汇编语言,并再次进行优化,最后将各个文件代码转换为机器代码并链接。链接是指将不同代码文件编译后的不同机器代码文件合并成一个可执行文件。

LLVM是构架编译器(compiler)的框架系统,以C++编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time),对开发者保持开放,并兼容已有脚本。

LLVM机器码生成器会针对不同的架构,比如arm64等生成不同的机器码。

编译器后端

代码下载:

llvm的源码地址:http://releases.llvm.org/
clang源码地址:https://opensource.apple.com/tarballs/clang/

iOS 项目编译过程简介:
  1. 写入辅助文件:将项目的文件结构对应表、将要执行的脚本、项目依赖库的文件结构对应表写成文件,方便后面使用;并且创建一个 .app 包,后面编译后的文件都会被放入包中。

  2. 运行预设脚本:Cocoapods 会预设一些脚本,当然你也可以自己预设一些脚本来运行。这些脚本都在 Build Phases 中可以看到。

  3. 编译文件:针对每一个文件进行编译,生成可执行文件 Mach-O,这过程涉及 LLVM 的完整流程,包括前端、优化器、后端。

  4. 链接文件:将项目中的多个可执行文件合并成一个文件。

  5. 拷贝资源文件:将项目中的资源文件拷贝到目标包;

  6. 编译 storyboard 文件:storyboard 文件也是会被编译的;

  7. 链接 storyboard 文件:将编译后的 storyboard 文件链接成一个文件;

  8. 编译 Asset 文件:我们的图片如果使用 Assets.xcassets 来管理图片,那么这些图片将会被编译成机器码,除了 icon 和 launchImage;

  9. 运行 Cocoapods 脚本:将在编译项目之前已经编译好的依赖库和相关资源拷贝到包中。

  10. 对包进行签名

  11. 完成打包

在上述流程中:2 - 9 步骤的数量和顺序并不固定,这个过程可以在Build Phases中指定。

编译过程查看

clang是xcode自带的编译器前端,所以可以通过mac的终端进行相应信息的查看

cd 项目路径

clang命令 项目下的文件

常用命令

  • 查看编译源文件需要的几个不同的阶段:
clang -ccc-print-phases main.m
  • 查看oc的c实现:
clang -rewrite-objc main.m
  • 查看操作内部命令:
clang -### main.m -o main.m
  • 预处理完成后就会进行词法分析,这里会把代码切成一个个 Token,比如大小括号,等于号还有字符串等:
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
  • 然后是语法分析,验证语法是否正确,然后将所有节点组成抽象语法树 AST:
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
  • 完成这些步骤后就可以开始IR中间代码的生成了,CodeGen 会负责将语法树自顶向下遍历逐步翻译成 LLVM IR,IR 是编译过程的前端的输出后端的输入:
clang -S -fobjc-arc -emit-llvm main.m -o main.ll
  • 生成汇编
clang -S -fobjc-arc main.m -o main.s
  • 生成目标文件
clang -fmodules -c main.m -o main.o
  • 生成可执行文件
clang main.o -o main

*****补充

  • 指定真机
xcrun -sdk iphoneos + 命令
  • 指定模拟器
xcrun -sdk iphonesimulator + 命令
  • 指定SDK版本
xcrun -sdk iphonesimulator10.3 + 命令

编译器处理过程:


终端查看编译过程

1,预处理。

2,编译生成IR(编译生成中间代码)。

3,汇编器生成汇编代码。

4,生成机器码

5,链接

6,生成Image,也就是最后的可执行文件。

预处理阶段:

处理包括宏的替换,头文件的导入,以及类似#if的处理

预处理阶段

此处代码比较长。。。。。省略很多~~~

编译阶段:

一、词法分析-Lexical Analysis

在compiler阶段,首先代码文本都会从 string 转化成特殊的标记流。将代码切成一个个 token,比如大小括号,等于号还有字符串等。是计算机科学中将字符序列转换为标记序列的过程。每一个标记都包含了对应的源码内容和其在源码中的位置,如果编译过程中遇到什么问题,clang 能够在源码中指出出错的具体位置。clang -Xclang -dump-tokens +文件命令,查看


查看编译代码

二,语法分析 - Semantic Analysis

之后上面的标记流会解析生成抽象语法树,我们可以使用 clang -Xclang -ast-dump -fsyntax-only ViewController.m 来展现解析这个过程

生成语法树:


终端输出语法树

三、静态分析 - Static Analyzer

一旦编译器把源码生成了抽象语法树,编译器可以对这棵树做分析处理,以找出代码中的错误,比如类型检查:即检查程序中是否有类型错误。例如:如果代码中给某个对象发送了一个消息,编译器会检查这个对象是否实现了这个消息(函数、方法)。此外,clang 对整个程序还做了其它更高级的一些分析,以确保程序没有错误。

三、输出汇编代码

我们可以使用下面的命令让clang输出汇编代码:

clang -S -fobjc-arc main.m -o main.s

汇编阶段:

汇编器:

编器将可读的汇编代码转换为机器代码。它会创建一个目标对象文件,一般简称为 对象文件。这些文件以 .o 结尾。如果用 Xcode 构建应用程序,可以在工程的 derived data 目录中,Objects-normal 文件夹下找到这些文件。

链接阶段:

链接器解决了目标文件和库之间的链接。例如上面汇编代码中 callq _printf, printf() 是 libc 库中的一个函数。无论怎样,最后的可执行文件需要能需要知道 printf() 在内存中的具体位置:例如,_printf 的地址符号是什么。链接器会读取所有的目标文件 (此处只有一个) 和库 (此处是 libc),并解决所有未知符号 (此处是 _printf) 的问题。然后将它们编码进最后的可执行文件中 (可以在 libc 中找到符号 _printf),接着链接器会输出可以运行的执行文件。

过程回顾:
编译过程概述
xcode执行完整步骤:

生成.app文件

把Entitlements.plist写入到DerivedData里,处理打包的时候需要的信息(比如application-identifier)。

创建一些辅助文件,比如各种.hmap (headermap是帮助编译器找到头文件的辅助文件:存储这头文件到其物理路径的映射关系)

编译.m文件,

生成.o文件。

链接动态库,生成一个mach o格式的可执行文件。

编译assets,

编译storyboard,

链接storyboard,

对App签名

生成 .app

Xcode查看截图

应用场景:

1,符号表和链接

在经过编译之后,clang会生成一个dsym文件。dsym文件中,存储了16进制的函数地址映射。通过dSYM文件,我们就可以由地址映射到具体的函数位置。


926487-20180809152214650-1863333822.png

1,帧编号—— (数字从大到小为发生的顺序)
2,二进制库的名称
3,调用方法的地址
4,第四列分为两个子列,一个基本地址和一个偏移量。第一个数字指向文件,第二个数字指向文件中的代码行。

2,clang的静态分析器

静态分析的截图

点击xcode的product-Analyze。
1,逻辑缺陷,例如访问未初始化的变量或空指针的解引用
2,内存管理缺陷,如内存泄露
3,无用存储缺陷(永不会被访问的变量)
4,因未遵从项目用到的框架(frameworks)或类库(libraries)所规范而导致的API使用缺陷

3,工具的使用

Xcode 在编译 iOS 项目的时候,使用的正是 LLVM,其实我们在编写代码以及调试的时候也在使用 LLVM 提供的功能。例如代码高亮(clang)、实时代码检查(clang)、代码提示(clang)、debug 断点调试(LLDB)。这些都是 LLVM 前端提供的功能,而对于后端来说,我们接触到的就是关于 arm64、armv7、armv7s 这些 CPU 架构了。

参考:

https://www.jianshu.com/p/d41c60b8c930
https://developerdoc.com/essay/LLDB/iOS_Compiler/
https://www.jianshu.com/p/3c51a42b87a6
https://www.jianshu.com/p/9fc7776cce9b

你可能感兴趣的:(iOS编译链接原理)