一、编译器

1、编译器做了什么?

编译器有两个职责:把 Objective-C 代码转化成低级代码,以及对代码做分析,确保代码中没有任何明显的错误。
Xcode的默认编译器是clang,clang 的功能是首先对 Objective-C 代码做分析检查,然后将其转换为低级的类汇编代码:LLVM Intermediate Representation(LLVM 中间表达码)。接着 LLVM 会执行相关指令将 LLVM IR 编译成目标平台上的本地字节码,这个过程的完成方式可以是即时编译 (Just-in-time),或在编译的时候完成。

2、clang和LLVM

1)LLVM

LLVM项目是模块化、可重用的编译器以及工具链技术的集合。

LVVM架构
LVVM三层式架构(官方LLVM):
image.png

  • 第一层支持多种语言作为输入
  • 第二层是一个共享式的优化器
  • 第三层是许多不同的目标平台
  • 第一层和第二层使用相同的中间代码LLVM IR
  • 优化阶段是一个通用的阶段,它针对的是统一的LLVM IR,不论是支持新的编程语言,还是支持新的硬件设备,都不需要对优化阶段做修改

    2)clang

    LLVM项目的一个子项目,基于LLVM架构的C/C++/Objective-C编译器前端。

  • clang的功能是首先对Objective-C代码进行分析检查,然后将其转换为低级的类汇编代码:LLVM 中加表达码

    3) clang与LLVM的关系

    image.png
    LLVM整体架构,前端用的是clang,广义的LLVM是指整个LLVM架构,一般狭义的LLVM指的是LLVM后端(包含代码优化和目标代码生成)。
    源代码(c/c++)经过clang—> 中间代码(经过一系列的优化,优化用的是Pass) —> 机器码

clang常用命令

3、编译器处理过程

1)预处理

每当编源译文件的时候,编译器首先做的是一些预处理工作。比如替换宏定义,类似代码#import <Foundation/Foundation.h> 预处理器对这行代码的处理是用 Foundation.h 文件中的内容去替换这行代码。如果Foundation.h 中也使用了类似的宏引入,则会按照同样的处理方式用各个宏对应的真正代码进行逐级替代。
自定义宏时,不要在需要预处理的代码中加入内联代码逻辑,典型的例子

  1. #import <Foundation/Foundation.h>
  2. #define MAX(a,b) a>b ? a : b
  3. int main(int argc, const char * argv[]) {
  4. @autoreleasepool {
  5. // insert code here...
  6. int i = 200;
  7. NSLog(@"%d",MAX(i++, 100)); // 输出 201
  8. NSLog(@"%d",i); // 输出 202
  9. }
  10. return 0;
  11. }

输出结果:201,202,原因是MAX(i++, 100)将转化为i++>100 ? i++ : 100
如果比较的值是300,则输出是 300,201

2) 词法分析标记

预处理完成以后,每一个 .m 源文件里都有一堆的声明和定义。这些代码文本都会从 string 转化成特殊的标记流。
示例代码:

  1. int main() {
  2. NSLog(@"hello, %@", @"world");
  3. return 0;
  4. }

使用clang -Xclang -dump-tokens test.m进行标记之后输出如下代码

  1. int 'int' [StartOfLine] Loc=<test.m:1:1>
  2. identifier 'main' [LeadingSpace] Loc=<test.m:1:5>
  3. l_paren '(' Loc=<test.m:1:9>
  4. r_paren ')' Loc=<test.m:1:10>
  5. l_brace '{' [LeadingSpace] Loc=<test.m:1:12>
  6. identifier 'NSLog' [StartOfLine] [LeadingSpace] Loc=<test.m:2:3>
  7. l_paren '(' Loc=<test.m:2:8>
  8. at '@' Loc=<test.m:2:9>
  9. string_literal '"hello, %@"' Loc=<test.m:2:10>
  10. comma ',' Loc=<test.m:2:21>
  11. at '@' [LeadingSpace] Loc=<test.m:2:23>
  12. string_literal '"world"' Loc=<test.m:2:24>
  13. r_paren ')' Loc=<test.m:2:31>
  14. semi ';' Loc=<test.m:2:32>
  15. return 'return' [StartOfLine] [LeadingSpace] Loc=<test.m:3:3>
  16. numeric_constant '0' [LeadingSpace] Loc=<test.m:3:10>
  17. semi ';' Loc=<test.m:3:11>
  18. r_brace '}' [StartOfLine] Loc=<test.m:4:1>
  19. eof '' Loc=<test.m:4:2>

可以发现,每一个标记都包含了对应的源码内容和其在源码中的位置。注意这里的位置是宏展开之前的位置,这样一来,如果编译过程中遇到什么问题,clang 能够在源码中指出出错的具体位置。

3)解析

之前生成的标记流将会被解析成一棵抽象语法树 (abstract syntax tree -- AST),在抽象语法树中的每个节点都标注了其对应源码中的位置,同样的,如果产生了什么问题,clang 可以定位到问题所在处的源码位置。

4)静态分析

一旦编译器把源码生成了抽象语法树,编译器可以对这棵树做分析处理,以找出代码中的错误。比如类型检查,检查是否有定义从未使用过的变量等,具体详见clang源码的目录lib/StaticAnalyzer/Checkers

5)代码生成和优化

clang 完成代码的标记,解析和分析后,接着就会生成 LLVM 代码。 clang 能对代码进行多方面的优化处理。

6)参考文献

[编译器]
[clang AST]
[clang编译器]
[LLVM]

二、Build过程

xxxxx

三、Mach-O可执行文件

我们用 Xcode 构建一个程序的过程中,会把源文件 (.m.h) 文件转换为一个可执行文件。这个可执行文件中包含的字节码会将被 CPU (iOS 设备中的 ARM 处理器或 Mac 上的 Intel 处理器) 执行。这个可执行文件就是Mach-O可执行文件。
将.m和.h文件转化为Mach-O这个过程,编译器需要进行以下处理:
1、预处理

  • 符号化 (Tokenization)
  • 宏定义的展开
  • #include 的展开

2、语法和语义分析

  • 将符号化后的内容转化为一棵解析树 (parse tree)
  • 解析树做语义分析
  • 输出一棵抽象语法树(Abstract Syntax Tree* (AST))

3、生成代码和优化

  • 将 AST 转换为更低级的中间码 (LLVM IR)
  • 对生成的中间码做优化
  • 生成特定目标代码
  • 输出汇编代码

4、汇编器

  • 将汇编代码转换为目标对象文件。

5、链接器

  • 将多个目标对象文件合并为一个可执行文件 (或者一个动态库)

Mach-O可执行文件的结构:

  • Header——描述了文件的大概信息
  • Load Commands——描述了 Data 在二进制文件和虚拟内存中的布局信息
  • Data —-存储了实际的内容,主要是程序的指令和数据,它们的排布完全依照 Load Commands 的描述。
    • Segment
      • __PAGEZERO
      • __TEXT
      • __DATA_CONST
      • __DATA
      • __LINKEDIT
    • Section

      四、LLDB

      LLDB 是一个有着 REPL 的特性和 C++ ,Python 插件的开源调试器

常用命令集:

  • help —-列举所有命令
  • print —-打印值
    • p
    • prin
    • pri
  • expression —-改变调试器中的值
    • e
  • continue —- process continue
    • c