原文来自微信公众号“编程语言Lab”:数值程序分析
搜索关注“编程语言Lab”公众号(HW-PLLab)获取编程语言更多技术内容!
欢迎加入编程语言社区 SIG-程序分析,了解更多程序分析相关的技术内容。
加入方式:添加文末小助手微信,备注“加入 SIG-程序分析”。
作者 | 陈立前
整理 | 纪妙
作者简介
陈立前,国防科技大学计算机学院副教授,主要从事程序分析与验证、抽象解释相关研究。在 ACM/IEEE Transactions、POPL、FSE 等期刊会议上发表论文多篇,获 ACM SIGSOFT 杰出论文奖(FSE 2020),出版教材译著 3 部。研究成果获省部级科技进步一等奖 1 项、二等奖 1 项。部分成果已在航天、国防等领域重大工程中应用。
视频回顾
# 研究背景 #
很多软件的代码里面都包含了大量的数值运算,如科学计算、金融、机器学习、物理模拟、统计分析等领域的软件。在嵌入式控制软件中,往往也会包含大量数值运算,而嵌入式控制软件在很多安全攸关的领域被大量使用,比如说在航天航空领域 GNC、姿轨控等相关的一些功能实现中都会用到数值运算。
这些软件数值运算中,用到的数值不像我们传统理解数学意义下的数,如实数和整数,而都是浮点数和机器整数。此外,在嵌入式软件设计中,往往会在事先设计时,把存储区域做一些划分,用来存一些数据,编写程序来操作这些存储区域时,会使用一些指针指向这些区域,然后使用指针算术访问数据。因此,这些软件中除了传统的数值运算,还涉及到一些指针算术的运算,当然我们可以把指针算术也看作是整数运算。
# 数值运算举例 - 求平均数
举一个简单的求两个数平均数的例子,可能大家第一印象会按照数学的模式去写,先做加法,然后再除以二,这样很容易就求得了两个数的平均数。这种在数学上大家肯定觉得没什么问题,但是如果在机器里面写成程序实现,这时候先做加法的话,x+y 很容易会出现浮点的上溢。而对于这种情况,如果把求平均数表达式稍微变一变,把参数先除以二,然后再做加法,你就会发现基本上就不会导致浮点上溢。
# 数值相关常见错误
正是因为这种数学上的运算,跟我们在机器里面的浮点数和整数的运算存在差异,有很多实数上的性质,对于浮点运算并不成立。那么,大家写程序的时候,可能很容易会导致一些数值相关的错误。有很多很常见的程序错误与此相关,比如说除零错、数组越界,浮点上溢、整数上溢等,还比如指针算术导致的非法指针访问。另外,程序中有大量的数值运算的话,还可能会导致一些计算精度的缺陷。
- 除零错、数组越界、浮点上溢、整数上溢等
- 指针算术导致的非法指针访问等
- 函数输入不在定义域内
- 计算精度缺陷
历史上出现的一些重大事件,比如像爱国者导弹防御系统拦截失败,阿丽亚娜 5 号火箭爆炸,openSSL 心脏滴血等,本质上都是跟数值相关的。有些是因为数值的溢出,有些是因为一些误差的累计,有些是因为缺少边界的检查,等等。
# 数值程序分析的思路
那么,我们应该如何检测这种错误呢?即通过 数值程序分析 来检测。
当程序写好了之后,我们可以通过程序分析的方法来检测这些错误。检测数值相关的错误的时候,最基础的一步是首先要生成不变式,有了不变式之后,我们再来分析这个程序里面的一些性质,检查一些性质是不是成立的,检测数值相关的缺陷,缺陷检测出来之后,我们还可以考虑缺陷修复。我们可以沿着这条途径来检测和修复数值相关的缺陷。
# 不变式生成
不变式生成 是数值程序分析中最基础的关键技术之一。不变式生成实际上是一个非常经典的课题,关注如何在每一个程序点处自动生成变量之间的不变式。我们关注的是 数值不变式,如下图红色标注的注释,就是不变式。有了不变式后,我们就可以去分析这个程序会不会出现一些数值相关的缺陷。
打个比方,对于上图这个非常简单的就一个变量和一个循环的程序。假设我们现在关注程序点 3 处的加法操作,若 x
是整型变量,加 1
会不会出现整数上溢呢?
这个问题实际上是跟 x
的类型有关,如果 x
是 char
类型,也就是 8
位类型的话,我们知道前面的不变式是 [0,255]
,如果再加上 1
,可能就会出现整数的上溢;当然如果 x
是 16
位的或者是 32
位的整型,那么这个加法操作就不会出现整数上溢问题。
因此,我们需要先拿到不变式,之后就可以检查程序语句是否满足数值的一些性质,或者说是否存在数值相关的一些缺陷。这是数值程序分析里面一个非常关键的技术。
业界有很多的方法来生成不变式,简单分为以下几类,比如最传统的 基于抽象解释 的,也可以用 基于约束求解 的方法来生成,最近几年也有用 机器学习 的方法来生成不变式,当然还有一些用动态的方法来生成一些 likely 不变式。
- 基于抽象解释
- 基于约束求解
- 基于学习
- 基于动态方法
今天这个报告,我主要介绍 基于抽象解释的不变式的生成,以及它在数值程序分析中的应用。
# 抽象解释 #
接下来,我先介绍一下抽象解释相关的理论。
# 抽象 & 近似
抽象解释是 1977 年提出来的 1,它最开始是用来对程序的语义进行 抽象(或近似) 的一种统一的框架。
这里的定义涉及到两个关键词,一个叫 抽象,一个叫 近似。我们应该怎么理解这两个词呢?接下来我会通过直观的例子来解释,希望能帮助大家理解。
首先,是对于 “近似” 的理解。比如说当我们用一个刻度尺去量一个物体的长度的时候,其实我们不能量出这个物体的精确的长度,但是我们人眼可以看到它大概是 3.4cm 左右,那么这个值实际上是一种近似。
接着,我们来看对 “抽象” (Abstraction) 的理解。比如说我们现在碰到这样一个问题,两个大数加起来之后再乘以一个大数的话,那么这个结果到底是正的还是负的呢?我们会首先想到先计算加法的结果,实际上这是一个比较大的值,然后再做乘法。如果用笔纸去算的话,那么需要费些时间才能把最终的值给算出来,并最后发现它是个正数。整个过程的计算代价还是比较大的。其实我们可以发现:如果两个数都是正数,那么两个正数相加的话,依然是个正数,然后再乘以另外一个正数,那么它的结果肯定也是个正数。这是一些简单的运算规则,但是根据这些规则计算的话,计算代价是非常低的。将每个数抽象为正负表示,这其实就是一种抽象的思想。通过抽象,我们把一些跟关注的问题本身无关的东西忽略掉。比如我们只关心这个结果是正的还是负的,而对它本身具体是什么值,我们并不关心,所以这个时候我们就可以把这个具体的值忽略掉,而只关心它的正负性,从而可以快速判定结果的正负。这就是抽象思想的体现。
抽象解释,为静态分析的设计提供了一个通用的框架,还可以用来自动生成程序的不变式。简单来讲,具体世界的状态比较多,它的取值的可能性也比较多,而中间的一些计算也比较繁琐或者代价比较大。那么我们就希望能通过一种抽象的方法,把它转到到一个抽象的空间里面来,使得在这个抽象的空间里面它的状态比较少,计算的代价也比较小,这样的话我们就能快速分析得到这个程序的一些性质。
# 伽罗瓦(Galois)连接
抽象解释里面最核心的一个概念叫做 伽罗瓦(Galois)连接:
( C , ≤ ) ⇌ α γ ( A , ⊑ ) (C, \leq) \xrightleftharpoons[\alpha]{\gamma} (A, \sqsubseteq) (C,≤)γ α(A,⊑)
定义如下:
对于给定的两个偏序集 ( C , ≤ ) (C, \leq) (C,≤) 和 ( A , ⊑ ) (A, \sqsubseteq) (A,⊑)( C C C 是 Concrete, A A A 是 Abstract),如果存在函数对 α : C → A \alpha:C \rightarrow A α:C→A 和 γ : A → C \gamma:A \rightarrow C γ:A→C 满足如下性质的话,那么我们认为这个函数对 ( α , γ ) (\alpha, \gamma) (α,γ) 是具体域 C C C 和抽象域 A A A 之间的 伽罗瓦连接。
∀ a ∈ A , c ∈ C : α ( c ) ⊑ a ⇔ c ≤ γ ( a ) \forall a \in A, c \in C : \alpha(c) \sqsubseteq a \Leftrightarrow c \leq \gamma(a) ∀a∈A,c∈C:α(c)⊑a⇔c≤γ(a)
该性质可以如此理解,假设把具体世界里的一个元素 c c c 抽象化之后得到