Swift(三)-类的SIL文件分析

3,913 阅读5分钟

「这是我参与2022首次更文挑战的第11天,活动详情查看:2022首次更文挑战

iOS开发语言,不论是OC还是Swift,都是通过LLVM进行编译的,最终生成.o文件,其编译流程如下图:

image.png

  • OC通过clang编译器,编译成IR,然后再生成可执行文件.o(也就是我们的机器码);
  • Swift则是通过Swift编译器生成IR,然后再生成可执行文件.o

Swift编译流程

Swift语言编译流程图如下:

image.png

  • Swift代码经过-dump-parse命令进行语法分析,生成抽象语法树AST
  • 抽象语法树通过-dump-ast进行语义分析(比如类型检查是否正确,是否安全);
  • 语义分析之后,Swift代码将会降级为SIL,也就是Swift中间语言(Swift intermediate language);
  • SIL分为Raw SIL(原生的,没有开启优化选项)和SILOpt Canonical SIL(经过优化的);
  • 最终通过LLVM降级为IR,然后通过后段代码编译为不同架构的机器码

流程中涉及的命令如下:

// 分析输出AST 
swiftc main.swift -dump-parse 

// 分析并且检查类型输出AST 
swiftc main.swift -dump-ast 

// 生成中间体语言(SIL),未优化
swiftc main.swift -emit-silgen 
 
// 生成中间体语言(SIL),优化后的
swiftc main.swift -emit-sil

// 生成LLVM中间体语言 (.ll文件)
swiftc main.swift -emit-ir

// 生成LLVM中间体语言 (.bc文件)
swiftc main.swift -emit-bc

// 生成汇编
swiftc main.swift -emit-assembly

// 编译生成可执行.out文件
swiftc -o main.o main.swift

相对于OC,在Swift的编译过程中多了SILSIL会对我们的代码进行安全检查,比如如下代码: 在OC中:

image.png

我们定义的int8_t类型的计算结果将会出错,因为int8_t只有 一个字节,其运算结果已经溢出,在OC中给出了一个错误的运算结果;我们再来看在Siwft中会发生什么:

image.png

我们看到,同样的代码,在Swift中的编译阶段就会报错,避免后续发生不可预知的错误;这些都是因为SIL存在的结果;

SIL文件分析

那么SIL究竟有着什么样的语法规则呢?我们来生成一个SIL文件进行分析;我们先写一段简单的代码:

image.png

接下来我们将这段代码生成SIL文件:

image.png

为了方便观看,我们也可以通过swiftc main.swift -emit-sil > ./main.sil指令将SIL文件输出为main.sil文件;

image.png

由于SIL文件内容较多,我们只选择重要的部分分析:

Teacher的声明

首先,我们先看一下TeacherSIL阶段的声明部分:

image.png

Teacher的定义中可以看到有初始化的存储属性agename,还有一个标识为@objcdeinit函数,以及默认的初始化器init()

main函数

image.png

  • @main:标识入口函数,在SIL@是作为标识符的;
  • %0--%9:这写在SIL中也被称为寄存器,可以理解为开发过程中的常量,一旦赋值,将无法再次修改(所以后边数字会一直累加);需要注意的是,此处的寄存器虚拟寄存器,最终运行到具体的设备上时,会使用真实的寄存器
  • alloc_global @$s4main1tAA7TeacherCvp:分配一个全局变量,该全局变量的名称是混写的,我们可以通过终端指令xcrun swift-demangle将其还原:

image.png

可以看到此处对应的是main文件中的t变量,对应的是main中的Teacher

  • %3 = global_addr @$s4main1tAA7TeacherCvp:拿到该全局变量的地址给%3
  • %4 = metatype $@thick Teacher.Type:获取Teacher.Type的元类型给%4
  • 根据注释可以知道%5Teacher.__allocating_init()函数的引用,也就是其指针地址;
  • %6 = apply %5(%4):将%5(%4)的结果,也就是Teacher的实例变量,赋值给%6
  • store %6 to %3:将%6也就是实例变量的内存地址,存放到%3这个全局变量中;
  • Swift底层中Int就是一个Struct类型,%8%9是在构建一个Int32的整数类型0,类似与我们OCmain函数最终的return 0

关于SIL语法规则,请查看SIL官方文档

__allocating_init()函数

在上述SIL代码中调用了s4main7TeacherCACycfC也就是Teacher.__allocating_init()函数来创建当前的实例对象的;我们在SIL中定位到该函数:

image.png

  • 该函数需要一个Teacher.Type的元类型,我们可以将此元类型理解为isa
  • %1 = alloc_ref $Teacheralloc_ref会创建一个Teacher的实例变量,其引用计数初始化为1alloc_ref实际上也就是去堆区申请内存空间;如果标识为objcSwift类将会使用Objective-C+allocWithZone:初始化方法;如何验证呢?我们在代码中添加如下断点:

image.png

运行程序,查看汇编指令:

image.png

我们看到将会调用Teacher.__allocating_init()函数,那么该函数是如何实现的呢?断点进入该函数:

image.png

__allocating_init()函数中主要调用了swift_allocObjectTeacher.init()

  • swift_allocObject在堆区找到合适的内存空间初始化;
  • Teacher.init()初始化成员变量;

这是一个纯粹的Swift类,那么如果我们将Teacher继承自NSObject会发生什么呢?

image.png

我们重复上述汇编调试步骤,进入Teacher.__allocating_init()函数:

image.png

我们发现,初始化函数变成了objc_allocWithZone以及objc_msgSend

  • objc_allocWithZone调用malloc函数申请内存空间;
  • objc_msgSend发送init消息;

swift_allocObject

在前边我们已经分析出Swift类初始化过程中会调用swift_allocObject,那么该函数做了什么呢?我们需要借助于Swift源码进行分析; 在该目录下找到stdlib->public->runtime->HeapObject.cpp文件,该文件是和我们的Swift类初始化相关的文件;在该文件中定位到_swift_allocObject_函数,这是一个私有函数,其是被swift::swift_allocObject调用起来的:

image.png

_swift_allocObject_实现如下:

image.png

该函数有三个参数:

  • HdapMetadata const *metadata:元数据类型;
  • requiredSize:所需要的大小;
  • requiredAlignmentMask:对齐所需要的掩码,可以从objc的源码中得知,其为7,因为是8字节对齐;

requiredSizerequiredAlignmentMask传递给函数swift_slowAlloc,该函数返回了一个HeapObject类型的指针; reinterpret_cast用来做指针类型的转换;

  • new (object) HeapObject(metadata)HeapObject初始化;

那么swift_slowAlloc是用来干什么的呢?

swift_slowAlloc

其实现如下:

image.png

swift_slowAlloc函数中,调用了malloc函数来开辟内存空间;

流程总结

那么,我们大致可以总结出Swift对象进行内存分配的流程:

  1. 首先会调用_allocating_init():该函数有编译器生成;
  2. 对于纯Swift类将会再调用swift_allocObject()函数;
  3. 然后在swift_allocObjec()总会调用私有函数_swift_allocObject
  4. 然后通过函数swift_slowAlloc调用malloc来申请堆区的内存空间;