主要内容:
- 理解
C
、C++
以及OC
的关系 - 编译型语言与解释型语言
- 编译器
LLVM
与CLang
- 理解
iOS
编译流程 - 预处理
- 编译
- 汇编
- 链接
一、理解C、C++以及OC的关系
1.C语言
-
C
语言是一门面向过程的计算机编程语言,既可用于系统软件开发,也适用于应用软件开发; -
C
语言编译器普遍存在于各种不同的操作系统中,例如Microsoft Windows
,Mac OS X
,Linux
,Unix
等; -
C
语言的设计影响了众多后来的编程语言,例如C++
、Objective-C
、Java
、C#
等;
2.C++语言
- 兼容了
C
语言面向过程特点,但又进行了扩充和完善; - 作为一种面向对象的语言,具有封装、多继承、多态等特性;
3.Objective-C语言
- 扩展了
C
语言的能力,使其具备面向对象设计的能力,相当于C
的超集; -
OC
代码中也可以有C
和C++
语句,它可以调用C
函数,也可以通过C++
对象访问方法;
4.OC与C++的比较
-
OC
与C++
都是从C
语言演变而来面向对象设计语言,也都兼容标准的C
语言;但它们属于不同的面向对象学派; - 两者最大的不同在于:
OC
提供了运行时的动态绑定机制,而C++
是编译时静态绑定,并通过嵌入类和虚函数来模拟实现; -
OC
在编译阶段降低了编译要求提高了灵活性,而C++
则是提高了编译要求,在编译过程中就发现更多的潜在错误,在运行前改正,降低了灵活性;
以下面的代码为例,在编译期间,C++
认为是错误的,而OC
则认为没有问题:
NSString *test =(id) [[NSArray alloc] init];
OC
与C++
在使用细节上的不同如下:
- 定型:
OC
是动态定型,可以允许根据字符串名字来访问方法和类,还可以动态链接和添加类; - 继承:
OC
不支持多继承,C++
支持多继承; - 函数调用:
OC
通过消息传递实现函数调用,而C++
直接进行函数调用; - 接口:
OC
采用Protocol
形式来定义接口,而C++
采用虚函数形式来定义接口; - 重载:
OC
不允许同一个类中两个方法有相同的名字(即使只是参数类型不同),但C++
可以;
二、编译型语言与解释型语言
Objective-C
属于编译型语言,这是为了保证iPhone
的执行效率;
1.编译型语言
- 程序运行前,必须先通过
编译器
生成机器码
,机器码直接通过CPU
执行,运行时不需要重新翻译; - 程序执行效率高,但依赖编译器,调试周期长、跨平台性差些;
- 代表语言:
C
、C++
、OC
等;
2.解释型语言
- 程序运行前,不需要进行编译,而是以文本方式存储程序代码,运行时需要解释器解释后再运行;
- 程序执行效率低下,但是程序具有动态性,运行后也可以随时增加和更新代码来改变程序逻辑;
- 代表语言:
Javascript
、Python
等;
三、编译器LLVM与CLang
1.编译器
概念:把一种编程语言(原始语言
)转换为另一种编程语言(目标语言)的程序;
大多数编译器都分前端
和后端
两部分:
- 前端:负责
词法分析
、语法分析
、生成中间代码
; - 后端:以
中间代码
作为输入,进行与架构无关的代码优化,接着针对不同架构生成不同的机器码;
补充:
- 前后端以
中间代码
作为媒介,使得前后端可以独立的变化,互不影响; - 这样的好处在于:新增一门语言只需要修改前端,而新增一种
CPU
架构只需要修改后端即可;
2.LLVM与Clang
LLVM
是苹果当前使用的编译器:
-
LLVM
是一套编译器基础设施项目,为自由软件,以C++
写成,包含一系列模块化的编译器组件和工具链,用来开发编译器前端
和后端
; - 基于
LLVM
衍生出了一些强大的子项目,比如:Clang
和LLDB
。
CLang
基于LLVM
,是一个高度模块化开发的轻量级编译器;
-
CLang
主要来自苹果电脑的支持,同时支持C
、Objective-C
以及C++
; -
CLang
用于替代Xcode5
版本前使用的GCC
,编译速度提高了3
倍:
3.理解iOS中的编译器
- 在
iOS
开发中,通常LLVM
被认为是编译器的后端,而Clang
是作为编译器的前端; - 二者以
IR
(中间代码)作为媒介,这样前后端分离,使得前后端可以独立的变化,互不影响; -
C
语言家族的前端是clang
,swift
的前端是swiftc
,但二者的后端都是LLVM
;
四、理解iOS编译流程
1.编译流程图
LLVM的编译过程相当复杂,iOS
代码运行需要经过:预处理
、编译
、汇编
、链接
四个关键阶段,具体的流程如下图:
2.准备测试文件
以OC
语言为例,详细分析代码的编译流程,准备一个main.m
文件的内容如下:
#import <Foundation/Foundation.h>
/// 增加注释:宏定义Name
#define Name "梧雨北辰"
int main(int argc, const char * argv[]) {
NSLog(@"Hello, %s", Name);
return 0;
}
五、预处理(Prepressing)
1.主要功能
- 替换宏:替换代码中各种宏定义,如定义的常量、函数等;
- 导入头文件:将
#include
包含的文件插入到该指令位置等; - 清理注释:删除所有注释:
//
、/*
*/
等; - 条件编译:处理
#if
、#ifdef
,#endif
等类似的条件编译; - 添加行号和文件名标识:以便于编译时编译器能够显示警告和错误的所在行号;
2.查看预处理结果
使用xcrun
命令,在终端执行预处理操作:
xcrun clang -E main.m
终端显示效果如下:
# 1 "main.m"
# 1 "<built-in>" 1
...
# 1 "/Applications/Xcode13.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/Foundation.framework/Headers/FoundationLegacySwiftCompatibility.h" 1 3
# 193 "/Applications/Xcode13.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/Foundation.framework/Headers/Foundation.h" 2 3
# 2 "main.m" 2
int main(int argc, const char * argv[]) {
NSLog(@"Hello, %s", "梧雨北辰");
return 0;
}
结果分析:
- 预处理后的文件中,注释已经被清理,宏定义也已经被替换;
- 预处理后的文件有很多行,因为该过程中导入了头文件(
Foundation.h
),而且这个过程是递归的;
六、编译(Compilation)
1. 词法分析(Lexical Analysis)
主要功能:通过扫描器,分割识别源代码符号(如大小括号、=
、字符串);
使用xcrun
命令,在终端执行词法分析操作:
xcrun clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
终端显示效果如下:
annot_module_include '#import <Foundation/Foundation.h>
/' Loc=<main.m:1:1>
int 'int' [StartOfLine] Loc=<main.m:4:1>
identifier 'main' [LeadingSpace]
......
r_brace '}' [StartOfLine] Loc=<main.m:7:1>
eof '' Loc=<main.m:10:1>
结果分析:
- 每个被分割的源代码符号都被记录了位置,方便后续定位错误;
- 比如
Loc=<main.m:4:1>
就表示:'int'
这个符号是从源文件main.m
的第4
行的第1
个字符开始的;
2.语法分析(Semantic Analysis)
主要功能:对源代码符号进行分析,验证语法是否正确,最后生成AST
语法树;
使用xcrun
命令,查看语法分析结果:
xcrun clang -fsyntax-only -Xclang -ast-dump main.c | open -f
AST
语法树:
- 是抽象语法树,结构上比代码更精简,遍历速度更快;
- 能够更快的进行静态检查,同时生成
IR
(中间代码);
3.静态分析(Static Analysis)
主要功能:对AST
树进行遍历分析,包括类型检查
、方法实现检查
,会及时提示错误;
4.生成中间代码(Code Generation)
主要功能:CodeGen
负责将AST
语法树自顶向下遍历,逐步翻译成IR
中间代码;
IR
中间代码:
- 这是一种更接近于机器码的语言,使得编译器被分为前端和后端,不同的平台可以利用各自的编译器将中间代码,转化为适合不同平台的机器码;
- 对于
iOS
系统来说,IR
中间代码生成的就是Mach-O
可执行文件; -
IR
是前端的输出,后端的输入;
七、汇编(Assembly)
输出中间代码
标志着前端工作的完成,接下来将进入后端的处理流程。
1.LLVM优化中间代码
中间代码IR
进入后端,LLVM
会对其进行优化:
Optimization Level
bitcode
2.生成汇编代码
LLVM
对IR
进行优化后,会针对不同架构生成不同汇编代码;
汇编阶段的目的:
- 将代码汇编化,并将符号进行归类;
- 将外部导入符号,放到重定位符号表;
- 最后生成一个或多个
.o
目标文件;
使用xcrun
命令,生成汇编文件:
xcrun clang -S main.m -o main.s
打开.s
文件,摘取内容如下:
.section __TEXT,__text,regular,pure_instructions
.build_version macos, 11, 0 sdk_version 11, 3
.globl _main ## -- Begin function main
// ......
callq _NSLog
// ......
.subsections_via_symbols
可以看到,汇编文件中的NSLog
操作已经被转化为汇编命令形式的调用,即callq _NSLog
;
3.生成目标文件
该阶段是汇编器
将汇编代码
转换为机器代码
,并输出目标文件
,即.o
文件;
使用xcrun
命令,生成目标文件:
xcrun clang -fmodules -c main.m -o main.o
使用file
命令,查看目标文件类型:
% file main.o
main.o: Mach-O 64-bit object x86_64
可以看到,汇编器生成Mach-O
格式的文件,而且是object
类型,即目标文件类型:
-
Mach-O
文件是用于iOS
和OS
平台上的文件类型; -
Mach-O
作为a.out
格式的替代,提供了更强的扩展性,也提升了符号表中信息的访问速度;
使用xcrun
命令,查看下main.o
中的符号:
xcrun nm -nm main.o
终端显示效果如下:
(undefined) external _NSLog
(undefined) external ___CFConstantStringClassReference
0000000000000000 (__TEXT,__text) external _main
可以看到,此时我们使用的NSLog
函数,对应着_NSLog
符号:
-
undefined
:表示在当前文件暂时找不到符号_NSLog
; -
external
:表示这个符号是外部可以访问的,对应表示文件私有的符号是non-external
;
八、链接(Linking)
主要功能:符号解析、重定位、合并目标文件,最终生成可执行文件;
1.使用xcrun命令执行链接,得到可执行文件
xcrun clang main.o -o main
2.使用file命令,查看文件类型
% file main
main: Mach-O 64-bit executable x86_64
% ./main
2021-10-01 19:06:41.846 main[5663:660299] Hello, 梧雨北辰
结果分析:虽然还是Mach-O
格式,但此时已经是executable
类型了,即可执行文件。而且运行该文件后也打印出了预期的结果;
3.再次使用xcrun命令,查看可执行文件的符号表
% xcrun nm -nm main
(undefined) external _NSLog (from Foundation)
(undefined) external ___CFConstantStringClassReference (from CoreFoundation)
(undefined) external dyld_stub_binder (from libSystem)
0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
0000000100003f40 (__TEXT,__text) external _main
0000000100008008 (__DATA,__data) non-external __dyld_private
结果分析:_NSLog
符号依然是undefined
,不过此时多了一些信息,即from Foundation
,表示这个符号来自于Foundation
,会在运行时动态绑定;
4.链接阶段的主要任务
1.符号解析
将每个符号引用和对应的符号定义关联起来;
- 链接器链接多文件时会创建符号表,用于记录所有已经定义和未定义的符号;
- 出现相同符号,会报错:
"ld:dumplicate symbols"
; - 在其他目标文件里没有找到到符号,会报错:
"Undefined symbols"
;
- 出现相同符号,会报错:
- 另外,链接器在整理函数的符号调用关系时,可以帮助我们理清那些函数没有被调用,并自动去除掉;
2.重定位
将变量名、函数名这些符号定义与一个内存位置关联起来;
- 因为只有通过了绑定,机器才知道需要操作什么内存地址;
- 否则,我们就需要在写代码时给每个指令设置好内存地址,不仅操作繁琐,而且容易引起出错;
3.合并目标文件
将多个.m文件
编译产生的.o
目标文件与其他Mach-O
文件(如dylib
、a
、tbd
),合成一个Mach-O
格式的可执行文件;
- 通常项目都会包含多个文件,不同文件之间的
变量
和接口函数
就会产生相互依赖关系; - 程序运行前,需要使用链接器将多个文件里的符号和地址绑定起来,才能保证整个程序里的变量、接口的正常调用;
5.理解静态链接与动态链接
静态链接:作用于编译期,链接后的文件依然可能会存在一些"undefined"
的符号。但是这些符号都会被记录下来,在运行时再通过dlopen
和dlsym
动态链接绑定;
动态链接:作用于运行时,这样的优势在于:诸多类似UIKit
这样的共享库将不必包含在每一个App
包里。比如:我们使用到的UIKit
系统库,等到点击App
真正开始运行之前,才会去链接依赖的UIKit
,链接完成再运行App
;