以服务于中国广大创业者为己任,立志于做最好的创业网站。

标签云创业博客联系我们

导航菜单

抖音标题发布后能修改吗,抖音视频文案怎么修改

  

  #背景   

  

  Tik Tok上线Swift后,在编译、CI/CD过程中偶尔会出现切分错误: 11和非法指令: 4的错误。   

  

  并且可以在重新编译后恢复正常。   

  

  因为属于编译器层抛出的Crash,而且提示的错误代码不固定,没有必要,所以暂时比较棘手。网上有很多类似的错误,但是分词。   

  

  Fault是访问错误内存的一般错误报告,所以参考意义不大。我和公司内外的团队沟通过,也遇到过类似的错误,但是原因不一样,很难从中吸取教训。   

  

  虽然Swift库二值化后相关代码不会参与编译,但局部出现的概率大大降低。   

  

  源代码仍然用在CI/CD/warehouse的二进制任务中。如果有问题,需要手动重试,影响效率,比较麻烦。因此,我们深入编译器寻找解决方案。   

  

  #崩溃堆栈   

  

     

  故障解决方案“/   

  

  #结论   

  

  简而言之,它是一个NSDictionary变量,将在Swift代码的OC中声明为类属性,并用作Swift Dictionary。也就是一个。   

  

  不可变变量是可变的。   

  

  使用了变量。编译器在检查SILInstruction时出错,并主动调用abort()来结束该过程,或者发生了EXC_BAD_ACCESS崩溃。   

  

  #准备。   

  

  #编译Swift。   

  

  由于错误已在本地重现,取与本地版本一致的swift-5.3.2-RELEASE版本,建议使用VSCode调试,Ninja施工。   

  

  忍者是一个专注于速度的小型建造系统。   

  

  #笔记。   

  

  *提前预留50G磁盘空间。   

  

  *第一次编译时间一个小时左右,CPU基本满了。   

  

  #下载并编译源代码。   

  

  brew install cmake ninjakdir swift-source CD swift-source git clone git @ github.com : apple/swift . git CD swift/utils。/update-check out-tag swift-5 . 3 . 2-RELEASE-clone。/build-脚本   

  

  #主目录   

  

     

  故障解决方案“/   

  

  #选择编译参数。   

  

  作者从Tik Tok项目中提取相关代码,并在本地重现编译错误后从Xcode中提取编译参数:   

  

     

  故障解决方案“/   

  

  # VSCode调试。   

  

  选择合适的LLDB插件,以CodeLLDB为例配置如下的launch.json.   

  

  args的内容是获取上一步提取的编译参数,每个参数分批用双引号括起来,用逗号隔开。   

  

  { 'version': '0.2.0 ',' configurations ' :[{ ' type ' : ' lldb ',' request': 'launch ',' name': 'Debug ',' program ' : ' $ { workspaceFolder }/build/Ninja-debugasert/swift-macosx-x86 _ 64/bin/swift ',' args ' 3:[' front ','-c ','-primary-file '/*和其他参数   

  

  # SIL   

  

  # LLVM   

  

  在深入SIL之前,我们先简单介绍一下LLVM,经典的LLVM。   

  

  如下图所示,三级架构分为前端、优化器和后端。当需要支持新的语言时,只需要实现前端部分,当需要支持新的架构时,只需要实现后端部分,前端连接中枢就是。   

  

  红外(中间体   

e Representation),IR 独立于编程语言和机器架构,故 IR 阶段的优化可以做到抽象而通用。

  

  

fault解决方案' />   

# Frontend

  

前端经过词法分析(Lexical Analysis),语法分析(Syntactic Analysis)生成 AST,语义分析(Semantic

  

Analysis),中间代码生成(Intermediate Code Generation)等步骤,生成 IR。

  

# IR

  

格式

  

IR 是 LLVM 前后端的桥接语言,其主要有三种格式

  

* 可读的格式,以.ll 结尾

  

* Bitcode 格式,以.bc 结尾

  

* 运行时在内存中的格式

  

这三种格式完全等价。

  

SSA

  

LLVM IR 和 SIL 都是 SSA(Static Single Assignment)形式,SSA

  

形式中的所有变量使用前必须声明且只能被赋值一次,如此实现的好处是能够进行更高效,更深入和更具定制化的优化。

  

如下图所示,代码改造为 SSA 形式后,变量只能被赋值一次,就能很容易判断出 y1=1 是可被优化移除的赋值语句。

  

  

fault解决方案' />   

结构

  

基础结构由 Module 组成,每个 Module 大概相当于一个源文件。Module 包含全局变量和 Function 等。Function

  

对应着函数,包括方法的声实现,参数和返回值等。Function 最重要的部分就是各类 Basic Block。

  

Basic Block(BB) 对应着函数的控制流图,是 Instruction 的集合,且一定以 Terminator Instructions

  

结尾,其代表着 Basic Block 执行结束,进行分支跳转或函数返回。

  

Instruction 对应着指令,是程序执行的基本单元。

  

  

fault解决方案' />   

# Optimizer

  

IR 经过优化器进行优化,优化器会调用执行各类 Pass。所谓 Pass,就是遍历一遍 IR,在进行针对性的处理的代码。LLVM 内置了若干

  

Pass,开发者也可自定义 Pass 实现特定功能,比如插桩统计函数运行耗时等。

  

Xcode Optimization Level

  

在 Xcode - Build Setting - Apple Clang - Code Generation - Optimization Level

  

中,可以选定优化级别,-O0 表示无优化,即不调用任何优化 Pass。其他优化级别则调用执行对应的 Pass。

  

  

fault解决方案' />   

# Backend

  

后端将 IR 转成生成相应 CPU 架构的机器码。

  

# Swiftc

  

不同于 OC 使用 clang 作为编译器前端,Swift 自定义了编译器前端 swiftc,如下图所示。

  

  

fault解决方案' />   

这里就体现出来 LLVM 三段式的好处了,支持新语言只需实现编译器前端即可。

  

对比 clang,Swift 新增了对 SIL(Swift Intermediate Language)的处理过程。SIL 是 Swift

  

引入的新的高级中间语言,用以实现更高级别的优化。

  

# Swift 编译流程

  

Swift 源码经过词法分析,语法分析和语义分析生成 AST。SILGen 获取 AST 后生成 SIL,此时的 SIL 称为 Raw

  

SIL。在经过分析和优化,生成 Canonical SIL。最后,IRGen 再将 Canonical SIL 转化为 LLVM IR

  

交给优化器和后端处理。

  

  

fault解决方案' />   

# SIL 指令

  

SIL 假设虚拟寄存器数量无上限,以%+数字命名,如%0,%1 等一直往上递增 以下介绍几个后续会用到的指令:

  

* alloc_stack : 分配栈内存

  

* apply : 传参调用函数

  

* Load : 从内存中加载指定地址的值

  

* function_ref : 创建对 SIL 函数的引用

  

SIL 详细的指令解析可参考官方文档。

  

# Identifier

  

LLVM IR 标识符有 2 种基本类型:

  

* 全局标识符 :包含方法和全局变量等,以@开头

  

* 局部标识符 :包含寄存器名和类型等,以%开头,其中%+数字代表未命名变量变量

  

在 SIL 中,标识符以@开头

  

* SIL function 名都以@+字母/数字命名,且通常都经过 mangle

  

* SIL value 同样以%+字母/数字命名,表示其引用着 instruction 或 Basic block 的参数

  

* @convention(swift)使用 Swift 函数的调用约定(Calling Convention),默认使用

  

* @convention(c)和@convention(objc_method)分别表示使用 C 和 OC 的调用约定

  

* @convention(method)表示 Swift 实例方法的实现

  

* @convention(witness_method)表示 Swift protocol 方法的实现

  

# SIL 结构

  

SIL 实现了一整套和 IR 类似的结构,定制化实现了SILModule SILFunction SILBasicBlock SILInstruction。

  

  

fault解决方案' />   

# 调试过程

  

# 复现 Crash

  

根据前文的准备工作设置好编译参数后,启动编译,复现 Crash,两种 Crash

  

都有复现,场景如下图所示。abort()和EXC_BAD_ACCESS会导致上文出现的Illegal instruction: 4和Segmentation

  

fault: 11错误。由于二者的上层堆栈一致,以下以前者为例进行分析。

  

  

fault解决方案' />   

  

fault解决方案' />   

# 堆栈分析

  

通过堆栈溯源可看出是在生成SILFunction后,执行postEmitFunction校验SILFunction的合法性时,使用SILVerifier层层遍历并校验

  

BasicBlock(visitSILBasicBlock)。对 BasicBlock

  

内部的SILInstruction进行遍历校验(visitSILInstruction)。

  

在获取SILInstruction的类型时调用getKind()返回异常,触发 Crash。

  

  

fault解决方案' />   

# 异常 SIL

  

* 由于此时SILInstruction异常,比较难定位是在校验哪段指令时异常,故在遍历SILInstruction时打印上一段指令的内容。

  

* swift 源代码根目录执行以下命令,增量编译

  

cd build/Ninja-DebugAssert/swift-macosx-x86_64ninja

  

复现后打印内容如下图所示:

  

> 调试小 tips:LLVM 中很多类都实现了 dump()函数用以打印内容,方便调试。

  

  

fault解决方案' />   

// function_ref Dictionary.subscript.setter%32 = function_ref @$sSDyq_Sgxcis : $@convention(method) <τ_0_0, τ_0_1 where τ_0_0 : Hashable> (@in Optional<τ_0_1>, @in τ_0_0, @inout Dictionary<τ_0_0, τ_0_1>) -> () // user: %33%33 = apply %32(%13, %11, %24) : $@convention(method) <τ_0_0, τ_0_1 where τ_0_0 : Hashable> (@in Optional<τ_0_1>, @in τ_0_0, @inout Dictionary<τ_0_0, τ_0_1>) -> ()%34 = load [take] %24 : $*Dictionary // users: %43, %37

  

# 正常 SIL

  

命令行使用swiftc -emit-silgen能生成 Raw SIL,由于该类引用到了 OC 文件,故加上桥接文件的编译参数,完整命令如下:

  

swiftc -emit-silgen /Users/cs/code/ThirdParty/Swift_MVP/Swift_MVP/SwiftCrash.swift -o test.sil -import-objc-header /Users/cs/code/ThirdParty/Swift_MVP/Swift_MVP/Swift_MVP-Bridging-Header.h

  

截取部分 SIL 如下

  

%24 = alloc_stack $Dictionary // users: %44, %34, %33, %31%25 = metatype $@objc_metatype TestObject.Type // users: %40, %39, %27, %26%34 = load [take] %24 : $*Dictionary // users: %42, %36%35 = function_ref @$sSD10FoundationE19_bridgeToObjectiveCSo12NSDictionaryCyF : $@convention(method) <τ_0_0, τ_0_1 where τ_0_0 : Hashable> (@guaranteed Dictionary<τ_0_0, τ_0_1>) -> @owned NSDictionary // user: %37%36 = begin_borrow %34 : $Dictionary // users: %38, %37%37 = apply %35(%36) : $@convention(method) <τ_0_0, τ_0_1 where τ_0_0 : Hashable> (@guaranteed Dictionary<τ_0_0, τ_0_1>) -> @owned NSDictionary // users: %41, %40

  

# SIL 分析

  

对正常 SIL 逐条指令分析

  

1. 在栈中分配类型为Dictionary的内存,将其地址存到寄存器%24,该寄存器的使用者是%44, %34, %33, %31

  

2. %25 表示类型TestObject.Type,即TestObject的类型 metaType

  

3. 加载%24 寄存器的值到%34 中,同时销毁%24 的值

  

4. 创建对函数_bridgeToObjectiveC()-> NSDictionary的引用,存到%35 中

  

* 由于函数名被 mangle,先将函数名 demangle,如下图所示,得到函数

  

  

fault解决方案' />   

  

fault解决方案' />   

* @convention(method)表明是 Swift 实例方法,有 2 个泛型参数,其中第一个参数τ_0_0实现了 Hashable 协议

  

5. 生成一个和%34 相同类型的值,存入%36,%36 结束使用之前,%34 一直存在

  

6. 执行%35 中存储的函数,传入参数%36,返回NSDictionary类型,结果存在%37。其作用就是将Dictionary转成了NSDictionary

  

# 曙光初现

  

对比异常 SIL,可以看出是在执行桥接方法_bridgeToObjectiveC()时失败,遂查看源码,发现是一个 OC

  

的NSDictionary不可变类型桥接到 Swift

  

的Dictionary成为一个可变类型时,对其内容进行修改。虽然这种写法存在可能导致逻辑异常,但并不致编译器 Crash,属于编译器代码

  

bug。更有意思的是,只有在 OC 中将该属性声明为类属性(class)时,才会导致编译器 Crash。

  

class SwiftCrash: NSObject { func execute() { //compiler crash TestObject.cachedData[""] = "" }}

  

@interface TestObject : NSObject@property (strong, nonatomic, class) NSDictionary *cachedData;@end

  

# 解决方案

  

# 源码修改

  

找到错误根源就好处理了,将问题代码中的 NSDictionary 改成 NSMutableDictionary 即可解决。

  

重新运行 Swift 编译器编译源码,无报错。

  

修改抖音源码后,也再没出现编译器 Crash 的问题,问题修复。

  

# 静态分析

  

# 潜在问题

  

虽然NSDictionary正常情况下可以桥接成 Swift 的Dictionary正常使用,但当在 Swift 中对 immutable

  

对象进行修改后,会重新生成新的对象,对原有对象无影响,测试代码和输出结果如下:

  

可以看出变量temp内容无变化,Swift 代码修改无效。

  

TestObject *t = [TestObject new];t.cachedData = [@{@"oc":@"oc"} mutableCopy];NSDictionary *temp = t.cachedData;NSLog(@"before execution : temp %p: %@",temp,temp);NSLog(@"before execution : cachedData %p: %@",t.cachedData,t.cachedData);[[[SwiftDataMgr alloc] init] executeWithT:t];NSLog(@"after execution : temp %p: %@",temp,temp);NSLog(@"after execution : cachedData %p: %@",t.cachedData,t.cachedData);

  

class SwiftDataMgr: NSObject { @objc func execute(t : TestObject) { t.cachedData["swift"] = "swift" }}

  

  

fault解决方案' />   

# 新增规则

  

新增对抖音源码的静态检测规则,检测所有 OC immutable 类是否在 Swift 中被修改。防止编译器 crash 和导致潜在的逻辑错误。

  

所有需检测的类如下:

  

NSDictionary/NSSet/NSData/NSArray/NSString/NSOrderedSet/NSURLRequest/NSIndexSet/NSCharacterSet/NSParagraphStyle/NSAttributedString

  

# 后记

  

行文至此,该编译器 Crash 问题已经解决。同时近期在升级 Xcode 至 12.5 版本时又遇到另一种编译器 Crash

  

且未提示具体报错文件,笔者如法炮制找出错误后并修复。待深入分析生成SILInstruction异常的根本原因后,另起文章总结。

  

此外笔者为 Swift 编译器提交了 bug 报告并附上最小可复现 demo, 有需要的同学可以在此链接下载:

  

bugs.swift.org/browse/SR-1…

  

# 加入我们

  

我们是负责抖音客户端基础能力研发和新技术探索的团队。我们在工程/业务架构,研发工具,编译系统等方向深耕,支撑业务快速迭代的同时,保证超大规模团队的研发效能和工程质量。在性能/稳定性等方面不断探索,努力为全球数亿用户提供最极致的基础体验。

  

如果你对技术充满热情,欢迎加入抖音基础技术团队,让我们共建亿级全球化 App。目前我们在深圳、北京、上海和杭州均有招聘需求。

  

内推可以联系邮箱:chenshan.cc@bytedance.com,邮件标题:姓名-工作年限-抖音-基础技术-iOS/Android。