产品战略专家梁宁确认出席AICon北京站,分享AI时代下的商业逻辑与产品需求 了解详情
写点什么

iOS 代码染色原理及技术实践

  • 2020-09-08
  • 本文字数:4287 字

    阅读完需:约 14 分钟

iOS代码染色原理及技术实践

背景

随着业务的迅速发展,业务代码逻辑的复杂度增加。QA 测试的质量对于产品上线后的稳定性更加重要。一般 QA 测试的工作流程分为两大项:自动化测试和人工测试。这两种测试后都需要得到代码覆盖率。自动化测试的覆盖率,在双端都有比较成熟的方案。


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

染色流程


流程图中涉及到了双端的关键节点以及技术点。我们重点介绍编译阶段。


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


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


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


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


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

编译阶段

在上文可以看出,编译阶段最核心的操作是对 IR 文件进行插桩。


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


语言处理系统


一个完整的语言处理系统中,从源程序到可执行的机器代码,如下图所示,历经几个重要模块。而我们上文提到的 IR 文件,是编译器模块中的产物,插桩处理也是在这个模块中进行。这里重点讨论下编译器。



编译器


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


传统编译器的劣势是:前端和后端没有完全分离,耦合在了一起,因而如果要支持一门新的语言或硬件平台,需要做大量的工作。一种更加灵活,适应性更好的编译器套件应运而生——LLVM.


LLVM


官网:http://www.aosabook.org/en/llvm.html


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 的官网:


https://gcc.gnu.org/onlinedocs/gcc/Gcov.html


下面简单介绍下 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 的源码及我们了解到的知识。在一个语言的任意处理阶段,我们都可以对其进行定制,甚至我们可以创造一个自己的专属语言。


源码参考:


https://github.com/llvm-mirror/llvm/blob/release_70/lib/Transforms/Instrumentation/GCOVProfiling.cpp


https://llvm.org/doxygen/group__LLVMCCoreValueBasicBlock.html#ga444a4024b92a990e9ab311c336e74633


https://gcc.gnu.org/onlinedocs/gcc/Gcov.html


本文转载自公众号高德技术(ID:amap_tech)。


原文链接


iOS代码染色原理及技术实践


2020-09-08 10:042282

评论

发布
暂无评论
发现更多内容

[Go WebSocket] 多房间的聊天室(一)思考篇

HullQin

Go golang 后端 websocket 9月月更

数据科学家、数据工程师和数据分析师三个角色的区别是什么

雨果

数据分析师 数据科学 数据工程师

如何让开发者直接在应用后台控制用户的运动状态?

HarmonyOS SDK

nft系统开发

开源直播系统源码

区块链 NFT 数字藏品 数字藏品软件开发

javaweb-JSP

喜羊羊

9月月更

JavaWeb -JavaBean MVC Filter 监听器 过滤器

喜羊羊

9月月更

调用 sap.ui.base.ManagedObject 的构造函数时,如何传递绑定路径进去

汪子熙

JavaScript SAP SAP UI5 ui5 9月月更

Sprint Review能不能做成Demo演示?

LigaAI

Scrum 敏捷开发 迭代增量开发 高效办公 企业号九月金秋榜

【活动预告】数据集成海外专场Meetup:走进Shopee,聊透SeaTunnel优化实践

Apache SeaTunnel

技术分享 数据同步 数据集成 社区活动

Qt|自定义Widget实现互斥效果问题

中国好公民st

qt QWidget 9月月更

VUE 项目本地没有问题,部署到服务器上提示错误

HoneyMoose

Java 多线程:基础

Java快了!

java;

客户案例|宜泊科技怎样实现智慧停车可观测

观测云

讲究卡路里多少的美食出圈了!维小饭被评为“2022中国轻食十大品牌”

联营汇聚

库调多了,都忘了最基础的概念 <锁与线程篇1>

知识浅谈

线程 9月月更

面试突击:什么是跨域问题?如何解决?

Java快了!

java;

计网复习二,网络应用

前端小刘不怕牛牛

计算机网络 HTTP 9月月更

数据治理(十一):数据安全管理Ranger初步认识

Lansonli

数据治理 9月月更

ChaosBlade Java 场景性能优化,那些你不知道的事

Java快了!

java;

为什么越来越多博士逃离科研?

博文视点Broadview

中秋节,华为云AI送上超级大月亮制作教程,体验赢开发者键鼠套装

华为云开发者联盟

人工智能 华为云 中秋节 企业号九月金秋榜

【JavaWeb】Servlet系列——使用纯Servlet做一个单表的CRUD操作

胖虎不秃头

Web java; 9月月更

Zilliz 论文入选数据库顶会 VLDB'22

Zilliz

数据库 分布式 云原生 VLDB'22

JavaScript 基础知识

喜羊羊

9月月更

TDengine支持多种写入协议,四种写入方式提效大全

TDengine

tdengine 开源 时序数据库 企业号九月金秋榜

4天带你上手HarmonyOS ArkUI开发

HarmonyOS开发者

HarmonyOS

【C语言深度剖析】详解strlen与sizeof的区别及用法

Albert Edison

C语言 sizeof 9月月更 strlen

Java终极学习路线-共计9大模块/6大框架/13个中间件

小明Java问道之路

Java 架构 JVM 中间件 9月月更

【JavaWeb】Servlet系列——HttpServletRequest接口详解

胖虎不秃头

Web java; 9月月更

Linux系统安装Redis

Centos 7 redis 底层原理 9月月更

云数据库技术|“重磅升级”后再测TDSQL-C

数据库 polarDB 玖章算术 TDSQL-C

iOS代码染色原理及技术实践_大前端_高德技术_InfoQ精选文章