关于ios:iOS-代码染色原理及技术实践

48次阅读

共计 4628 个字符,预计需要花费 12 分钟才能阅读完成。

背景

随着业务的迅速倒退,业务代码逻辑的复杂度减少。QA 测试的品质对于产品上线后的稳定性更加重要。个别 QA 测试的工作流程分为两大项:自动化测试和人工测试。这两种测试后都须要失去代码覆盖率。自动化测试的覆盖率,在双端都有比拟成熟的计划。

本文着重介绍人工测试过程中,怎么失去对应的代码覆盖率。波及到的技术次要是代码染色。以下会先介绍整体的工作流程,再对波及到的技术一一论述。

茫茫人海中,你看到这一篇文章,欢送你来一场 iOS 交换技术的碰撞,互相学习,共同提高技术!iOS 开发交换技术群:563513413

染色流程

流程图中波及到了双端的要害节点以及技术点。咱们重点介绍编译阶段。

  • 编译阶段:生成染色包 (对 IR 文件插桩)

须要在编译中减少编译选项,编译后会为每个可执行文件生成对应的 .gcno 文件。

  • 运行阶段:生成二进制覆盖率文件。

在测试代码中调用覆盖率散发函数,会生成对应的 .gcda 文件。

  • 解析阶段:将二进制覆盖率文件可视化。

编译阶段

在上文能够看出,编译阶段最外围的操作是对 IR 文件进行插桩。

什么是 IR 文件?插桩逻辑是什么?咱们往下看。

语言解决零碎

一个残缺的语言解决零碎中,从源程序到可执行的机器代码,如下图所示,历经几个重要模块。而咱们上文提到的 IR 文件,是编译器模块中的产物,插桩解决也是在这个模块中进行。这里重点探讨下编译器。

编译器

说起编译器,咱们理解到的传统编译器架构分为前端、优化器和后端。

传统编译器的劣势是:前端和后端没有齐全拆散,耦合在了一起,因此如果要反对一门新的语言或硬件平台,须要做大量的工作。一种更加灵便,适应性更好的编译器套件应运而生——LLVM.

LLVM

官网:www.aosabook.org/en/llvm.htm…

LLVM 是一个开源的,模块化和可重用的编译器和工具链技术的汇合,或者说是一个编译器套件。

能够应用 LLVM 来编译 Kotlin,Ruby,Python,Haskell,Java,D,PHP,Pure,Lua 和许多其余语言。

LLVM 外围库还提供一个优化器,对风行的 CPU 做代码生成反对。

LLVM 同时反对 AOT 事后编译和 JIT 即时编译。

2012 年,LLVM 取得美国计算机协会 ACM 的软件系统大奖,和 UNIX,WWW,TCP/IP,Tex,JAVA 等齐名。

LLVM 和传统编译器最大的不同点在于,前端输出的任何语言,在通过编译器前端解决后,生成的两头码都是 IR 格局的。接下来看下 LLVM 架构下的微小劣势,iOS&MacOS 平台的编译器。

iOS&MacOS 平台编译器

iOS、MacOS 平台开发用的 IDE:Xcode。在 Xcode 5 版本前应用的是 GCC 编译器,在 Xcode 5 中将 GCC 彻底摈弃,替换为 LLVM。LLVM 蕴含了编译器前端、优化器和编译器后端三大模块。

其中 Swift 除了在编译器前端和 Objective-C 稍有不同,其余模块都是雷同的。

如下图所示,能看出 LLVM 的劣势,对于一门新的编程语言,只须要提供对应的编译前端,生成 IR。就能够实现整个新语言的解决。

聊过了 IR 文件在整个语言处理过程中的地位,上面咱们看下 IR 文件生成逻辑以及插桩相干的逻辑。这不得不提到 Clang。

Clang

Clang 是 LLVM 的子项目,是 C、C++ 和 Objective-C 的编译器。Clang 在整个 Objective-C 编译过程中表演了编译器前端的角色,同时也参加到了 Swift 编译过程中的 Objective-C API 映射阶段。

Clang 的特点是编译速度快,模块化,代码简略易懂,诊断信息可读性强,占用内存小以及容易扩大和重用等。

Clang 的次要性能是输入代码对应的形象语法树(AST),针对用户产生的编译谬误精确地给出倡议,并将代码编译成 LLVM IR。

以 Xcode 为例,Clang 编译 Objective-C 代码的速度是 Xcode 5 版本前应用的 GCC 的 3 倍,其生成的 AST 所耗用掉的内存仅仅是 GCC 的五分之一左右。

对于 iOS 我的项目能够应用对应的命令获取,本文不作具体介绍。

对于编译器前端的次要工作项,感兴趣的读者浏览《编译原理》——龙书。

介绍完了 IR 的“生成器”。接下来咱们具体介绍 IR 文件。

LLVM IR

LLVM Intermediate Representation。LLVM 的中间代码,是编译器前端的输入,和编译器后端的输出。是连贯编译器前端与 LLVM 后端的一个桥梁。

通常常见的文件格式为 ll 和 bt。做过 iOS 开发的读者应该理解 bitcode。bt 就是编译器开启 bitcode 后的一种中间代码格局。

IR 提供了独立于任何特定机器架构的源语,因而它是 LLVM 优化和进行代码生成的要害,也是 LLVM 有别于其余编译器的最大特点。LLVM 的外围性能都是围绕 IR 建设的。

通常中间代码的示意模式分为:语法树(syntax tree)、三地址指令序列。为了更好的理解 IR 文件。这里介绍下三地址指令。

三地址指令

也能够称为三地址代码。之所以被称为三地址指令,是源于它的指令模式:x = y op z,其中 op 是一个二目运算符,y 和 z 是运算重量的地址,x 是运算后果的寄存地址。三地址指令最多只执行一个运算,通常是计算,比拟或者分支跳转运算。

三地址代码拆分了多运算符算术表达式以及控制流语句的嵌套构造,所以实用于指标代码的生成和优化。

复制代码

// 像 x+y*z 这样的源代码被翻译成三地址指令序列:t1=y*zt2=x+t1 // 源码:do i = i + 1; while(a[i] < 10); 被翻译成如下的三地址指令 i = i + 1t1 = a[i]if t1 < 10 goto 6 其中 t1,t2 是编译器产生的长期名字。复制代码 

然而程序运行过程中,每个模块并不是齐全独立的。存在着模块间的跳转。这些被翻译出的三地址指令,又被组合成另一种便于了解的模式——BB 块。

基本块

基本块 (Basic Block) 是满足下列条件的最大的 间断三地址指令序列

  • 控制流只能从基本块中的第一个指令进入该块。
  • 除了基本块的最初一个指令,控制流在来到基本块之前不会停机或者跳转。
  • 只有基本块中的第一个指令被执行,那么基本块中的所有指令都会失去执行

其中中间代码指令序列生成 BB 块的算法如下:

  • 确定中间代码序列中哪些指令是首指令

    • 中间代码的第一个三地址指令是一个首指令。
    • 任意一个条件或无条件转移指令之后的指标指令是一个首指令。
    • 紧跟在一个条件或无条件转移指令之后的指令是一个首指令。
  • 每个首指令对应的基本块包含了从它本人开始,直到下一个首指令(不含)或者中间代码的结尾指令之间的所有指令。

举例:

复制代码

i = 1 // 第一个三地址指令,所以作为首指令 j = 1 // 第 11 行,跳转语句的指标指令。所以作为首指令 t1 = 10*it2 = t1+jt3 = 8*t2t4 = t3-88a[t4] = 0.0j = j+1if j<=10 goto (3) // 自身作为跳转指令,所以是首指令 i = i+1if i<=10 goto (2) // 自身作为跳转指令,所以是首指令 i = 1t5 = i – 1 // 第 17 行,跳转语句的指标指令。所以是首指令 t6 = 88*t5a[t6] = 1.0i = i+1if i<=10 goto (13)// 自身作为跳转指令,所以是首指令 // 把一个 10x10 的矩阵设置成单位矩阵中的中间代码 for(i=1;i<=10;i++){for(j=1;j<=10;j++){a[i,j] = 0.0;    }}for(i=1;i<=10;i++){a[i,j] = 1.0;}
复制代码 

对应被划分的 BB 块:

在理解了 BB 块之后。咱们间隔怎么对 IR 文件进行插桩的假相曾经越来越近了,上面咱们来看下最初一个最重要的环节。

流图

当将一个中间代码程序划分成为基本块之后,咱们用一个流图来示意它们之间的控制流。流图 (flow graph) 的结点就是这些基本块。流图就是通常的图,它能够用任何适宜示意图的数据结构来示意。

从基本块 B 到基本块 C 之间有一条边当且仅当基本块 C 的第一个指令紧跟在 B 的最初一个指令之后执行。存在这样一条边的起因有两种:

  • 有一个从 B 的结尾跳转到 C 的结尾的条件或无条件 跳转语句
  • 依照原来的三地址语句序列中的程序,C 紧跟在 B 之后,且 B 的结尾不存在无条件跳转语句。

咱们说 B 是 C 的前驱 (predecessor), 而 C 是 B 的一个后继 (successor)。

通常会减少两个分部称为 入口(entry) 进口(exit) 的结点。它们不和任何可执行的两头指令对应。从入口到流图的第一个可执行结点有一条边(edges)。从任何蕴含了可能是程序的最初执行指令的基本块到进口有一条边。如果程序的最初指令不是一个无条件转移指令,那么蕴含了程序的最初一条指令的基本块是进口结点的一个前驱。但任何蕴含了跳转到程序之外的跳转指令的基本块也是进口结点的前驱。

其中 B0-B7 是 BB 块。E0-E7 是边(edges)

插桩逻辑

覆盖率计数指令的插入会进行两次循环,外层循环遍历编译单元中的函数,内层循环遍历函数的基本块。函数遍历用来向 gcno 文件中写入函数地位信息。

一个函数中基本块的插桩办法如下:

  • 统计所有 BB 的后继数 n,创立和后继数大小雷同的数组 ctr[n]。
  • 当前继数编号为序号将执行次数顺次记录在 ctr[i] 地位,对于多后继状况依据条件判断插入。

依据生成流图的规定,能够很容易失去桩点地位,[] 处就是插入的桩点序号。

对于工程配置能够参考 GCOV 的官网:

gcc.gnu.org/onlinedocs/…

上面简略介绍下 gcov,gcno,gcda 这三个 gcc 家族的要害成员。

GCOV

GCOV 是一个 GNU 的本地笼罩测试工具, 随同 GCC 公布,配合 GCC 独特实现对 C 或者 C++ 文件的语句笼罩和分支笼罩测试。是一个命令行形式的控制台程序。须要工具链的反对。

GCNO

利用 Clang 别离生成源文件的 AST 和 IR 文件,比照发现,AST 中不存在计数指令,而 IR 中存在用来记录执行次数的代码。

覆盖率映射关系生成源码是 LLVM 的一个 Pass,用来向 IR 中插入计数代码并生成.gcno 文件(关联计数指令和源文件)。

上图右侧。即为 gcno 的可视化格局。

实质上 gcno 是二进制内容。须要借助 gcov 工具 (gcov -dump xxx.gcno) 将文件转换为这种可视的格局。

其中每个字段的含意

  • 函数所在文件的绝对路径(如上图红框所示)。
  • Block:0-7 代表 BB 文件的编号。
  • Counter 为插桩后生成的存储执行次数的字段。
  • Source Edges 是前继。
  • Destination 是后继。
  • Lines 是指令在代码文件中行数。

GCDA

gcda 是由加了 -fprofile-arcs 编译参数的编译后的文件运行所产生的,它蕴含了弧跳变的次数和其余的概要信息。

借助 gcov 工具能够查看 gcda 文件的大抵内容:

gcda 文件曾经是一个包含了函数执行状况的文件。残余的工作就是将执行状况更加可视化,和源码进行匹配。

理解了三个 gc 的重要成员。借助一些前端工具,咱们就能够失去一份具体的覆盖率报告了。对于前端工具,大家能够自行搜寻。

最初附上覆盖率的一个报告片段

技术扩大

理解上述基础知识后,咱们更加容易了解 LLVM 中的架构及各个模块的性能。咱们能够在插桩过程中,批改原有的插桩逻辑。咱们能够编写 XCode 编译器插件。总之,借助 LLVM 的源码及咱们理解到的常识。在一个语言的任意解决阶段,咱们都能够对其进行定制,甚至咱们能够发明一个本人的专属语言。

作者:iOSlan
链接:https://juejin.im/post/687223…
起源:掘金

正文完
 0