代码级质量技术之基本框架介绍
作者 | CQT&星云团队
一、背景
GEEK TALK
代码级质量技术:顾名思义为了服务质量更好,涉及到代码层面的相关技术,特别要指出的是,代码级质量技术不单纯指代码召回技术,如静态代码扫描、单元测试等。
研究代码级质量技术主要有以下几个方面的原因:
1、随着精准测试等概念的兴起,对代码覆盖率的依赖逐渐加重,代码插桩的性能、准确性、时效性等都成为业界要解决的难题;
2、随着智能化,尤其是基于风险驱动的测试发展,对代码的理解需要得到突破,才能更好的从代码实现中挖掘风险、判断风险;
3、在黑盒级别的测试,质量工作者往往是通过对象的返回或外在表象观测对下的异常表现进而发现问题,但是其实可能很多潜在的异常并没有到“肉眼”可观测到的级别而导致问题漏出,这时候就需要有更多的对象更细的运行数据来供质量工作者分析去发现问题的蛛丝马迹,诸如:内存泄露、性能恶化等,所以业界有很多prof、火焰图、asan等偏白盒动态检测问题的技术出现;
4、代码作为产生实际问题最前沿的阵地,大部分问题都可以归因到某段代码不合理,如果可以在代码级别直接召回问题,无论从仿真复杂度、修复成本、定位成本等均会得到极大改善;
5、代码是工程师交流的舞台,通过对代码级质量技术的研究和规范,可以促进代码更加的具备鲁棒性和更优质的设计如单测提升可测性等,也可以促进质量保障人员对代码加深掌控与理解,进而在质量保障各类场合发挥关键作用。
从指导质量行为、极大提升召回问题能力、增强代码鲁棒性、提升人员对代码的掌控力等多个方面,都可以看出代码级质量技术的关键且不可替代作用,百度质量效能平台于2019年开始关注和投入该方向,在代码理解、代码探针、代码质量技术应用等多个层级多方面进行探索和落地,接下来的文章中会依次为大家介绍。
二、代码级质量技术架构
GEEK TALK
要理解代码级质量技术的原理和后续的主要应用场景,首先要理解代码从语言到可执行态的基本过程,下面以C++为例说下基本过程:
C++从代码到可执行bin文件,主要分为四个阶段:预处理、编译、汇编和链接。
预处理:处理一些#号定义的命令或语句(如#define、#include、#ifdef等),生成.i文件;
编译:进行词法分析、语法分析和语义分析等,生成.s的汇编文件,大家熟悉的AST抽象语法树就在该过程产生;
汇编:将对应的汇编指令翻译成机器指令,生成二进制.o目标文件;
链接:链接分为两种,静态链接:将静态链接库中的内容直接装填到可执行程序中;动态链接:只在可执行程序中记录与动态链接库中共享对象的映射信息。
代码级质量技术的技术原理,主要是获取到该过程的代码片段数据或植入对应的目标代码,来达到对应的质量目标,如获取片段数据可以用来理解代码判断风险,可以用来指结构化代码结构,供自动生成单元测试和代码检测提供基础数据;如植入对应目标代码,可以用来做插桩(即覆盖率采集)或动态数据采集等。
基于上述介绍与理解,我们把代码级质量技术划分大范围为两个层次,两个层次内包含多个层次,如下图所示:
大层次一:代码理解,CodeC(Code Comprehend):偏底层技术,基于底层AST等能力、分析出代码的特性(AST、调用链、依赖等)和风险度,通过API、SDK等方式对外提供基础服务
存储层:主要用来代码编译过程的基础数据和对应的数据存储选型调优等,在这个过程主要难点在于基础工具的选型和过程性能的调优,以达到可以在业务应用的目标;
分析层:分析层是依托基础的数据,根据特定的要求,对数据进行结构化的建模,如函数调用链、依赖关系等,做好基础的分析供上层应用;
模型层:用于通过分析层和基础数据,去训练代码存在的潜在风险或风险偏向(性能问题突出等);
API层:通过API、SDK等方式对外提供基础服务。
该层会遇到众多技术挑战,如要适配不同语言的解析器、编译过程;基础框架进行代码调优;分析过程数据缺失修复等,是一项非常细致且有技术挑战的工作,当然我们在该过程也会探索出一些技术经验供大家参考。
大层次二:代码级质量技术应用,Codeπ:主要是依托代码理解的过程或产出,植入对应的信息,以达到对应的质量目标,这个层次应用场景是关键,因此我们是以解决问题的目标为导向,对该层次进行细分,所以目标或应用场景的不同会使得该层次的分类会不断增加,目前分为以下四类:
CodeQ(Code Quality): 与召回问题相关(智能UT、基于规则的代码缺陷检测、基于AI的代码缺陷检测、火焰图、ASAN等在个分类);
CodeP(Code Probe):与动态插桩相关(ccover、covtool、jacoco、gocov等在这层),主要是往代码里面植入探针获取运行行为数据;
CodeH(Code Health):评估代码健康度(类似sonarcube等)、代码风险度评估用于决策后续的质量行为;
CodeDL(Code defect location):代码缺陷定位。
下面的章节我们会分布从第二级的层次,为大家做基本原理和过程介绍,后续还会有系列发文再深入的介绍对应实现内容。
三、代码理解层介绍
GEEK TALK
代码理解是一个以软件程序为对象,对其内部的运作流程进行分析,获取相关的知识信息,这些信息可以用于软件开发、软件测试、软件维护等各个阶段,旨在对程序进行性能优化和正确性验证。代码理解常用的分析方向有静态分析、动态分析、非源码分析3类,但是随着LLM大模型的发展,我们也正在研究模型在代码理解领域的突破与应用。
静态分析:是指在不运行代码的方式下,通过词法分析、语法分析、控制流、数据流分析等技术对程序代码进行扫描,验证代码是否满足规范性、安全性、可靠性、可维护性等指标的一种代码分析技术。
动态分析:软件系统在模拟的或真实的环境中执行之前、之中和之后,对软件系统行为的分析。
非代码分析:主要是对数据文件、配置文件等非源码文件和源码间进行关联分析,当代码仓变更时,能感知变更内容对源码、功能的影响。
动态分析多为对程序进行的一些功能测试或性能测试等对程序的运行结果,资源使用情况的相关程序分析工作。故本小节主要介绍静态程序分析相关的代码理解技术,不对动态程序分析做展开。
静态程序分析在不执行程序程序的情况下对程序进行分析,分析的对象可以是针对特定版本的源代码或者二进制文件,通过词法分析、语法分析、控制流、数据流分析等技术对程序代码进行扫描,根据不同的分析目标,得到对应的分析结果。在学术界和工业界主要应用在软件安全领域,验证代码是否满足规范性、安全性、可靠性、可维护性;在百度内部,除漏洞检测外,静态程序分析还包括多维度的代码分析和度量手段,在交付系统和监测系统中被广泛使用。
业界静态分析一般基于以下4种方式展开:
关键字匹配,基于正则表达式分析
基于AST的代码分析,结合正则表达式和关键字能力
优点:结合语法和语义,可以引入作用域等更多概念,更准确。
缺点:无法应对所有场景,另外,基于AST来分析得到的数据流,忽略了分支、跳转、循环等影响执行过程顺序的条件,缺少控制流信息。
基于IR/CFG的代码分析等自制的中间语言数据结构分析
属于当前比较主流的代码分析方案,例如被源伞,实现了多种语言生成统一的IR,这样一来对于新语言的扫描支持难度就变得大大减少。
IR:是一种类似于汇编语言的线性代码,其中各个指令按照顺序执行。其中现在主流的IR是三地址码(四元组),例如llvm的IR。
CFG:(Control flow graph)控制流图,在程序中最简单的控制流单位是一个基本块,在CFG中,每一个节点代表一个基本块,每一个边代表一个可控的控制转移,整个CFG代表了整个代码的的控制流程图。基于IR来生成得到CFG。
基于QL(Query Language)分析
例如codeQL,把源代码转化成一个可查询的数据库,通过 对源代码工程进行关键信息分析提取,构成一个关系型数据库。安全漏洞、Bug 和其他错误被建模为可针对从代码中提取的数据库执行的查询。
常用的静态程序分析技术:
数据流分析
数据流分析收集程序运行到不同位置时各个值的信息和它们随时间变化的信息。污点检验是一个典型的通过数据流分析进行程序风险检测的例子,它会找到所有的可能被使用者修改的变量(也就是有“污点”、不安全的变量),并阻止这些变量在被修复安全漏洞前被使用。
控制流分析
用于分析程序控制流结构的静态分析技术,目的在于生成程序的控制流图,在污点分析、编译器设计、程序分析、程序理解等领域都有重要应用。
指针分析
指针分析主要用于分析指针所有可能指向的对象,它会分析出一个指针所指向的内存位置和所对应的对象。可以用于调用分析、代码优化、Bug追查、安全性分析与验证测试。
四、代码级应用之探针
GEEK TALK
代码探针,也是插桩技术,它是在保证被测程序原有逻辑完整性的基础上在程序中插入一些探针(又称为“探测仪”,本质上就是进行信息采集的代码段,可以是赋值语句或采集覆盖信息的函数调用),通过探针的执行并抛出程序运行的特征数据,通过对这些数据的分析,可以获得程序的控制流和数据流信息,进而得到逻辑覆盖等动态信息,从而实现测试目的的方法。
不同语言的插桩技术有所不同,常见的技术有:ccover、covtool、jacoco、gocov。
CodeP代码探针可以应用在: 代码监控(方法执行耗时等),代码分析(函数、数据流的跟踪等),业务埋点。
除此之外,代码探针最常见的场景是代码覆盖率采集。
4.1 覆盖率
代码覆盖率,是软件测试中的一种度量,描述程序中源代码被测试的比例和程度,所得比例称为代码覆盖率 ,分析未覆盖部分的代码,从而反推在前期测试设计是否充分,没有覆盖到的代码是否是测试设计的盲点。覆盖率统计的分类包含:
行覆盖率:行覆盖率是最基本的指标,表示是否代码中的每个可执行语句都被执行过;
分支覆盖率:分支覆盖使用一组测试参数来测试是否代码中所有的分支都能被测试到了;
路径覆盖率: 对包括所有分支在内的所有的路径都能测试一遍,这就是路径覆盖;
变更行覆盖率:上一次发布代码后更新的代码的行覆盖率,这个数据可以方便的看出新的代码是否做了测试。
覆盖率的业务使用场景广泛比如:RD自测、RD准入、QA准出、外包评估、精准测试、集成测试、基线升级评估、灰度测试评估,自动化测试能力评估、众测等。
代码探针实现覆盖率统计的步骤如下:
1、识别待插桩的函数
2、用codep技术对函数进行插桩,插桩技术分为:
源码插桩: 侵入式的在源代码的基础上替换或者插入另外一些代码
编译过程插桩: 在字节码文件中写入桩函数 (如asm、javassit等技术)
3、探针采集覆盖信息整合, 统计覆盖率数据
五、代码级应用之召回
GEEK TALK
从召回异常问题的角度介绍两类代码级技术应用:智能UT、SA。与现有异常测试方法进行对比分析,压力测试、功能测试依赖编译运行且需人工构造异常场景,存在高成本、低召回的问题。而智能UT和SA基于白盒分析产出的数据可以提前、快速、低成本、轻量级地召回异常问题。
5.1 智能UT
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证,这里的最小测试单元是指函数或者类。在问题召回层面,UT针对最小单元进行测试,构造数据简单、易于验证正确性,便于后续功能的回归,能够更早地发现问题,定位和解决问题成本低。
传统UT依赖开发人员人工编写单测代码来进行测试,存在开发成本高、依赖人的意识等缺点。基于单测的基本原理衍生出了智能UT工具。智能UT通过自动的分析函数和随机的构造测试数据,可自动生成异常单元测试代码,生成的代码可以直接用于单元测试任务,单元测试运行后,智能UT工具能分析代码中存在的稳定性问题。
如下图展示,智能UT建设的主要思路是将一个开发人员编写单元测试代码的过程进行拆解,将整个过程抽象为确认待测试函数->分析代码->构造测试数据->生成测试代码四个步骤,利用白盒数据和一系列算法模拟上述单测代码生成的过程从而自动地生成异常单元测试代码并应用于单元测试任务。
△UT与智能UT过程对比
5.2 SA-基于规则的代码缺陷检测
SA(static analysis)意为静态代码扫描,整个扫描过程无需编译运行,仅通过词法分析,语法分析,语义分析等技术对代码进行扫描,进而发现代码逻辑错误和编程缺陷。依据编程语言的自身特性,可将各类风险场景提取转化为通用的规则进行异常拦截。现有SA检查是通过依赖代码分析、基于通用的风险规则进行代码缺陷检查的静态代码扫描工具,可召回例如空指针访问、数组越界、除零等风险问题。
下图展示了基于规则的静态代码检查处理流程。
△SA处理流程框架
优点:无需编译运行、资源消耗少,扫描分析过程高度自动化、不依赖人力。
缺点:依赖后验知识、存在滞后性,依赖人开发规则、准召低、可持续性差。
此外在代码级领域还有专门被测对象动态行为的检测技术,用于发现程序细微的异常,比如业界常用的火焰图、gprof、ASAN等工具,就是在程序运行时收集程序表现数据,用于检测程序异常问题。
六、代码级应用之孤岛函数识别
GEEK TALK
6.1 什么是孤岛函数
6.2 为什么要做孤单函数识别
无用函数属于技术债治理的场景之一,无用代码的存在增加了软件开发、测试、以及问题排查的开销,例如QA和RD需要更多额外的精力来评估需求的影响范围。
通过孤岛函数识别的能力可以做无用代码和相关用例、配置、数据的清理,提升代码可维护性,同时提升问题的排查效率。
6.3 如何识别孤岛函数
1、静态分析方法
原理:通过函数调用分析,获取入度为0的函数,再结合不同语言特性,筛选出实际未被调用的函数。例如:c++语言中构造函数等非显式调用的函数、纯虚拟函数等函数特性。
优点:执行速度快,开发成本低。
缺点:受限于不同语言特性,识别准确率需要持续优化。例如:配置反射调用、多态特性下识别准确率更低。
2、动态分析方法
原理:通过探针的方式,在程序运行时对函数调用栈进行记录,一般是基于抽样或者开关的方式来控制。
优点:识别准确率相比静态分析方法要高。
缺点:代码侵入对性能有损,同时也受限于流量丰富度,可能需要接入适配。
3、动静结合方法
将上述两种方法结合起来时使用,进行能力互补。
这篇文章更多的是从背景、结构和各个层的基本概念介绍了代码级质量技术的概况,接下来的文章将会在各个方面、各个层级进行展开,欢迎大家关注和一起探讨。