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 节点组成,分别表示 x
、y
、x + y
、(x + y)
、z
和整个表达式 (x + y) * z
。表示 x
和 y
的 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()
、getStartColumn
、getEndLine()
和 getEndColumn()
来获取有关其文件、开始行和列以及结束行和列的信息。
AstNode 的最重要的子类是 Stmt 和 Expr,它们分别表示语句和表达式。本节简要讨论了它们中一些更重要的子类和谓词。有关 Stmt 和 Expr 的所有子类的完整参考,请参见 用于 Go 的抽象语法树类。
语句¶
ExprStmt
:表达式语句;使用getExpr()
访问表达式本身Assignment
:赋值语句;使用getLhs(i)
访问第i
个左侧,使用getRhs(i)
访问第i
个右侧;如果只有一个左侧,您可以使用getLhs()
,右侧也是如此SimpleAssignStmt
:不涉及复合运算符的赋值语句AssignStmt
:形式为lhs = rhs
的简单赋值语句DefineStmt
:形式为lhs := rhs
的简写变量声明
CompoundAssignStmt
:具有复合运算符的赋值语句,例如lhs += rhs
IncStmt
、DecStmt
:分别表示递增语句或递减语句;使用getOperand()
访问正在递增或递减的表达式BlockStmt
:花括号之间的语句块;使用getStmt(i)
访问块中的第i
个语句IfStmt
:if
语句;使用getInit()
、getCond()
、getThen()
和getElse()
分别访问(可选)初始化语句、正在检查的条件、如果条件为真则要评估的“then”分支以及(可选)如果条件为假则要评估的“else”分支LoopStmt
:循环;使用getBody()
访问其主体ForStmt
:for
语句;使用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)
访问第i
个case
或default
子句ExpressionSwitchStmt
: 一个检查表达式的值的switch
语句TypeSwitchStmt
: 一个检查表达式的类型的switch
语句
CaseClause
:switch
语句中的case
或default
子句;使用getExpr(i)
访问第i
个表达式,使用getStmt(i)
访问此子句主体中的第i
个语句SelectStmt
: 一个select
语句;使用getCommClause(i)
访问第i
个case
或default
子句CommClause
:select
语句中的case
或default
子句;使用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
: 基本类型的字面量;子类IntLit
、FloatLit
、ImagLit
、RuneLit
和StringLit
代表各种特定类型的字面量FuncLit
: 一个函数字面量;使用getBody()
访问函数的主体CompositeLit
: 一个复合字面量;使用getKey(i)
和getValue(i)
分别访问第i
个键和第i
个值ParenExpr
: 一个带括号的表达式;使用getExpr()
访问括号之间的表达式IndexExpr
: 一个索引表达式base[idx]
;分别使用getBase()
和getIndex()
访问base
和idx
SliceExpr
: 一个切片表达式base[lo:hi:max]
;分别使用getBase()
、getLow()
、getHigh()
和getMax()
访问base
、lo
、hi
和max
;请注意,lo
、hi
和max
可以省略,在这种情况下相应的谓词没有定义ConversionExpr
: 一个转换表达式T(e)
;分别使用getTypeExpr()
和getOperand()
访问T
和e
TypeAssertExpr
: 一个类型断言e.(T)
;分别使用getExpr()
和getTypeExpr()
访问e
和T
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()
为真,而不是<=
或>=
名称¶
虽然 Ident
和 SelectorExpr
是非常有用的类,但它们通常过于笼统:Ident
涵盖程序中的所有标识符,包括出现在声明中的标识符以及引用,并且不区分引用包、类型、变量、常量、函数或语句标签的名称。类似地,SelectorExpr
可能引用包、类型、函数或方法。
类 Name
及其子类提供对该空间的更细粒度的映射,沿着结构和命名空间这两个轴进行组织。在结构方面,名称可以是 SimpleName
,这意味着它是一个简单标识符(因此是一个 Ident
),或者它可以是 QualifiedName
,这意味着它是一个限定标识符(因此是一个 SelectorExpr
)。在命名空间方面,Name
可以是 PackageName
、TypeName
、ValueName
或 LabelName
。ValueName
又可以是 ConstantName
、VariableName
或 FunctionName
,具体取决于名称引用的实体类型。
类 ReferenceExpr
提供了相关的抽象:引用表达式是一个表达式,它引用变量、常量、函数、字段或数组或切片的元素。使用谓词 isLvalue()
和 isRvalue()
分别确定引用表达式是否出现在语法上下文中,在该上下文中它被分配到或从中读取。
最后,ValueExpr
将 ReferenceExpr
概括为包括所有其他可以评估为值的表达式(与引用包、类型或语句标签的表达式相反)。
函数¶
在语法级别,函数以两种形式出现:在函数声明中(由类 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
是在局部范围内声明的变量,即不在包级别;ReceiverVariable
、Parameter
和 ResultVariable
分别描述接收器、参数和结果,并定义了一个谓词 getFunction()
来访问相应的函数。最后,类 Field
表示结构体字段,并提供一个成员谓词 hasQualifiedName(pkg, tp, f)
,如果该字段具有名称 f
并且属于包 pkg
中的类型 tp
,则为真。(请注意,由于嵌套,同一个字段可能属于多个类型。)
类 Function
具有一个子类 Method
,表示方法(包括接口方法和在命名类型上定义的方法)。类似于 Field
,Method
提供一个成员谓词 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
用于内置类型 bool
;NumericType
用于各种数值类型,包括 IntType
、Uint8Type
、Float64Type
等;StringType
用于类型 string
;NamedType
、ArrayType
、SliceType
、StructType
、InterfaceType
、PointerType
、MapType
、ChanType
分别用于命名类型、数组、切片、结构体、接口、指针、映射和通道。最后,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 := 0
、p != nil
、x = p.f
和 return x
的节点,以及其他一些节点。这些节点之间的边模拟了这些语句和表达式的可能执行顺序,如下所示(为了演示目的有所简化)
例如,从 p != nil
到 x = p.f
的边模拟了比较结果为 true
并执行“then”分支的情况,而从 p != nil
到 return 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 转换
请注意,与控制流图不同,赋值 x := 0
和 x = p.f
不被表示为节点。相反,它们被表示为从表示赋值右侧的节点到表示左侧变量的节点之间的边。对于该变量的任何后续使用,都有一条从变量到该使用的数据流边,因此通过遵循数据流图中的边,我们可以追踪运行时值通过变量的流动。
重要的是要指出,CodeQL 库为 Go 提供的数据流图仅模拟 局部 流,即单个函数内的流。例如,从函数调用中的参数到相应函数参数的流不受数据流边的表示。
在 CodeQL 中,数据流节点由类 DataFlow::Node
表示,节点之间的边由谓词 DataFlow::localFlowStep
捕获。谓词 DataFlow::localFlow
将其从单个流步骤推广到零个或多个流步骤。
大多数表达式都有相应的数据流节点;例外包括类型表达式、语句标签和其他没有值的表达式,以及短路运算符。要从表达式的 AST 节点映射到相应 DFG 节点,请使用 DataFlow::exprNode
。请注意,AST 节点和 DFG 节点是不同的实体,不能互换使用。
在 DataFlow::Node
上还有一个谓词 asExpr()
,它允许您恢复 DFG 节点底层的表达式。但是,此谓词应该谨慎使用,因为许多数据流节点不对应于表达式,因此此谓词对于它们将不会被定义。
类似于 Expr
,DataFlow::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 + 1
、x += 1
和x++
由不同类型的 AST 节点表示,而在 DFG 级别,它们都被建模为一个二元操作节点,其操作数为x
和1
DataFlow::UnaryOperationNode
:类似,但用于一元运算符
DataFlow::PointerDereferenceNode
:指针解引用,可以是表单*p
中的显式解引用,也可以是通过指针的字段或方法引用中的隐式解引用DataFlow::AddressOperationNode
:类似,但用于获取实体的地址DataFlow::RelationalComparisonNode
、DataFlow::EqualityTestNode
:对应于RelationalComparisonExpr
和EqualityTestExpr
AST 节点的数据流节点
最后,类 Read
和 Write
分别表示对变量、字段或数组、切片或映射的元素的读取或写入。使用它们的成员谓词 readsVariable
、writesVariable
、readsField
、writesField
、readsElement
和 writesElement
来确定读取/写入是指什么。
调用图¶
调用图将函数(和方法)调用连接到它们调用的函数。调用图信息由 DataFlow::CallNode
上的两个成员谓词提供:getTarget()
返回调用的声明目标,而 getACallee()
返回调用在运行时可能调用的所有实际函数。
这两个谓词在处理对接口方法的调用方面有所不同:虽然 getTarget()
将返回接口方法本身,但 getACallee()
将返回实现接口方法的所有具体方法。
全局数据流和污点追踪¶
谓词 DataFlow::localFlowStep
和 DataFlow::localFlow
对推理单个函数中值的流动很有用。但是,更高级的用例,尤其是在安全分析中,必然需要推理全局数据流,包括流入、流出和跨函数调用,以及通过字段。
在 CodeQL 中,这种推理是用 数据流配置 来表达的。数据流配置有三个要素:源、汇和屏障(也称为清理器),它们都是数据流节点的集合。给定这三个集合,CodeQL 提供了一个通用机制,用于查找从源到汇的路径,可能进入和退出函数和字段,但绝不流过屏障。
要定义数据流配置,您可以定义一个实现 DataFlow::ConfigSig
的模块,包括谓词 isSource
、isSink
和 isBarrier
来定义源、汇和屏障的集合。然后,通过将 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
的两个成员谓词 dominates
和 postDominates
捕获。
条件保护节点¶
条件保护节点是一个合成控制流节点,它记录了在控制流图中的某个点,条件的真值已知这一事实。例如,再次考虑我们上面看到的代码片段
x := 0
if p != nil {
x = p.f
}
return x
在“then”分支的开头,p
已知不为 nil
。此知识通过在分配给 x
之前的条件保护节点进行编码,记录了 p != nil
在此为 true
这一事实
此信息的典型用途是在分析中查找 nil
解引用:这种分析能够得出结论,字段读取 p.f
是安全的,因为它紧接在保证 p
不为 nil
的条件保护节点之前。
在 CodeQL 中,条件保护节点由类 ControlFlow::ConditionGuardNode
表示,它提供各种成员谓词来推理保护节点保证哪些条件。
静态单赋值形式¶
静态单赋值形式(简称 SSA 形式)是一种程序表示形式,其中原始程序变量被映射到更细粒度的 SSA 变量。每个 SSA 变量只有一个定义,因此具有多个赋值的程序变量对应于多个 SSA 变量。
大多数情况下,查询作者不必直接处理 SSA 形式。数据流图在后台使用它,因此从 SSA 获得的大多数好处可以通过简单地使用数据流图来获得。
例如,我们运行示例的数据流图实际上看起来更像这样
请注意,程序变量 x
已被映射到三个不同的 SSA 变量 x1
、x2
和 x3
。在这种情况下,这种表示形式没有太多好处,但总的来说,SSA 形式在数据流分析中具有众所周知的优势,对此我们参考相关文献。
如果您确实需要使用原始 SSA 变量,它们由类 SsaVariable
表示。类 SsaDefinition
表示 SSA 变量的定义,它们与 SsaVariable
有一一对应关系。成员谓词 getDefinition()
和 getVariable()
存在于两者之间的映射。您可以使用 SsaVariable
的成员谓词 getAUse()
来查找 SSA 变量的用途。要访问 SSA 变量底层的程序变量,请使用成员谓词 getSourceVariable()
。
全局值编号¶
全局值编号 是一种技术,用于确定程序中的两个计算是否保证产生相同的结果。这通过将抽象表示形式与每个数据流节点相关联来完成(通常称为 值编号,即使在实践中它通常不是数字),以便相同的计算由相同的值编号表示。
由于这是一个不可判定问题,全局值编号是 保守的,这意味着,如果两个数据流节点具有相同的编号,则保证它们在运行时具有相同的值,反之则不然。(也就是说,可能存在数据流节点实际上总是评估为相同的值,但它们的编号不同。)
在 CodeQL 库中,您可以使用 globalValueNumber(nd)
谓词来计算数据流节点 nd
的全局值编号。值编号表示为不透明的 QL 类型 GVN
,它提供的信息很少。通常,您需要对全局值编号做的就是将它们相互比较,以确定两个数据流节点是否具有相同的值。