程序分析-基于SVF实现AnderSen指针分析算法

一.前言

1.1.指针分析

指针分析是数据流分析的一种,主要目的是计算运行时指针可能指向的内存区域。比如

p = &a;
q = p;

p t s ( p ) = p t s ( q ) = { a } pts(p) = pts(q) = \{a\} pts(p)=pts(q)={a}

指针分析可以应用于:

  • 建立变量之间的数据依赖关系。

  • 变量别名分析:在下面代码中p = &a; q = p; *p = x; y = *q;。因为 pq都指向同一块内存,所以 y 的值和 x 一样。

  • 编译优化和bug检测:

    • 常量传播:*p = 1; x = *q; 中,如果 pq 在任意情况下都是别名(must-aliases,在每个执行路径 pq 都指向同一块内存)那么 x 就是常量 1
    • 污点分析:*p = taintInput; x = *q; 如果 pq 是别名那么 x 可能受污点影响。

和其它程序分析技术一样,指针分析也可分为:

  • flow-insensitive 和 flow-sensitive

  • context-insensitive 和 context-sensitive

  • path-insensitive 和 path-sensitive

1.2.SVF

SVF是UTS Sui老师开发的一个基于LLVM的针对LLVM IR的程序静态分析工具。可以对

  • 基于LLVM IR的语言进行过程间依赖分析。

  • 执行指针别名分析。

  • value-flow追踪等。

详细介绍可参考SVF-概述

SVF中已经实现了一些AnderSen算法的变体,这里我关注最简单、原始版本的AnderSen算法。这里的内容参考了Sui老师的课件[2]

二.AnderSen算法

2.1.AnderSen算法约束

这里我主要探究AnderSen算法,一个flow-insensitive、context-insensitive、path-insensitive的算法。主要分析的对象是LLVM IR(首先将C代码用clang编译成LLVM IR),基于SVF的API实现简单的AnderSen算法。

AnderSen算法的约束如下:

Constraint TypeAssignmentConstraintMeaning
Basea = &b a ⊇ { b } a \supseteq \{b\} a{b} { b } ∈ p t s ( a ) \{b\} \in pts(a) {b}pts(a)
Simplea = b a ⊇ b a \supseteq b ab p t s ( a ) ⊇ p t s ( b ) pts(a) \supseteq pts(b) pts(a)pts(b)
Complexa = *b a ⊇ ∗ b a \supseteq *b ab ∀ v ∈ p t s ( b ) , p t s ( a ) ⊇ p t s ( v ) \forall v \in pts(b), pts(a) \supseteq pts(v) vpts(b),pts(a)pts(v)
Complex*a = b ∗ a ⊇ b *a \supseteq b ab ∀ v ∈ p t s ( a ) , p t s ( v ) ⊇ p t s ( b ) \forall v \in pts(a), pts(v) \supseteq pts(b) vpts(a),pts(v)pts(b)

而SVF中实现了多种AnderSen-style算法,而我目前实现的简易AnderSen算法只关注4种基本情况,对应LLVM IR种存在的4种约束。

Constraint TypeAssignmentLLVM IRConstraint Rule
Addressa = &b%a = alloca //b p t s ( a ) = p t s ( a ) ∪ { b } pts(a) = pts(a) \cup \{b\} pts(a)=pts(a){b}
Copya = b%a = bitcast %b p t s ( a ) = p t s ( a ) ∪ p t s ( b ) pts(a) = pts(a) \cup pts(b) pts(a)=pts(a)pts(b)
Loada = *b%a = load %b ∀ v ∈ p t s ( b ) , p t s ( a ) = p t s ( a ) ∪ p t s ( v ) \forall v \in pts(b), pts(a) = pts(a) \cup pts(v) vpts(b),pts(a)=pts(a)pts(v)
Store*a = bstore %b, %a ∀ v ∈ p t s ( a ) , p t s ( v ) = p t s ( v ) ∪ p t s ( b ) \forall v \in pts(a), pts(v) = pts(v) \cup pts(b) vpts(a),pts(v)=pts(v)pts(b)
  • 在执行AnderSen算法之前,需要将C语言代码编译成LLVM IR并通过SVF的API构造相应的Constraint Graph。

  • 之后在Constraint Graph上执行AnderSen算法,在执行AnderSen算法的时候Constraint Graph会发生变动。

这里先从[1]中摘取AnderSen算法的流程

在这里插入图片描述

2.2.示例

2.2.1.将C语言源代码编译为SSA形式的LLVM IR

这里引用1个简单的例子,C语言代码如下(example.c):

void swap(char **p, char **q){
    char* t = *p;
    *p = *q;
    *q = t;
}

int main(){
    char a1, b1;
    char *a = &a1;
    char *b = &b1;
    swap(&a,&b);
}

然后分别用

  • clang -c -S -fno-discard-value-names -Xclang -disable-O0-optnone -emit-llvm example.c -o example.ll 保留变量名将C代码编译为LLVM IR。

  • opt -S -mem2reg example.ll -o example.ll 将LLVM IR转化为SSA形式的IR。

完成编译后SSA形式的LLVM IR如下:

define dso_local void @swap(i8** %p, i8** %q) #0 {
entry:
  %0 = load i8*, i8** %p, align 8
  %1 = load i8*, i8** %q, align 8
  store i8* %1, i8** %p, align 8
  store i8* %0, i8** %q, align 8
  ret void
}

; Function Attrs: noinline nounwind uwtable
define dso_local i32 @main() #0 {
entry:
  %a1 = alloca i8, align 1
  %b1 = alloca i8, align 1
  %a = alloca i8*, align 8
  %b = alloca i8*, align 8
  store i8* %a1, i8** %a, align 8
  store i8* %b1, i8** %b, align 8
  call void @swap(i8** %a, i8** %b)
  ret i32 0
}

未转化成SSA的IR如下:

define dso_local void @swap(i8** %p, i8** %q) #0 {
entry:
  %p.addr = alloca i8**, align 8
  %q.addr = alloca i8**, align 8
  %t = alloca i8*, align 8
  store i8** %p, i8*** %p.addr, align 8
  store i8** %q, i8*** %q.addr, align 8
  %0 = load i8**, i8*** %p.addr, align 8
  %1 = load i8*, i8** %0, align 8
  store i8* %1, i8** %t, align 8
  %2 = load i8**, i8*** %q.addr, align 8
  %3 = load i8*, i8** %2, align 8
  %4 = load i8**, i8*** %p.addr, align 8
  store i8* %3, i8** %4, align 8
  %5 = load i8*, i8** %t, align 8
  %6 = load i8**, i8*** %q.addr, align 8
  store i8* %5, i8** %6, align 8
  ret void
}

; Function Attrs: noinline nounwind uwtable
define dso_local i32 @main() #0 {
entry:
  %a1 = alloca i8, align 1
  %b1 = alloca i8, align 1
  %a = alloca i8*, align 8
  %b = alloca i8*, align 8
  store i8* %a1, i8** %a, align 8
  store i8* %b1, i8** %b, align 8
  call void @swap(i8** %a, i8** %b)
  ret i32 0
}

明显SSA形式的更简单,之后的分析也建立在SSA形式IR之上。

2.2.2.构造程序赋值图(Program Assignment Graph,PAG)

程序赋值图和约束图(Constraint Graph)广义上来说是同一个图,只不过在执行指针分析过程中,PAG保持不变,CG可能会改变。

上面SSA IR对应的PAG如下(简化版):

请添加图片描述

SVF版AnderSen算法伪代码如下:

在这里插入图片描述

2.2.3.执行AnderSen算法

首先是初始化部分,初始化部分主要关注LLVM IR中的 alloca 语句,在PAG中就是address边(绿色的边),从中初始化部分变量的 pts 集和 WorkList 。初始化之后部分变量的 pts 集如下:

  • pts(a1) = { o1 }

  • pts(a2) = { o2 }

  • pts(a) = { o3 }

  • pts(b) = { o4 }

WorkList 从头到尾依次为 %a1, %b1, %a, %b

请添加图片描述

AnderSen算法主循环过程中,会发生2个变化:

  • 约束图中会不断新添加新的Copy边(黑色)。

  • 约束图中变量的 pts 集会变大(不会变小)。

WorkList 开头4个元素(%a1 -> %b)遍历完之后,约束图、pts 集和 WorkList 变动如下:

请添加图片描述可以看出:

  • 约束图添加了 %a1 -> o3%b1 -> o4 2条Copy边。

  • %p%qpts 集被更新。

  • WorkList 新添加了 %a1, %p, %b1, %q

在现在的 WorkList 开头4个元素(%a1 -> %q) 遍历完之后,约束图的变化如下:

请添加图片描述

可以看到:

  • 约束图多了 %1 -> o3%0 -> o4o3 -> %0o4 -> %1 2条Copy边。

  • o3o4pts 集被更新。

  • WorkList 从头至尾依次为 o3, %1, o3, o4, %0, o4

再次遍历完 WorkList 中的6个元素,约束图变化如下:

请添加图片描述

可以看到约束图中已经没有新的Copy边被添加进来,发生的变化有:

  • o3%0%1pts 集发生了变化。

  • WorkList 有新元素添加。

最后经过几轮迭代,算法也达到收敛状态,收敛状态如下图所示:

请添加图片描述
至此AnderSen算法也算运行完毕,那么下面就看下在SVF中这些元素是以什么样的形式存在的。

可以注意到AnderSen算法运行过程中,需要做的操作有:

  • 修改变量(结点)的 pts 集。

  • 往约束图上添加Copy边。

2.3.SVF版AnderSen算法

我的运行环境如下:

  • Ubuntu 18.04

  • LLVM-12.0.0

  • SVF-2.2

2.3.1.SVF API

AnderSen算法实现可参考SVF wiki。如果需要自定义AnderSen算法只需写类继承AndersenBase类即可。需要自定义的函数是 processAllAddrsolveWorklistaddCopyEdge,分别对应初始化、主体循环和添加Copy边。

AnderSen算法是在约束图上运行的,约束图由程序赋值图 build 而成。因此需要了解的SVF中的数据类型主要有:

  • 约束图的定义:参考ConstraintGraph类,在AnderSen算法实现种用到了这个类的 addAddrCGEdgegetConstraintNode 函数。

  • 边的定义:参考ConstraintEdge类、连接的结点类型为 ConstraintNode 类。成员变量 ConstraintEdgeK 包括 Addr, Copy, Store, Load, NormalGep, VariantGep 6种,不过我这里只关注前4种,后2种在field-sensitive的分析中会用到,成员变量还有 edgeId 用来标识边。算法中还用到了父类的 getSrcIDgetDstID 方法。

  • 结点的定义:参考ConstraintNode类,其继承于GenericNode类。成员变量包括12个EdgeSet,分别保存6种 ConstraintEdge 的所有入边出边,以及 nodeID 来标识结点。

这里还有程序赋值图的定义,约束图可以看作是程序赋值图的简化,方便指针分析算法执行,不考虑结点中的信息。分别包含 PAG 类、PAGEdge类和PAGNode类。这里

  • PAGEdge 类相对 ConstraintEdge 类,种类包括 Addr, Copy, Store, Load, Call, Ret, NormalGep, VariantGep, ThreadFork, ThreadJoin, Cmp, BinaryOp, UnaryOp

  • PAGNode 相对 ConstraintNode 多了种类信息,类别包括 ValNode, ObjNode, RetNode, VarargNode, GepValNode, GepObjNode, FIObjNode, DummyValNode, DummyObjNode, CloneGepObjNode, CloneFIObjNode, CloneDummyObjNode. 以及多了一些成员变量。

可以说 ConstraintGraph 只包含了 PAG 的拓扑信息,而不包含 PAG 的语义信息

上面示例的 PAG 如下图所示:

在这里插入图片描述
运行完AnderSen算法后约束图如下:

在这里插入图片描述
可以看到PAG的每个结点的内容为一个语句,约束图中每个结点仅包含ID,部分结点还包含了变量信息。

在上面两个图中:

  • 18对应 O1、20对应 O2 、22对应O3、24对应 O4

  • 17对应 %a1、19对应 %b1、21对应 %a、23对应 %b

  • 7对应 %p、8对应 %q

  • 9对应 %0、10对应 %1

2.3.2.AnderSen算法

完整的AnderSen算法代码如下(AnderSen.h):

#include "SVF-FE/LLVMUtil.h"
#include "SVF-FE/PAGBuilder.h"
#include "WPA/Andersen.h"

using namespace SVF;
using namespace llvm;
using namespace std;

class AndersenPTA: public SVF::AndersenBase{
public:
    // Constructor
    AndersenPTA(SVF::PAG* _pag) : AndersenBase(_pag){};

    //dump constraint graph
    void dump_consCG(string name){
        consCG->dump(name);
    };

private:

    // To be implemented
    void processAllAddr();

    // To be implemented
    virtual void solveWorklist();

    /// Add copy edge on constraint graph
    virtual bool addCopyEdge(SVF::NodeID src, SVF::NodeID dst){
        if (consCG->addCopyCGEdge(src, dst))
            return true;
        else
            return false;
    }

};



// TODO: Implement your Andersen's Algorithm here
void AndersenPTA::solveWorklist(){
    processAllAddr();

    // Andersen's worklist-based transitive closure solving starts from  here
    // Keep solving until workList is empty.
    while (!isWorklistEmpty()){
        NodeID nodeId = popFromWorklist();
        ConstraintNode *node = consCG->getConstraintNode(nodeId);

        /// foreach o \in pts(p)
        for (NodeID o : getPts(nodeId)) {
            /// *p = q  pts(q) \subseteq pts(o)
            for (ConstraintEdge *edge: node->getStoreInEdges())
                if (addCopyEdge(edge->getSrcID(), o))
                    pushIntoWorklist(edge->getSrcID());

            // r = *p   pts(o) \subseteq pts(r)
            for (ConstraintEdge *edge: node->getLoadOutEdges())
                if (addCopyEdge(o, edge->getDstID()))
                    pushIntoWorklist(o);
        }
        /// q = p or q = &p->f  pts(p) \subseteq pts(q)
        for (ConstraintEdge *edge : node->getDirectOutEdges())
            if(unionPts(edge->getDstID(),edge->getSrcID()))
                pushIntoWorklist(edge->getDstID());
    }
}


// TODO: Initialize each pointer at the address constraint
void AndersenPTA::processAllAddr(){
    for (ConstraintGraph::const_iterator nodeIt = consCG->begin(), nodeEit = consCG->end(); nodeIt != nodeEit; nodeIt++){
        ConstraintNode *cgNode = nodeIt->second;
        for (ConstraintNode::const_iterator it = cgNode->incomingAddrsBegin(), eit = cgNode->incomingAddrsEnd();
             it != eit; ++it){
            /// Implement your code here:
            numOfProcessedAddr++;

            const AddrCGEdge *addr = SVFUtil::cast<AddrCGEdge>(*it);
            NodeID dst = addr->getDstID();
            NodeID src = addr->getSrcID();
            if (addPts(dst, src))
                pushIntoWorklist(dst);
        }
    }
}

main 函数代码如下:

#include <iostream>
#include "SVF-FE/LLVMUtil.h"
#include "SVF-FE/PAGBuilder.h"
#include "AnderSen.h"

using namespace llvm;
using namespace std;
using namespace SVF;

int main(int argc, char **argv) {
    int arg_num = 0;
    char **arg_value = new char*[argc];
    std::vector<std::string> moduleNameVec;
    SVFUtil::processArguments(argc, argv, arg_num, arg_value, moduleNameVec);
    cl::ParseCommandLineOptions(arg_num, arg_value, "Whole Program Points-to Analysis\n");

    SVFModule* svfModule = LLVMModuleSet::getLLVMModuleSet()->buildSVFModule(moduleNameVec);
    /// Build Program Assignment Graph (PAG)
    SVF::PAGBuilder builder;
    SVF::PAG *pag = builder.build(svfModule);
    pag->dump ("pag");

    AndersenPTA *andersenPTA = new AndersenPTA(pag);
    andersenPTA->analyze();
    andersenPTA->dump_consCG("consG");

    SVF::LLVMModuleSet::releaseLLVMModuleSet();
    SVF::PAG::releasePAG();
    delete andersenPTA;
    return 0;
}

约束图中每个结点的 pts 集为其它结点。获取 pts 集用到了 getPts 方法,这个方法最初在BVDataPTAImpl类中定义,而 AndersenBase 继承了 BVDataPTAImpl 类,并覆写了该方法。它底层调用了PTDataTy类的 getPts 方法。

typedef PTData<NodeID, NodeBS, NodeID, PointsTo> PTDataTy

...

virtual inline const PointsTo& getPts(NodeID id){
   return ptD->getPts(id);
}

可以看到 getPts 方法返回值是PointsTo类型,其中 PointsTo 本质是llvm::SparseBitVector<>

typedef llvm::SparseBitVector<> NodeBS;
typedef NodeBS PointsTo;

2.3.3.程序执行流程

从代码中可以看出整个程序的执行流程如下:

Created with Raphaël 2.3.0 开始 加载LLVM IR build PAG 从PAG构建约束图 在约束图上执行AnderSen算法 结束

我这里只探究了AnderSen算法的实现,并没有探究build PAG和构建约束图的过程。

2.3.4.应用

指针分析的一个应用是变量别名分析,别名分析的结果用llvm中的AliasResult表示:

enum AliasResult : uint8_t {
  NoAlias = 0,
  MayAlias,
  PartialAlias,
  MustAlias,
};

包含4种结果,基类 PointerAnalysis (继承链 AndersenBase -> BVDataPTAImpl -> PointerAnalysis)提供alias方法查询2个元素(PAG结点、Value 或者 MemoryLocation )是否存在别名,具体实现在PointerAnalysisImpl.cpp,这里直接调用。

在上述例子中调用 andersenPTA->alias(9, 22);(分析9号和22号PAG结点,分别对应 %0o3)得到的结果是 MayAlias。参考BVDataPTAImpl::alias

AliasResult BVDataPTAImpl::alias(const PointsTo& p1, const PointsTo& p2){
    PointsTo pts1;
    expandFIObjs(p1,pts1);
    PointsTo pts2;
    expandFIObjs(p2,pts2);
    if (containBlackHoleNode(pts1) || containBlackHoleNode(pts2) || pts1.intersects(pts2))
        return llvm::MayAlias;
    else
        return llvm::NoAlias;
}

传入pts集合计算别名返回值只有 MayAliasNoAlias 两种。至于 MustAliasPartialAlias 以后再探索吧。

三.总结

这里我参考SVF的教程实现了一个简单的flow-insensitive和field-insensitive的AnderSen算法,算法不难不过SVF内部构造PAG和Constraint Graph的过程我这里没探究,以及PAG结点、别名分析过程都没探究。以后会慢慢研究这些过程。

SVF中也实现了一些AnderSen算法的变体,以及其它flow-sensitive的算法,以后我也会深入研究这块内容。

四.参考文献

[1] Pointer Analysis. CS252r Spring 2011
[2] Teaching-Software-Analysis

  • 12
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: 这是一个Docker命令,用于创建一个软链接。 - `l`: 创建软链接 - `n`: 如果软链接存在,不进行覆盖 - `s`: 创建符号链接 - `v`: 显示详细信息 - `f`: 强制执行操作 具体用法如下: ``` docker ln -svfn /path/to/source /path/to/destination ``` 其中,`/path/to/source` 是源文件或目录的路径,`/path/to/destination` 是链接的目标路径。使用此命令可以将源文件或目录链接到指定的目标路径。 ### 回答2: 在Docker中,`ln -svf`是一个用于创建软链接的命令。下面对每个选项进行解释: - `ln`:ln代表链接命令,用于创建链接。 - `-s`:这个选项表示创建软链接(符号链接),而不是硬链接。软链接是指向源文件的指针,类似于快捷方式。 - `-v`:此选项用于在创建链接之前,在终端中显示详细的输出。它会显示运行命令并创建链接的过程。 - `-f`:此选项用于强制执行操作,即使存在同名的链接。如果已经存在符号链接,通过使用`-f`选项,它将被覆盖并重新创建。 综上所述,`ln -svf`用于在Docker中创建一个软链接(符号链接),并在创建之前显示详细的输出。如果同名链接已经存在,则通过使用`-f`选项,它将被覆盖并重新创建。 ### 回答3: "dock ln -svf" 是一个Docker命令,用于创建软链接来指向Docker容器中的文件或目录。 其中,ln是Linux系统中的软链接命令,用于创建一个文件或目录的软链接。-s选项表示创建软链接,-v选项表示显示链接的详细信息,-f选项表示如果目标已经存在则强制覆盖。 在Docker中使用"docker ln -svf"命令时,通常会有两个参数。第一个参数是链接的目标,即要创建软链接的文件或目录的路径。第二个参数是链接的链接名,即创建的软链接的名称。 该命令的作用是在Docker容器中创建一个指向指定文件或目录的软链接。通过软链接,可以方便地引用容器中的文件或目录,而不需要知道其真实位置。软链接可以与其他容器或主机系统进行交互,使得文件的访问和管理更加灵活和方便。 需要注意的是,使用该命令需要在容器内部执行,不能直接在主机系统上执行。另外,软链接只在容器内有效,在主机系统上可能无法直接访问软链接文件或目录。 总之,"docker ln -svf"命令是在Docker容器内部创建软链接的命令,可以提供方便的文件和目录访问方式,增强容器的灵活性和可管理性。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值