CodeQL 文档

Go 的 CodeQL 库

分析 Go 程序时,您可以使用 Go 的 CodeQL 库中大量类。

概述

CodeQL 附带一个用于分析 Go 代码的扩展库。该库中的类以面向对象的形式呈现 CodeQL 数据库中的数据,并提供抽象和谓词来帮助您完成常见的分析任务。

该库实现为一组 QL 模块,即扩展名为 .qll 的文件。模块 go.qll 导入大多数其他标准库模块,因此您可以通过在查询开头包含以下内容来包含完整的库

import go

从广义上讲,Go 的 CodeQL 库提供了对 Go 代码库的两种视图:在 语法级别 上,源代码表示为一个 抽象语法树 (AST),而在 数据流级别 上,它表示为一个 数据流图 (DFG)。在这两者之间,还有一个程序的中间表示形式,即控制流图 (CFG),尽管这种表示形式本身很少有用,主要用于构建更高级别的 DFG 表示形式。

AST 表示形式捕获了程序的语法结构。您可以使用它来推断语法属性,例如语句在彼此之间的嵌套方式,以及表达式的类型以及名称引用的变量。

另一方面,DFG 提供了对数据如何在运行时通过变量和操作流动的近似值。例如,安全查询使用它来建模用户控制的输入如何通过程序传播。此外,DFG 包含有关给定调用可能调用哪个函数的信息(考虑通过接口进行的虚拟调度),以及有关在运行时不同操作执行顺序的控制流信息。

一般来说,通常只应在表面语法查询中使用 AST。任何涉及程序更深层语义属性的分析都应在 DFG 上进行。

本教程的其余部分简要总结了该库提供的最重要的类和谓词,包括对相应的 详细 API 文档 的引用(如果适用)。我们首先概述 AST 表示形式,然后解释名称和实体,它们用于表示名称绑定信息,以及类型和类型信息。然后我们将继续讨论控制流和数据流图,最后介绍调用图和一些高级主题。

抽象语法

AST 将程序呈现为节点的层次结构,每个节点对应于程序源代码的语法元素。例如,程序中的每个表达式和每个语句都对应一个 AST 节点。这些 AST 节点按父子关系排列,反映了语法元素的嵌套方式以及内部元素在封闭元素中出现的顺序。

例如,这是表达式 (x + y) * z 的 AST

ast

它由六个 AST 节点组成,分别表示 xyx + y(x + y)z 和整个表达式 (x + y) * z。表示 xy 的 AST 节点是表示 x + y 的 AST 节点的子节点,x 是第零个子节点,y 是第一个子节点,反映了它们在程序文本中的顺序。类似地,x + y(x + y) 的唯一子节点,它是 (x + y) * z 的第零个子节点,它的第一个子节点是 z

所有 AST 节点都属于类 AstNode,它定义了通用的树遍历谓词

  • getChild(i):返回此 AST 节点的第 i 个子节点。
  • getAChild():返回此 AST 节点的任何子节点。
  • getParent():返回此 AST 节点的父节点(如果有)。

这些谓词只能用于执行通用的 AST 遍历。要访问特定 AST 节点类型的子节点,应使用下面介绍的专用谓词。特别是,查询不应依赖于子节点相对于父节点的数字索引:这些被视为实现细节,可能会在库的不同版本之间发生变化。

AstNode 节点中的谓词 toString() 提供了 AST 节点的简短描述,通常只是表明它是什么类型的节点。谓词 toString() 提供访问与 AST 节点对应的源代码的权限。源代码未存储在数据集中,因此 CodeQL 查询无法直接访问它。

AstNode 中的谓词 getLocation() 返回一个 Location 实体,描述了 AST 节点所表示的程序元素的源代码位置。您可以使用它的成员谓词 getFile()getStartLine()getStartColumngetEndLine()getEndColumn() 来获取有关其文件、开始行和列以及结束行和列的信息。

AstNode 的最重要的子类是 StmtExpr,它们分别表示语句和表达式。本节简要讨论了它们中一些更重要的子类和谓词。有关 StmtExpr 的所有子类的完整参考,请参见 用于 Go 的抽象语法树类

语句

  • ExprStmt:表达式语句;使用 getExpr() 访问表达式本身
  • Assignment:赋值语句;使用 getLhs(i) 访问第 i 个左侧,使用 getRhs(i) 访问第 i 个右侧;如果只有一个左侧,您可以使用 getLhs(),右侧也是如此
    • SimpleAssignStmt:不涉及复合运算符的赋值语句
      • AssignStmt:形式为 lhs = rhs 的简单赋值语句
      • DefineStmt:形式为 lhs := rhs 的简写变量声明
    • CompoundAssignStmt:具有复合运算符的赋值语句,例如 lhs += rhs
  • IncStmtDecStmt:分别表示递增语句或递减语句;使用 getOperand() 访问正在递增或递减的表达式
  • BlockStmt:花括号之间的语句块;使用 getStmt(i) 访问块中的第 i 个语句
  • IfStmtif 语句;使用 getInit()getCond()getThen()getElse() 分别访问(可选)初始化语句、正在检查的条件、如果条件为真则要评估的“then”分支以及(可选)如果条件为假则要评估的“else”分支
  • LoopStmt:循环;使用 getBody() 访问其主体
    • ForStmtfor 语句;使用 getInit()getCond()getPost() 分别访问初始化语句、循环条件和后置语句,所有这些都是可选的
    • RangeStmt: 一个 range 语句;使用 getDomain() 访问迭代域,使用 getKey()getValue() 访问分别被赋予给后续键和值的表达式(如果有)
  • GoStmt: 一个 go 语句;使用 getCall() 访问在新 goroutine 中被评估的调用表达式
  • DeferStmt: 一个 defer 语句;使用 getCall() 访问被延期的调用表达式
  • SendStmt: 一个发送语句;分别使用 getChannel()getValue() 访问通道和正在通过通道发送的值
  • ReturnStmt: 一个 return 语句;使用 getExpr(i) 访问第 i 个返回表达式;如果只有一个返回表达式,可以使用 getExpr() 代替
  • BranchStmt: 中断结构化控制流的语句;使用 getLabel() 获取可选的目标标签
    • BreakStmt: 一个 break 语句
    • ContinueStmt: 一个 continue 语句
    • FallthroughStmt: 一个位于 switch case 结尾的 fallthrough 语句
    • GotoStmt: 一个 goto 语句
  • DeclStmt: 一个声明语句,使用 getDecl() 访问此语句中的声明;请注意,人们很少需要直接处理声明语句,因为对它们声明的实体进行推理通常更容易
  • SwitchStmt: 一个 switch 语句;使用 getInit() 访问(可选的)初始化语句,使用 getCase(i) 访问第 icasedefault 子句
    • ExpressionSwitchStmt: 一个检查表达式的值的 switch 语句
    • TypeSwitchStmt: 一个检查表达式的类型的 switch 语句
  • CaseClause: switch 语句中的 casedefault 子句;使用 getExpr(i) 访问第 i 个表达式,使用 getStmt(i) 访问此子句主体中的第 i 个语句
  • SelectStmt: 一个 select 语句;使用 getCommClause(i) 访问第 icasedefault 子句
  • CommClause: select 语句中的 casedefault 子句;使用 getComm() 访问此子句的发送/接收语句(default 子句未定义),使用 getStmt(i) 访问此子句主体中的第 i 个语句
  • RecvStmt: select 语句的 case 子句中的接收语句;使用 getLhs(i) 访问此语句的第 i 个左侧,使用 getExpr() 访问底层的接收表达式

表达式

Expression 有一个谓词 isConst(),如果表达式是编译时常量,则此谓词为真。对于这样的常量表达式,可以使用 getNumericValue()getStringValue() 来确定它们的数值和字符串值,分别。请注意,这些谓词对于无法在编译时确定其值的表达式没有定义。还要注意,getNumericValue() 的结果类型是 QL 类型 float。如果表达式的数值无法表示为 QL float,此谓词也没有定义。在这种情况下,可以使用 getExactValue() 获取常量值的字符串表示。

  • Ident: 一个标识符;使用 getName() 访问其名称
  • SelectorExpr: 形如 base.sel 的选择器;使用 getBase() 访问点之前的部分,使用 getSelector() 访问点之后的标识符
  • BasicLit: 基本类型的字面量;子类 IntLitFloatLitImagLitRuneLitStringLit 代表各种特定类型的字面量
  • FuncLit: 一个函数字面量;使用 getBody() 访问函数的主体
  • CompositeLit: 一个复合字面量;使用 getKey(i)getValue(i) 分别访问第 i 个键和第 i 个值
  • ParenExpr: 一个带括号的表达式;使用 getExpr() 访问括号之间的表达式
  • IndexExpr: 一个索引表达式 base[idx];分别使用 getBase()getIndex() 访问 baseidx
  • SliceExpr: 一个切片表达式 base[lo:hi:max];分别使用 getBase()getLow()getHigh()getMax() 访问 baselohimax;请注意,lohimax 可以省略,在这种情况下相应的谓词没有定义
  • ConversionExpr: 一个转换表达式 T(e);分别使用 getTypeExpr()getOperand() 访问 Te
  • TypeAssertExpr: 一个类型断言 e.(T);分别使用 getExpr()getTypeExpr() 访问 eT
  • CallExpr: 一个调用表达式 callee(arg0, ..., argn);使用 getCalleeExpr() 访问 callee,使用 getArg(i) 访问第 i 个参数
  • StarExpr: 一个星号表达式,根据上下文,它可能是一个指针类型表达式或一个指针解引用表达式;使用 getBase() 访问星号的操作数
  • TypeExpr: 表示类型的表达式
  • OperatorExpr: 带有一元或二元运算符的表达式;使用 getOperator() 访问运算符
    • UnaryExpr: 带有一元运算符的表达式;使用 getAnOperand() 访问运算符的操作数
    • BinaryExpr: 带有二元运算符的表达式;分别使用 getLeftOperand()getRightOperand() 访问左侧和右侧操作数
      • ComparisonExpr: 执行比较的二元表达式,包括相等性测试和关系比较
        • EqualityTestExpr: 等式测试,即 ==!=;谓词 getPolarity() 的结果对于前者为 true,对于后者为 false
        • RelationalComparisonExpr: 关系比较;使用 getLesserOperand()getGreaterOperand() 分别访问比较的较小运算符和较大运算符;如果使用 <> 进行严格比较,则 isStrict() 为真,而不是 <=>=

名称

虽然 IdentSelectorExpr 是非常有用的类,但它们通常过于笼统:Ident 涵盖程序中的所有标识符,包括出现在声明中的标识符以及引用,并且不区分引用包、类型、变量、常量、函数或语句标签的名称。类似地,SelectorExpr 可能引用包、类型、函数或方法。

Name 及其子类提供对该空间的更细粒度的映射,沿着结构和命名空间这两个轴进行组织。在结构方面,名称可以是 SimpleName,这意味着它是一个简单标识符(因此是一个 Ident),或者它可以是 QualifiedName,这意味着它是一个限定标识符(因此是一个 SelectorExpr)。在命名空间方面,Name 可以是 PackageNameTypeNameValueNameLabelNameValueName 又可以是 ConstantNameVariableNameFunctionName,具体取决于名称引用的实体类型。

ReferenceExpr 提供了相关的抽象:引用表达式是一个表达式,它引用变量、常量、函数、字段或数组或切片的元素。使用谓词 isLvalue()isRvalue() 分别确定引用表达式是否出现在语法上下文中,在该上下文中它被分配到或从中读取。

最后,ValueExprReferenceExpr 概括为包括所有其他可以评估为值的表达式(与引用包、类型或语句标签的表达式相反)。

函数

在语法级别,函数以两种形式出现:在函数声明中(由类 FuncDecl 表示)以及作为函数字面量(由类 FuncLit 表示)。由于通常方便地推理任一类型的函数,因此这两个类共享一个共同的超类 FuncDef,它定义了一些有用的成员谓词

  • getBody() 提供对函数体的访问
  • getName() 获取函数名称;对于函数字面量来说,它没有定义,因为函数字面量没有名称
  • getParameter(i) 获取函数的第 i 个参数
  • getResultVar(i) 获取函数的第 i 个结果变量;如果只有一个结果,则可以使用 getResultVar() 来访问它
  • getACall() 获取一个数据流节点(见下文),该节点表示对该函数的调用

实体和名称绑定

并非所有代码库的元素都能够表示为 AST 节点。例如,在标准库或依赖项中定义的函数在程序本身的源代码中没有源级定义,并且像 len 这样的内置函数根本没有定义。因此,函数不能简单地与其定义标识,类似地对于变量、类型等等。

为了消除这种差异,并提供对函数的统一视图,无论它们在哪里定义,Go 库引入了 实体 的概念。实体是一个命名的程序元素,即包、类型、常量、变量、字段、函数或标签。所有实体都属于类 Entity,它定义了一些有用的谓词

  • getName() 获取实体的名称
  • hasQualifiedName(pkg, n) 如果该实体是在包 pkg 中声明的,并且具有名称 n,则为真;此谓词仅针对类型、函数和包级变量和常量定义(但不针对方法或局部变量)
  • getDeclaration() 将实体连接到其声明标识符(如果有的话)
  • getAReference() 获取一个引用该实体的 Name

相反,类 Name 定义了一个谓词 getTarget(),它获取名称引用的实体。

Entity 有几个子类表示特定类型的实体:PackageEntity 用于包;TypeEntity 用于类型;ValueEntity 用于常量 (Constant)、变量 (Variable) 和函数 (Function);以及 Label 用于语句标签。

Variable 又有一些子类表示特定类型的变量:LocalVariable 是在局部范围内声明的变量,即不在包级别;ReceiverVariableParameterResultVariable 分别描述接收器、参数和结果,并定义了一个谓词 getFunction() 来访问相应的函数。最后,类 Field 表示结构体字段,并提供一个成员谓词 hasQualifiedName(pkg, tp, f),如果该字段具有名称 f 并且属于包 pkg 中的类型 tp,则为真。(请注意,由于嵌套,同一个字段可能属于多个类型。)

Function 具有一个子类 Method,表示方法(包括接口方法和在命名类型上定义的方法)。类似于 FieldMethod 提供一个成员谓词 hasQualifiedName(pkg, tp, m),如果该方法具有名称 m 并且属于包 pkg 中的类型 tp,则为真。谓词 implements(m2) 如果该方法实现了方法 m2,则为真,即它与 m2 具有相同的名称和签名,并且它属于实现 m2 所属接口的类型。对于任何函数,getACall() 提供对可能调用该函数的调用站点的访问,可能通过虚拟分派进行。

最后,模块 Builtin 提供了一种方便的方法来查找与内置函数和类型相对应的实体。例如,Builtin::len() 是表示内置函数 len 的实体,Builtin::bool()bool 类型,Builtin::nil() 是值 nil

类型信息

类型由类 Type 及其子类表示,例如 BoolType 用于内置类型 boolNumericType 用于各种数值类型,包括 IntTypeUint8TypeFloat64Type 等;StringType 用于类型 stringNamedTypeArrayTypeSliceTypeStructTypeInterfaceTypePointerTypeMapTypeChanType 分别用于命名类型、数组、切片、结构体、接口、指针、映射和通道。最后,SignatureType 表示函数类型。

请注意,类型 BoolType 与实体 Builtin::bool() 不同:后者将 bool 视为声明的实体,前者将其视为类型。但是,可以使用谓词 getEntity() 将类型映射到其对应的实体(如果有的话)。

Expr 和类 Entity 都定义了一个谓词 getType() 来确定表达式或实体的类型。如果无法确定表达式或实体的类型(例如,由于在提取过程中无法找到某些依赖项),它将与类 InvalidType 的无效类型关联。

控制流

大多数 CodeQL 查询编写者很少直接使用程序的控制流表示,但了解其工作原理仍然很有用。

与将程序视为 AST 节点层次结构的抽象语法树不同,控制流图将其视为 控制流节点 的集合,每个节点代表在运行时执行的单个操作。这些节点通过表示操作执行顺序的(有向)边相互连接。

例如,考虑以下代码片段

x := 0
if p != nil {
  x = p.f
}
return x

在 AST 中,它被表示为一个 IfStmt 和一个 ReturnStmt,前者具有一个 NeqExpr 和一个 BlockStmt 作为其子节点,等等。这提供了代码语法结构的非常详细的图片,但它并没有立即帮助我们推断比较和赋值等各种操作的执行顺序。

在 CFG 中,有对应于 x := 0p != nilx = p.freturn x 的节点,以及其他一些节点。这些节点之间的边模拟了这些语句和表达式的可能执行顺序,如下所示(为了演示目的有所简化)

cfg

例如,从 p != nilx = p.f 的边模拟了比较结果为 true 并执行“then”分支的情况,而从 p != nilreturn x 的边模拟了比较结果为 false 并跳过“then”分支的情况。

特别要注意,CFG 节点可以有多个输出边(例如从 p != nil)以及多个输入边(例如进入 return x),以代表运行时的控制流分支。

还要注意,只有执行某种值操作的 AST 节点才有相应的 CFG 节点。这包括表达式(例如比较 p != nil)、赋值语句(例如 x = p.f)和返回语句(例如 return x),但不包括仅起语法作用的语句(例如块语句)和其语义已由 CFG 边反映的语句(例如 if 语句)。

重要的是要指出,CodeQL 库为 Go 提供的控制流图仅模拟 局部 控制流,即单个函数内的流。例如,从函数调用到它们调用的函数的流不受控制流边的表示。

在 CodeQL 中,控制流节点由类 ControlFlow::Node 表示,节点之间的边由 ControlFlow::Node 的成员谓词 getASuccessor()getAPredecessor() 捕获。除了表示运行时操作的控制流节点外,每个函数还具有一个合成入口节点和一个退出节点,分别代表函数执行的开始和结束。这些存在是为了确保对应于函数的控制流图具有唯一的入口节点和唯一的退出节点,这是许多标准控制流分析算法所必需的。

数据流

在数据流级别,程序被认为是 数据流节点 的集合。这些节点通过表示数据在运行时通过程序流动方式的(有向)边相互连接。

例如,有对应于表达式的数据流节点以及对应于变量(更准确地说,SSA 变量)的其他数据流节点。以下是对应于上面显示的代码片段的数据流图,为了简单起见,忽略了 SSA 转换

dfg

请注意,与控制流图不同,赋值 x := 0x = p.f 不被表示为节点。相反,它们被表示为从表示赋值右侧的节点到表示左侧变量的节点之间的边。对于该变量的任何后续使用,都有一条从变量到该使用的数据流边,因此通过遵循数据流图中的边,我们可以追踪运行时值通过变量的流动。

重要的是要指出,CodeQL 库为 Go 提供的数据流图仅模拟 局部 流,即单个函数内的流。例如,从函数调用中的参数到相应函数参数的流不受数据流边的表示。

在 CodeQL 中,数据流节点由类 DataFlow::Node 表示,节点之间的边由谓词 DataFlow::localFlowStep 捕获。谓词 DataFlow::localFlow 将其从单个流步骤推广到零个或多个流步骤。

大多数表达式都有相应的数据流节点;例外包括类型表达式、语句标签和其他没有值的表达式,以及短路运算符。要从表达式的 AST 节点映射到相应 DFG 节点,请使用 DataFlow::exprNode。请注意,AST 节点和 DFG 节点是不同的实体,不能互换使用。

DataFlow::Node 上还有一个谓词 asExpr(),它允许您恢复 DFG 节点底层的表达式。但是,此谓词应该谨慎使用,因为许多数据流节点不对应于表达式,因此此谓词对于它们将不会被定义。

类似于 ExprDataFlow::Node 具有一个成员谓词 getType() 来确定节点的类型,以及谓词 getNumericValue()getStringValue()getExactValue() 来检索节点的值(如果它是常量)。

DataFlow::Node 重要的子类包括

  • DataFlow::CallNode:函数调用或方法调用;使用 getArgument(i)getResult(i) 分别获取对应于此调用的第 i 个参数和第 i 个结果的数据流节点;如果只有一个结果,getResult() 将返回它
  • DataFlow::ParameterNode:函数的参数;使用 asParameter() 访问相应的 AST 节点
  • DataFlow::BinaryOperationNode:涉及二元运算符的操作;每个 BinaryExpr 都有一个对应的 BinaryOperationNode,但也有在 AST 级别不显式的二元操作,例如那些源于复合赋值和增量/减量语句的操作;在 AST 级别,x + 1x += 1x++ 由不同类型的 AST 节点表示,而在 DFG 级别,它们都被建模为一个二元操作节点,其操作数为 x1
  • DataFlow::UnaryOperationNode:类似,但用于一元运算符
    • DataFlow::PointerDereferenceNode:指针解引用,可以是表单 *p 中的显式解引用,也可以是通过指针的字段或方法引用中的隐式解引用
    • DataFlow::AddressOperationNode:类似,但用于获取实体的地址
    • DataFlow::RelationalComparisonNodeDataFlow::EqualityTestNode:对应于 RelationalComparisonExprEqualityTestExpr AST 节点的数据流节点

最后,类 ReadWrite 分别表示对变量、字段或数组、切片或映射的元素的读取或写入。使用它们的成员谓词 readsVariablewritesVariablereadsFieldwritesFieldreadsElementwritesElement 来确定读取/写入是指什么。

调用图

调用图将函数(和方法)调用连接到它们调用的函数。调用图信息由 DataFlow::CallNode 上的两个成员谓词提供:getTarget() 返回调用的声明目标,而 getACallee() 返回调用在运行时可能调用的所有实际函数。

这两个谓词在处理对接口方法的调用方面有所不同:虽然 getTarget() 将返回接口方法本身,但 getACallee() 将返回实现接口方法的所有具体方法。

全局数据流和污点追踪

谓词 DataFlow::localFlowStepDataFlow::localFlow 对推理单个函数中值的流动很有用。但是,更高级的用例,尤其是在安全分析中,必然需要推理全局数据流,包括流入、流出和跨函数调用,以及通过字段。

在 CodeQL 中,这种推理是用 数据流配置 来表达的。数据流配置有三个要素:源、汇和屏障(也称为清理器),它们都是数据流节点的集合。给定这三个集合,CodeQL 提供了一个通用机制,用于查找从源到汇的路径,可能进入和退出函数和字段,但绝不流过屏障。

要定义数据流配置,您可以定义一个实现 DataFlow::ConfigSig 的模块,包括谓词 isSourceisSinkisBarrier 来定义源、汇和屏障的集合。然后,通过将 DataFlow::Global<..> 应用于配置来计算数据流。

除了纯粹的数据流之外,许多安全分析还需要执行更通用的 污点跟踪,它还考虑通过值转换操作(如字符串操作)的流动。要跟踪污点,您可以将 TaintTracking::Global<..> 应用于您的配置。

详细介绍全局数据流和污点跟踪超出了本简要介绍的范围。有关数据流和污点跟踪的概述,请参阅“关于数据流分析”。

高级库

最后,我们简要描述一些对高级查询编写者有用的概念和库。

基本块和支配

许多重要的控制流分析将控制流节点组织成 基本块,它们是最大直线控制流节点序列,不包含任何分支。在 CodeQL 库中,基本块由类 BasicBlock 表示。每个控制流节点都属于一个基本块。您可以在类 ControlFlow::Node 中使用谓词 getBasicBlock() 以及在 BasicBlock 中使用谓词 getNode(i) 来在两者之间移动。

支配是控制流分析中的一个标准概念:如果从入口节点到 bb 的第一个节点的任何控制流图路径都必须经过 dom,则称基本块 dom 支配 基本块 bb。换句话说,每当程序执行到达 bb 的开头时,它都必须经过 dom。此外,每个基本块都被认为支配自身。

反过来,如果从 bb 的最后一个节点到出口节点的任何控制流图路径都必须经过 postdom,则称基本块 postdom 后支配 基本块 bb。换句话说,在程序执行离开 bb 之后,它最终必须到达 postdom

这两个概念由类 BasicBlock 的两个成员谓词 dominatespostDominates 捕获。

条件保护节点

条件保护节点是一个合成控制流节点,它记录了在控制流图中的某个点,条件的真值已知这一事实。例如,再次考虑我们上面看到的代码片段

x := 0
if p != nil {
  x = p.f
}
return x

在“then”分支的开头,p 已知不为 nil。此知识通过在分配给 x 之前的条件保护节点进行编码,记录了 p != nil 在此为 true 这一事实

cfg2

此信息的典型用途是在分析中查找 nil 解引用:这种分析能够得出结论,字段读取 p.f 是安全的,因为它紧接在保证 p 不为 nil 的条件保护节点之前。

在 CodeQL 中,条件保护节点由类 ControlFlow::ConditionGuardNode 表示,它提供各种成员谓词来推理保护节点保证哪些条件。

静态单赋值形式

静态单赋值形式(简称 SSA 形式)是一种程序表示形式,其中原始程序变量被映射到更细粒度的 SSA 变量。每个 SSA 变量只有一个定义,因此具有多个赋值的程序变量对应于多个 SSA 变量。

大多数情况下,查询作者不必直接处理 SSA 形式。数据流图在后台使用它,因此从 SSA 获得的大多数好处可以通过简单地使用数据流图来获得。

例如,我们运行示例的数据流图实际上看起来更像这样

ssa

请注意,程序变量 x 已被映射到三个不同的 SSA 变量 x1x2x3。在这种情况下,这种表示形式没有太多好处,但总的来说,SSA 形式在数据流分析中具有众所周知的优势,对此我们参考相关文献。

如果您确实需要使用原始 SSA 变量,它们由类 SsaVariable 表示。类 SsaDefinition 表示 SSA 变量的定义,它们与 SsaVariable 有一一对应关系。成员谓词 getDefinition()getVariable() 存在于两者之间的映射。您可以使用 SsaVariable 的成员谓词 getAUse() 来查找 SSA 变量的用途。要访问 SSA 变量底层的程序变量,请使用成员谓词 getSourceVariable()

全局值编号

全局值编号 是一种技术,用于确定程序中的两个计算是否保证产生相同的结果。这通过将抽象表示形式与每个数据流节点相关联来完成(通常称为 值编号,即使在实践中它通常不是数字),以便相同的计算由相同的值编号表示。

由于这是一个不可判定问题,全局值编号是 保守的,这意味着,如果两个数据流节点具有相同的编号,则保证它们在运行时具有相同的值,反之则不然。(也就是说,可能存在数据流节点实际上总是评估为相同的值,但它们的编号不同。)

在 CodeQL 库中,您可以使用 globalValueNumber(nd) 谓词来计算数据流节点 nd 的全局值编号。值编号表示为不透明的 QL 类型 GVN,它提供的信息很少。通常,您需要对全局值编号做的就是将它们相互比较,以确定两个数据流节点是否具有相同的值。

  • ©GitHub, Inc.
  • 条款
  • 隐私