Swift 编译简介
- Swift 的编译环境配置和编译流程,请参考我之前的博客:Swift之源码编译的环境搭建和编译流程;
- 新建一个 Swift 工程,在 main.swift 中创建一个 YDWTeacher 类,并通过默认的初始化器,创建一个实例对象并赋值给 t,如下:
class YDWTeacher {
var age: Int = 18
var name: String = "YDW"
}
let t = YDWTeacher()
- 然后在终端中查看抽象语法树:swiftc -dump-ast main.swift,如下:
-
接下来,要研究的是这个初始化器到底做了一个什么样的操作?因此引入 SIL (Swift intermediate language);
-
iOS 的开发语言,不管是 OC 还是 Swift,底层都是通过 LLVM 编译的,生成.o可执行文件,如下所示:
-
不难看出:
- OC 中通过 clang 编译器,编译成 IR,然后再生成可执行文件.o(即机器码);
- swift 中通过 swiftc 编译器,编译成 IR,然后再生成可执行文件;
-
再来看一下:一个 Swift 文件的编译过程经历哪些步骤:
-
下面是 Swift 中的编译流程,其中SIL(Swift Intermediate Language),是 Swift 编译过程中的中间代码,主要用于进一步分析和优化 Swift 代码。如下图所示,SIL 位于在 AST 和 LLVM IR 之间:
- Swift 与 OC 的区别在于 Swift 生成了高级的 SIL;Swift 在编译的过程中使用的前端编译器是 Swiftc,和我们之前的 OC 中使用的 clang 是有所区别的。
- 通过 swiftc -h 终端命令,查看 swiftc 能做什么:
- 分析说明:
- -dump-ast 语法和类型检查,打印AST语法树
- -dump-parse 语法检查,打印AST语法树
- -dump-pcm 转储有关预编译Clang模块的调试信息
- -dump-scope-maps expanded-or-list-of-line:column
Parse and type-check input file(s) and dump the scope map(s) - -dump-type-info Output YAML dump of fixed-size types from all imported modules
- -dump-type-refinement-contexts
Type-check input file(s) and dump type refinement contexts(s) - -emit-assembly Emit assembly file(s) (-S)
- -emit-bc 输出一个LLVM的BC文件
- -emit-executable 输出一个可执行文件
- -emit-imported-modules 展示导入的模块列表
- -emit-ir 展示IR中间代码
- -emit-library 输出一个dylib动态库
- -emit-object 输出一个.o机器文件
- -emit-pcm Emit a precompiled Clang module from a module map
- -emit-sibgen 输出一个.sib的原始SIL文件
- -emit-sib 输出一个.sib的标准SIL文件
- -emit-silgen 展示原始SIL文件
- -emit-sil 展示标准的SIL文件
- -index-file 为源文件生成索引数据
- -parse 解析文件
- -print-ast 解析文件并打印(漂亮/简洁的)语法树
- -resolve-imports 解析import导入的文件
- -typecheck 检查文件类型
SIL
一、什么是 SIL 分析?
- SIL 依赖于 swift 的类型系统和声明,所以 SIL 语法是 swift 的延伸。一个 sil 文件是一个增加了SIL定义的swift源文件;
- SIL 文件中没有隐式 import,如果使用 swift 或者 Buildin 标准组件的话必须明确的引入;
- SIL 函数由一个或多个block组成,一个block是一个指令的线性序列,每个block中的最后一条指令将控制转移到另一个block,或从函数返回。
- 如果想要对 SIL 的内容进行详细地探索,请参考:2015 LLVM Developers’ Meeting
二、SIL 分析 mian 函数
- 查看抽象语法树之后,继续在终端中调用 swiftc -emit-sil main.swift >> ./main.sil && code main.sil 命令,生成 main.sil 文件;
- 用 VSCode 打开 SIL 文件:
// main
//`@main`:标识当前main.swift的`入口函数`,SIL中的标识符名称以`@`作为前缀
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
//`%0、%1` 在SIL中叫做寄存器,可以理解为开发中的常量,一旦赋值就不可修改,如果还想继续使用,就需要不断的累加数字(注意:这里的寄存器,与`register read`中的寄存器是有所区别的,这里是指`虚拟寄存器`,而`register read`中是`真寄存器`)
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
//`alloc_global`:创建一个`全局变量`,即代码中的`t`
alloc_global @$s4main1tAA10YDWTeacherCvp // id: %2
//`global_addr`:获取全局变量地址,并赋值给寄存器%3
%3 = global_addr @$s4main1tAA10YDWTeacherCvp : $*YDWTeacher // user: %7
//`metatype`获取`YDWTeacher`的`MetaData`赋值给%4
%4 = metatype $@thick YDWTeacher.Type // user: %6
//将`__allocating_init`的函数地址赋值给 %5
// function_ref YDWTeacher.__allocating_init()
%5 = function_ref @$s4main10YDWTeacherCACycfC : $@convention(method) (@thick YDWTeacher.Type) -> @owned YDWTeacher // user: %6
//`apply`调用 `__allocating_init` 初始化一个变量,赋值给%6
%6 = apply %5(%4) : $@convention(method) (@thick YDWTeacher.Type) -> @owned YDWTeacher // user: %7
//将%6的值存储到%3,即全局变量的地址(这里与前面的%3形成一个闭环)
store %6 to %3 : $*YDWTeacher // id: %7
//构建`Int`,并`return`
%8 = integer_literal $Builtin.Int32, 0 // user: %9
%9 = struct $Int32 (%8 : $Builtin.Int32) // user: %10
return %9 : $Int32 // id: %10
} // end sil function 'main'
- 分析:
- @main 这⾥标识当前 main.swift 的⼊⼝函数,SIL 中的标识符名称以 @ 作为前缀;
- %0, %1… 在 SIL 也叫做寄存器,这⾥可以理解为⽇常开发中的常量,⼀旦赋值之后就不可以再修改,如果 SIL 中还要继续使⽤,那么就不断的累加数值。 同时这⾥所说的寄存器是虚拟的,最终运⾏到机器上,会使⽤真的寄存器;
- alloc_gobal:创建⼀个全局变量;
- global_addr: 拿到全局变量的地址,赋值给 %3;
- metatype 拿到 YDWTeacher 的 Metadata 赋值给 %4 将 __allocating_init 的函数地址赋值给 %5;
- __apply 调⽤ __allocating_init , 并把返回值给 %6;
- 将 %6 的值存储到 %3(也就是刚刚创建的全局变量的地址);
- 构建 Int , 并 return;
- 注意:code 命令是在 .zshrc 中做了如下配置,可以在终端中指定软件打开相应文件:
$ open .zshrc
// ****** 添加以下别名
alias subl='/Applications/SublimeText.app/Contents/SharedSupport/bin/subl'
alias code='/Applications/Visual\ Studio\ Code.app/Contents/Resources/app/bin/code'
// ****** 使用
$ code main.sil
// 如果想SIL文件高亮,需要安装插件:VSCode SIL
- 从 SIL 文件中,可以看出,代码是经过混淆的,可以通过以下命令还原,以s4main1tAA10YDWTeacherCvp 为例:xcrun swift-demangle s4main1tAA10YDWTeacherCvp,结果如下:
xcrun swift-demangle s4main1tAA10YDWTeacherCvp
$s4main1tAA10YDWTeacherCvp ---> main.t : main.YDWTeacher
- 在 SIL 文件中搜索 s4main10YDWTeacherCACycfC,其内部实现主要是分配内存+初始化变量:
- allocing_ref:创建一个 YDWTeacher 的实例对象,当前实例对象的引用计数为1;
- 调用init方法;
// ********* main入口函数中代码 *********
%5 = function_ref @$s4main10YDWTeacherCACycfC : $@convention(method) (@thick YDWTeacher.Type) -> @owned YDWTeacher
// s4main10YDWTeacherCACycfC 实际就是__allocating_init()
// YDWTeacher.__allocating_init()
sil hidden [exact_self_class] @$s4main10YDWTeacherCACycfC : $@convention(method) (@thick YDWTeacher.Type) -> @owned YDWTeacher {
// %0 "$metatype"
bb0(%0 : $@thick YDWTeacher.Type):
// 堆上分配内存空间
%1 = alloc_ref $YDWTeacher // user: %3
// function_ref YDWTeacher.init() 初始化当前变量
%2 = function_ref @$s4main10YDWTeacherCACycfc : $@convention(method) (@owned YDWTeacher) -> @owned YDWTeacher // user: %3
// 返回
%3 = apply %2(%1) : $@convention(method) (@owned YDWTeacher) -> @owned YDWTeacher // user: %4
return %3 : $YDWTeacher // id: %4
} // end sil function '$s4main10YDWTeacherCACycfC'
- SIL语言对于Swift源码的分析是非常重要的,关于其更多的语法信息,可以参考:Swift Intermediate Language (SIL)。
符号断点调试
- 在我们的 TestSwift 工程中设置“__allocating_init”符号断点;
- 然后执行,可以看到:内部调用的是swift_allocObject;
源码分析
- 在 VSCode 中的REPL(命令交互行,类似于python的,可以在这里编写代码)中编写如下代码(也可以拷贝),并搜索 *_swift_allocObject 函数加一个断点,如下所示:
- 然后初始化一个实例对象t,回车:
- 这里的 Local 中可以看出:requiredSize 是内存大小,requiredAlignmentMask 是内存对齐方式;requiredAlignmentMask 是 swift 中的字节对齐方式,这个和OC中是一样的,必须是8的倍数,不足的会自动补齐,目的是以空间换时间,来提高内存操作效率;
static HeapObject *_swift_allocObject_(HeapMetadata const *metadata,
size_t requiredSize,
size_t requiredAlignmentMask) {
assert(isAlignmentMask(requiredAlignmentMask));
auto object = reinterpret_cast<HeapObject *>(
swift_slowAlloc(requiredSize, requiredAlignmentMask));
// NOTE: this relies on the C++17 guaranteed semantics of no null-pointer
// check on the placement new allocator which we have observed on Windows,
// Linux, and macOS.
new (object) HeapObject(metadata);
// If leak tracking is enabled, start tracking this object.
SWIFT_LEAKS_START_TRACKING_OBJECT(object);
SWIFT_RT_TRACK_INVOCATION(object, swift_allocObject);
return object;
}
- swift_allocObject 的源码如下,主要分为:
- 通过 swift_slowAlloc 分配内存,并进行内存字节对齐;
- 通过new + HeapObject + metadata初始化一个实例对象;
- 函数的返回值是 HeapObject 类型,所以当前对象的内存结构就是 HeapObject 的内存结构;
- 进入 swift_slowAlloc 函数,其内部主要是通过 malloc 在堆中分配 size 大小的内存空间,并返回内存地址,主要是用于存储实例变量:
void *swift::swift_slowAlloc(size_t size, size_t alignMask) {
void *p;
// This check also forces "default" alignment to use AlignedAlloc.
if (alignMask <= MALLOC_ALIGN_MASK) {
#if defined(__APPLE__)
p = malloc_zone_malloc(DEFAULT_ZONE(), size);
#else
// 堆中创建size大小的内存空间,用于存储实例变量
p = malloc(size);
#endif
} else {
size_t alignment = (alignMask == ~(size_t(0)))
? _swift_MinAllocationAlignment
: alignMask + 1;
p = AlignedAlloc(size, alignment);
}
if (!p) swift::crash("Could not allocate memory.");
return p;
}
- 进入 HeapObject 初始化方法,需要两个参数:metadata、refCounts:
struct HeapObject {
/// This is always a valid pointer to a metadata object.
HeapMetadata const *metadata;
SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS;
#ifndef __swift__
HeapObject() = default;
// Initialize a HeapObject header as appropriate for a newly-allocated object.
constexpr HeapObject(HeapMetadata const *newMetadata)
: metadata(newMetadata)
, refCounts(InlineRefCounts::Initialized)
{
}
// Initialize a HeapObject header for an immortal object
constexpr HeapObject(HeapMetadata const *newMetadata,
InlineRefCounts::Immortal_t immortal)
: metadata(newMetadata)
, refCounts(InlineRefCounts::Immortal)
{
}
- 分析:
- 其中 metadata 类型是 HeapMetadata,是一个指针类型,占8字节;
- refCounts(引用计数,类型是 InlineRefCounts,而 InlineRefCounts 是一个类RefCounts 的别名,占8个字节),swift 采用 arc 引用计数;
总结
- 对于实例对象 t 来说,其本质是一个 HeapObject 结构体,默认 16 字节内存大小(metadata 8字节 + refCounts 8字节),与 OC 的对比如下:
- OC 中实例对象的本质是结构体,是以 objc_object 为模板继承的,其中有一个 isa 指针,占 8 字节;
- Swift 中实例对象,默认的比 OC 中多了一个。refCounted 引用计数大小,默认属性占 16 字节;
- Swift 中对象的内存分配流程是:_allocating_init --> swift_allocObject --> _swift_allocObject --> swift_slowAlloc --> malloc;
- init 在其中的职责就是初始化变量,这点与 OC 中是一致的。