Joern工具
Joern工具是一个代码解析工具,主要针对C语言,Joern有2个版本:
-
第一个版本将解析的
c, cpp, hpp
等文件解析为图结构后导出edge.csv
和node.csv
。用法简单些,不过感觉bug有点多,同时很久不更新的,官方将后续更新都迁移到第二个版本上。 -
第二个导出
dot
文件。不过第二个实在太难用了,还要手写scala脚本,导出的图结点全是AST结点。
我这里分析了第一个版本0.3.1分支的代码,主要目的是基于这个开发自己的工具,因为导出csv对我的工作实在是不方便。
Joern基于Antlr实现,Antlr写代码分析工具的大概套路就是写g4文件,写完之后可以生成java代码(Antlr也支持python等语言,不过java最好用),Joern 0.3.1的g4文件位置如下。
Joern的工作流程大致如下:
Joern首先将Antlr的AST转化为自定义的AST结构,然后基于自定义的AST结构生成CFG以及CDG(控制依赖图)、DDG(数据依赖图)
Joern的代码目录结构如下:
antlr/C
ast
cdg
cfg
databaseNodes
ddg
fileWalker
graphutils
misc
neo4j
outputModules
parsing
tests
tools
udg
-
antlr/C
下为Antlr生成的的Listener和Visitor。 -
ast
为Joern自定义的AST结点。 -
cdg
定义支配分析,控制依赖代码以及控制依赖图的定义。 -
cfg
包含CFG的定义以及自定义AST向CFG转换的代码。 -
ddg
包含数据依赖分析相关代码。 -
parsing
下为AntlrAST向Joern自定义AST转换的代码。 -
udg
包主要计算每个结点使用和定义的symbol,为ddg
的辅助工具。 -
misc
包含一些自定义工具类。
顺带一提,在老版本的joern中变量定义没有考虑函数指针的情况
declarator: ptrs? identifier type_suffix?;
而新版本的joern中加入了函数指针和模板特性
declarator: ptrs? identifier template_args? type_suffix? |
ptrs? '(' func_ptrs identifier ')' type_suffix;
一.自定义AST
自定义AST主要用2类结点:
-
Statement:语句类结点
-
Expression:表达式
AntlrAST转化为自定义AST可参考FunctionContentBuilder。
1.1.Statement
Statement对应的文法在Function.g4中
statement: opening_curly
| closing_curly
| block_starter
| jump_statement
| label
| simple_decl
| expr_statement
| water
;
其中
-
block starter指
if, else, while, try
等等,这类语句都会包含语句块(CompoundStatement
){ xxx }
-
jump statement指
break, continue, goto, return
-
opening_curly指
{
, closing_curly指}
(这里有点奇怪,Joern中{
也算一条语句) -
expr statement指表达式语句,也就是expr +
;
-
label指
switch
语句中的case xxx
表达式 -
simple decl指变量定义语句
joern中需要注意 if
和 do
语句
If-Else示例
{
if (cond1) {
}
else if (cond2){
}
else{
}
}
Antlr AST为(文法中没有定义 CompoundStatement
)
转化后自定义AST为
生成自定义AST的代码参考FunctionContentBuilder和NestingReconstructor,new XXX
新建AST结点,xx.addChild
添加AST边。
Do-While示例
do{
xxx
}
while (cond1);
Antlr AST:
自定义AST:
Condition
类继承自 ExpressionHolder
继承自 Expression
,其中while已经被隐藏了,而同结构 while
循环对应的自定义AST如下:
可以看到父节点类型为 WhileStatement
,而子结点一样但是顺序变了,因为执行 addChild
的顺序发生了改变。
For示例
对于 for
语句
for (initExpr; cond1; Expr){
xxx
}
AntlrAST为
自定义AST为
Switch示例
switch(cond1){
case id1: break;
}
Antlr AST:
自定义AST:
1.2.Expression
Expression类的继承关系如下
类别太多这里我就不一一列举了,先列举几个:
-
BinaryExpression为双目运算表达式
-
UnaryExpression为单目运算表达式
-
ConditionalExpression为三目运算表达式(
cond? expr1: expr2
),不过计算CFG的时候三目表达式都在一个CFG结点上。 -
PrimaryExpression为运算数,可以为变量名、字符串常量或者数值型常量。
-
Identifier为标识符,大部分情况下代表变量名,有时候也包括函数名等等。
-
InitializerList为初始化列表,比如
char a[3] = {'a', 'b', 'c'};
中InitializerList
指{'a', 'b', 'c'}
,中间3个元素每个对应一个PrimaryExpression
。
二.CFG的建立
在构建完自定义AST之后就开始构建CFG,构建CFG的代码放在cfg包下,老版本Joern中构建CFG需要考虑 blockStarter
和 jump
类结点,新版本加入了 try
和 catch
,这里我暂时无视 try
语句的控制流图:
blockStarter
包括:
-
分支语句
if
,else
,switch
-
循环语句
for
,while
,do-while
CFG的构建代码参考 CCFGFactory。
-
CFG结点类型主要包含了 ASTNodeContainer,CFGEntryNode,CFGExitNode,CFGErrorNode。
ASTNodeContainer
为普通CFG结点类型,封装了ASTNode
,继承自CFGNode
。 -
CFG边默认包含了3种标签:
empty
,true
,false
。但是在switch
语句中边标签为case
值或者defalut
。
joern中还包括了些 fix
函数,因为在开始构建控制流时,joern将这4种跳转语句视为普通的代码语句,因此它们会与后继语句有CFG边相连,而实际上这些语句都会跳转到别处,所以存在错误控制流,这些fix函数就是为了修复这些控制流而存在。
2.1.普通连续结点的CFG
首先看 newInstance(ASTNode… nodes) 方法,这个方法输入的是连续的 ASTNode
,每个 ASTNode
都是一个CFG结点,这个方法的逻辑很简单,就是为这些 ASTNode
生成一个局部小CFG,包含了 EntryNode
和 ExitNode
,所有结点顺序连接起来(通过CFG边)。
2.2.函数定义语句
代码为 newInstance(ASTNode… nodes),可以看到joern
-
首先构造形参列表的CFG,每个形参为1个CFG结点。
-
然后构造函数主体的CFG,最后将参数CFG和函数主体CFG相连。
void fun(int a, char b){
// functionBody
}
2.3.If
代码参考 newInstance(IfStatement ifStatement)。对于
if (cond) {
block1
}
else {
block2
}
而 else if (cond) { }
则当成 else { if (cond) {} }
处理。
生成的CFG如下:
2.4.Switch
解析 Switch
对应代码 newInstance(SwitchStatement switchStatement),我贴上一个补充注释的
// switch语句
public static CFG newInstance(SwitchStatement switchStatement) {
try {
CCFG switchBlock = new CCFG();
// 处理condition
CFGNode conditionContainer = new ASTNodeContainer(switchStatement.getCondition());
switchBlock.addVertex(conditionContainer);
switchBlock.addEdge(switchBlock.getEntryNode(), conditionContainer);
CCFG switchBody = convert(switchStatement.getStatement());
switchBlock.addCFG(switchBody);
boolean defaultLabel = false;
// 获取每个case: xxx
for (Entry<String, CFGNode> block : switchBody.getLabels().entrySet()) {
// 如果内容是default
if (block.getKey().equals("default"))
defaultLabel = true;
switchBlock.addEdge(conditionContainer, block.getValue(), block.getKey());
}
for (CFGEdge edge : switchBody.ingoingEdges(switchBody.getExitNode()))
switchBlock.addEdge(edge.getSource(), switchBlock.getExitNode());
// 不存在defalut标签的话,添加一条condition到switch结尾的CFG边
if (!defaultLabel)
switchBlock.addEdge(conditionContainer, switchBlock.getExitNode());
// switch中break直接与switch end相连
fixBreakStatements(switchBlock, switchBlock.getExitNode());
return switchBlock;
}
catch (Exception e) {
// e.printStackTrace();
return newErrorInstance();
}
}
对于如下代码:
switch(cond){
case 1:
break;
case 2:
break;
}
CFG为:
2.5.While
代码为 newInstance(WhileStatement whileStatement)。
对于代码
while (cond) {
whileBody
}
CFG为
2.6.do-while
代码为 newInstance(DoStatement doStatement)。
do{
doBody
}
while(cond);
2.7.For
代码为 newInstance(ForStatement forStatement)。
代码
for (init; cond; expr) {
forBody
}
CFG为
如果 expr
为空,即 for (init; cond; )
那么CFG为
暂时还没有对ForRange语句的支持。
三.控制依赖分析
在我之前的博客中简单说了控制依赖和数据依赖。控制依赖主要表现形式如下(if
, switch
, while
, for
通用):
statement1;
if (cond) {
statement2;
statement3;
}
statement4;
那么控制依赖图如下:
即condition表达式会对其相连块语句中的每一个statement产生控制依赖。控制依赖边的源点通常是condition表达式。在这里也简单说下普通CFG结点通常出度为1,但是condition结点的出度通常大于1。
控制依赖分析相关的代码在cdg包下,控制依赖图的创建可分为3个步骤,参考CDGCreator.create和DominatorTreeCreator.create方法(前者调用后者):
-
创建逆向CFG(Entry变Exit,CFG边source和destination互换)。
-
构建支配树(DominatorTreeCreator.buildDominatorTree方法)。
-
基于支配树构建CDG(控制依赖图,DominatorTreeCreator.determineDominanceFrontiers方法)。
假设当前有CFG(简化):
那么逆向CFG就是:
DominatorTree
有3个成员变量:
-
dominators
:保存post-dominate关系,上述示例中dominators的值为{ 1 --> 7, 2 --> 6, 3 --> 6, 4 --> 6, 5 --> 7, 6 --> 7 }
,key post-dominate value(1 post-dominate 7) -
dominanceFrontiers
:保存控制依赖关系,key 依赖 value,上述示例中值为{ 2 --> {1}, 3 --> {2}, 4 --> {2}, 5 --> {1}, 6 --> {1} }
。value的长度通常为1,一个语句不太可能同时依赖多个condition。 -
postorderEnumeration
:保存逆向CFG中结点后序遍历的顺序,上述示例中结点后续遍历顺序为(无视Exit和Entry)1, 2, 3, 4, 6, 5, 7
。postorderEnumeration
的值为{1 --> 1, 2 --> 2, 3 --> 3, 4 --> 4, 5 --> 6, 6 --> 5, 7 --> 7}
,key的值在遍历时第value个被访问。 -
同时还用到了内部变量 orderedVertices,它的值就是结点后序遍历列表,示例值为
1, 2, 3, 4, 6, 5, 7
上述示例的CDG为
3.1.创建支配树
初始化支配树主要是计算 postorderEnumeration
和 orderedVertices
,然后逆序遍历 orderedVertices
(以 7, 5, 6, 4, 3, 2, 1
的顺序访问结点,本质是顺序访问逆向CFG),对于列表中的每个结点,当该结点有多个前驱时求出前驱的公共支配结点(比如 1
有前驱 2
和 5
,那么求出 2
和 5
的公共支配结点,2 --> 6 --> 7
, 5 --> 7
,因此 7
是公共支配结点),并设置当前结点支配公共支配结点(1 --> 7
)。
3.2.控制依赖分析
顺序访问 orderedVertices
中的所有控制依赖源点(前驱数大于1,即顺序访问 1, 2
)。求出当前结点与前驱结点支配路径的交点(1
的前驱为 2, 5
,1 --> 7
, 2 --> 6 --> 7
, 5 -->7
,7
就是交点),然后所有路径上除了交点以外的结点都会控制依赖于当前结点 (路径交集涉及 1, 2, 5, 6, 7
,除去 1
本身和交点 7
,其它结点 2, 5, 6
都会控制依赖于 1
)。
四.Use-Def分析
4.1.目标
相关代码在udg包下,这段代码主要是为了计算每个语句使用了以及定义了哪些变量,还不是语句间的数据依赖关系。
举个例子,对于语句 x += y
,这部分会分析出 x
被定义以及使用(+=
用到了 x
),而 y
只被使用。也就是这部分只是逐语句分析。
UseDefGraph的定义如下:
public class UseDefGraph
{
// A UseDefGraph is a table indexed
// by identifiers. Each table-entry
// is a list of the UseOrDefRecords
// of the identifier.
MultiHashMap<String, UseOrDefRecord> useOrDefRecordTable = new MultiHashMap<String, UseOrDefRecord>();
public MultiHashMap<String, UseOrDefRecord> getUseDefDict()
{
return useOrDefRecordTable;
}
public List<UseOrDefRecord> getUsesAndDefsForSymbol(String symbol)
{
return useOrDefRecordTable.get(symbol);
}
public void addDefinition(String identifier, ASTNode astNode)
{
add(identifier, astNode, true);
}
public void addUse(String identifier, ASTNode astNode)
{
add(identifier, astNode, false);
}
private void add(String identifier, ASTNode astNode, boolean isDef)
{
UseOrDefRecord record = new UseOrDefRecord(astNode, isDef);
useOrDefRecordTable.add(identifier, record);
}
}
MultiHashMap
是joern中自定义实现的数据类型,MultiHashMap<K, V>
等同于 Map<K, LinkList<V>>
,useOrDefRecordTable
的 key 是每个符号,value记录了该语句中这个符号的使用和定义情况。符号是指语句中出现的变量。
UseOrDefRecord是一个二元组,为 < ASTNode, boolean >
类型,它表示一个符号在该ASTNode下是被定义或者被使用。
4.1.1.示例
以 x += y
来举例,其AST和分析完毕时 useOrDefRecordTable
的结果如下:
{
"x": [
[ExpressionStatement, true],
[AssignmentExpr, true],
[ExpressionStatement, false],
[AssignmentExpr, false]
],
"y": [
[ExpressionStatement, false],
[AssignmentExpr, false]
]
}
其中只有 ExpressionStatement
属于CFG结点,因此暂时忽略 AssignmentExpr
,这个结果表示在该语句中,符号 x
既被使用又被定义,而 y
只被使用。
4.1.2.结构体示例
结构体的def-use分析可以参考下面示例:
int ddg_test_struct(){
struct my_struct foo;
foo.bar = 10;
copy_somehwere(foo);
}
AST如下:
分析结果如下:
{
"foo": [
// struct my_struct foo;
[IdentifierDeclStatement, true],
[IdentifierDecl, true],
// foo.bar = 10;
[ExpressionStatement, false],
[MemberAccess, false],
[ExpressionStatement, true],
[AssignmentExpr, true],
// copy_somehwere(foo);
[ExpressionStatement, false],
[Argument, false]
],
"foo . bar": [
// foo.bar = 10;
[ExpressionStatement, true],
[AssignmentExpr, true]
]
}
从结果来看:
-
struct my_struct foo;
定义了符号foo
。 -
foo.bar = 10;
使用了符号foo
,并定义了符号foo
和foo . bar
。 -
copy_somehwere(foo);
使用了符号foo
。
4.1.3.数组和指针示例
void test()
{
char* dst = (char*)malloc(sizeof(char)*100);
char source[100];
memset(source, 'A', 100);
source[99] = '\0';
memcpy(dst, source, 100);
*dst = 'A';
}
AST为:
解析结果为:
{
"* dst": [
[ExpressionStatement, true],
[true]
],
"malloc": [
[IdentifierDeclStatement, false],
[AssignmentExpr, false]
],
"dst": [
[IdentifierDeclStatement, true],
[AssignmentExpr, true],
[ExpressionStatement, false],
[Argument, false],
[ExpressionStatement, false],
[UnaryOp, false]
],
"* source": [
[ExpressionStatement, true],
[AssignmentExpr, true],
],
"source": [
[IdentifierDeclStatement, true],
[IdentifierDecl, true],
[ExpressionStatement, false],
[Argument, false],
[ExpressionStatement, false],
[ArrayIndexing, false],
[ExpressionStatement, false],
[Argument, false]
]
}
可以看到:
-
char* dst = (char*)malloc(sizeof(char)*100);
定义了符号dst
,使用了符号malloc
(感觉这里malloc
是被错误解析出来的)。 -
char source[100];
定义了符号source
。 -
memset(source, 'A', 100);
使用了符号source
。 -
source[99] = '\0';
定义了符号* source
,使用了符号source
。 -
memcpy(dst, source, 100);
使用了符号dst
、source
。 -
*dst = 'A';
定义了符号* dst
,使用了符号dst
。
可以看到对于数组指针类型的变量,在使用时除了本身变量名作为符号,还会加上一个 * + 变量名
的符号。
4.2.算法
构建 UseDefGraph
参考CFGToUDGConverter.convert
for (CFGNode cfgNode : statements)
{
// skip empty blocks
if (cfgNode instanceof ASTNodeContainer)
{
ASTNode statementNode = ((ASTNodeContainer) cfgNode)
.getASTNode();
ASTNodeASTProvider provider = new ASTNodeASTProvider();
provider.setNode(statementNode);
Collection<UseOrDef> usesAndDefs = astAnalyzer
.analyzeAST(provider);
addToUseDefGraph(useDefGraph, usesAndDefs, statementNode);
}
}
显然,算法是每个语句独立解析,最后将结果汇聚到一起。解析语句主要是遍历该语句对应的AST子树,这里用到了ASTDefUseAnalyzer类和ASTNodeASTProvider类。前者主要遍历某个语句对应的AST求语句类符号(symbol)的定义和使用关系,后者是 ASTNode
的封装。
ASTDefUseAnalyzer
遍历AST的时候用到了后序遍历,特殊处理的AST结点类型参考ASTDefUseAnalyzer.createUseDefEnvironment。
private UseDefEnvironment createUseDefEnvironment(ASTProvider astProvider)
{
String nodeType = astProvider.getTypeAsString();
switch (nodeType)
{
case "AssignmentExpr":
return new AssignmentEnvironment();
case "IncDecOp":
return new IncDecEnvironment();
case "IdentifierDecl":
case "Parameter":
return new DeclEnvironment();
case "CallExpression":
return createCallEnvironment(astProvider);
case "Argument":
return createArgumentEnvironment(astProvider);
case "PtrMemberAccess":
return new PtrMemberAccessEnvironment();
case "MemberAccess":
return new MemberAccessEnvironment();
case "Condition":
case "ReturnStatement":
return new UseEnvironment();
case "ArrayIndexing":
return new ArrayIndexingEnvironment();
case "UnaryOp":
return new UnaryOpEnvironment();
case "Identifier":
return new IdentifierEnvironment();
default:
return new UseDefEnvironment();
}
}
4.2.1.AssignmentExpr, IdentifierDecl, Identifier
以 char* dst = (char*)malloc(sizeof(char)*100);
为例,其AST为:
Joern提取出来的symbol有2个 dst
,malloc
。
先关注 AssignmentExpr
之前的部分:
Joern中定义了一个UseDefEnvironment类用来处理单个AST结点,成员变量 symbols
包含了该结点及其子结点下包含了 symbol
,UseDefEnvironment
有多种子类型,不同的子类型处理不同类型的结点。UseDefEnvironment
有2个重要的方法 isDef
和 isUse
判断AST结点中 symbol是定义还是使用。
处理上述结点时算法主要从 Identifier
类结点中提取symbol。
AssignmentEnvironment的 isUse
和 isDef
定义如下:
@Override
public boolean isUse(ASTProvider child)
{
int childNum = child.getChildNumber();
// 如果是第一个symbol
if (childNum == 0)
{
// 如果operator不为空 并且 operator不是 =,也就是 x = y没有使用x,而x += y即使用也重新定义
String operatorCode = astProvider.getOperatorCode();
if (operatorCode != null && !operatorCode.equals("="))
return true;
else
return false;
}
return true;
}
// Assignment Expr中第一个symbol为重新定义,后面的均不是
@Override
public boolean isDef(ASTProvider child)
{
int childNum = child.getChildNumber();
if (childNum == 0)
return true;
return false;
}
-
isUse
:赋值语句有多种x = y
重新定义了x
但是没有使用x
,而x += y
既使用了x
又重新定义了x
。y
不属于赋值语句第一个子结点,因此直接算使用。 -
isDef
:赋值语句中只要symbol处于第一个子结点下那都属于重新定义。
IdentifierDecl
和 Parameter
(函数定义参数)都对应DeclEnvironment。
public boolean isUse(ASTProvider child)
{
return false;
}
public boolean isDef(ASTProvider child)
{
String type = child.getTypeAsString();
int childNum = child.getChildNumber();
return (childNum == 1 && type.equals("Identifier"));
}
-
isUse
恒等于false
表示IdentifierDecl
不存在使用只有定义。 -
isDef
则是考虑到这么一个情况-
对于
char a;
,那么其AST子结点Identifier
的子结点数量为1(调试的时候发现的),此时该语句定义了a
,对于char a = 0;
,语句前半部分char a
对应的AST子结点Identifier
子结点数量0,因此没有定义a
. -
而后半部分属于
AssignmentExpr
,其下有子结点Identifier: a
(这个Identifier
是被AssignmentEnvironment
而不是DeclEnvironment
处理的,)。 -
因此
char a;
中symbola
被DeclEnvironment.isDef
标记为定义,而char a=0;
中a
被AssignmentEnvironment.isDef
标记为定义。
-
4.2.2.Argument
ArgumentEnvironment相关代码如下:
public class ArgumentEnvironment extends EmitDefAndUseEnvironment
{
boolean isTainted = false;
public void addChildSymbols(LinkedList<String> childSymbols,
ASTProvider child)
{
if (isDef(child)){
// For tainted arguments, add "* symbol" instead of symbol
// to defined symbols. Make an exception if symbol starts with '& '
LinkedList<String> derefChildSymbols = new LinkedList<String>();
for(String symbol : childSymbols){
if(!symbol.startsWith("& ")){
derefChildSymbols.add("* " + symbol);
// !patch to see if we can detect macro-sources!
derefChildSymbols.add(symbol);
}else
derefChildSymbols.add(symbol.substring(2));
}
defSymbols.addAll(derefChildSymbols);
}
if (isUse(child))
useSymbols.addAll(childSymbols);
}
public boolean isUse(ASTProvider child)
{
return true;
}
public boolean isDef(ASTProvider child)
{
return isTainted;
}
public void setIsTainted()
{
isTainted = true;
}
}
-
isUse
表示函数调用实参无论何时都会使用symbol。 -
isDef
:返回参数是否受污点影响。 -
addChildSymbols
中的代码表示-
foo(x)
中x
的Tainted
为true
,那么* x
和x
都会被定义,同时x
被使用。 -
foo(&x)
(直接传地址,引用只在函数定义形参中出现) 中当x
的Tainted
为true
时,x
会被重新定义但是& x
会被使用。
-
4.2.3.UnaryOp
包括指针引用情况,以 *p = *(a + 1)
为例,AST为:
UnaryOpEnvironment 中的相关代码如下:
public void addChildSymbols(LinkedList<String> childSymbols,
ASTProvider child)
{
String codeStr = astProvider.getEscapedCodeStr();
// 如果以取地址符开头,比如 &p = xx,那么symbol = p, add的就是 & p
if(codeStr != null && codeStr.startsWith("&")){
for(String symbol : childSymbols){
symbols.add("& " + symbol);
}
return;
}
// 如果是取指向的,那么直接将p添加进符号表
if(codeStr == null|| !codeStr.startsWith("*")){
symbols.addAll(childSymbols);
return;
}
LinkedList<String> retval = new LinkedList<String>();
// emit all symbols as '* symbol'
LinkedList<String> derefedChildren = new LinkedList<String>();
for(String c : childSymbols){
derefedChildren.add("* " + c);
}
retval.addAll(derefedChildren);
// emit entire code string
retval.add(codeStr);
useSymbols.addAll(childSymbols);
symbols.addAll(retval);
}
针对上述代码生成的分析结果为(false
表示使用,true
表示定义):
{
["p", false],
["a", false],
["* ( a + 1 )", false],
["* p", true],
["* a", false]
}
直接从 identifier
中提取出来的symbol只有 p, a
,而 derefedChildren.add("* " + c);
添加了symbol * p, * a
。retval.add(codeStr);
添加了symbol *(a+1)
。
4.2.4.MemberAccess和PtrMemberAccess
以 a.f1 = b.f2;
举例,AST如下:
MemberAccess相关代码如下:
public LinkedList<String> upstreamSymbols()
{
LinkedList<String> retval = new LinkedList<String>();
// emit all symbols
retval.addAll(symbols);
// emit entire code string
String codeStr = astProvider.getEscapedCodeStr();
retval.add(codeStr);
return retval;
}
public void addChildSymbols(LinkedList<String> childSymbols,
ASTProvider child)
{
int childNum = child.getChildNumber();
// Only add the left child but never the right child
if (childNum == 0)
super.addChildSymbols(childSymbols, child);
}
-
addChildSymbols
函数的功能是将第一个子结点的symbol加入symbol列表,比如a.f1
中a
会被添加f1
不会。 -
upstreamSymbols
会将子结点的symbol和子身对应的代码片段添加进symbol列表,比如a.f1
中a
和a . f1
会被添加进列表。
解析结果如下:
{
["a", false],
["a", true],
["a . f1", true],
["b", false],
["b . f2", false]
}
MemberAccessEnvironment.useOrDefsFromSymbols
函数解析出 ["a", false]
。
PtrMemberAccessEnvironment 和 MemberAccessEnvironment
类似,区别在于 upstreamSymbols
中
public LinkedList<String> upstreamSymbols()
{
LinkedList<String> retval = new LinkedList<String>();
// emit all symbols as '* symbol'
// 给变量添加 * 符号
LinkedList<String> derefedChildren = new LinkedList<String>();
for(String c : symbols){
derefedChildren.add("* " + c);
}
retval.addAll(derefedChildren);
// emit entire code string
// 与MemberAccessEnvironment一样
String codeStr = astProvider.getEscapedCodeStr();
retval.add(codeStr);
return retval;
}
那么对于 a->f1 = b.f2;
,解析结果为:
解析结果如下:
{
["a", false],
["* a", true],
["a -> f1", true],
["b", false],
["b . f2", false]
}
4.2.5.ArrayIndexing
ArrayIndexingEnvironment的相关代码如下:
public class ArrayIndexingEnvironment extends EmitUseEnvironment
{
public void addChildSymbols(LinkedList<String> childSymbols,
ASTProvider child)
{
LinkedList<String> derefedChildren = new LinkedList<String>();
for(String c : childSymbols){
derefedChildren.add("* " + c);
}
symbols.addAll(derefedChildren);
useSymbols.addAll(childSymbols);
}
}
也就是说数组名会作为使用symbol添加,而 *
+ 数组名会进一步被处理,以 a[i] = b[j];
为例,其AST为:
解析结果为:
{
[a, false],
[b, false],
[* a, true],
[* b, false],
[i, false],
[j, false],
[* i, true],
[* j, false]
}
当然,有的结果感觉不太对,比如 [* i, true]
和 [* j, false]
,i, j
都是整型变量不是数组,原因可能是 ArrayIndexingEnvironment
添加符号时没有考虑 childNumber
,后续可以添加一个判断,只对第一个子结点进行符号处理。
五.数据依赖分析
5.1.算法
数据依赖的分析过程是一个Reaching Definition问题,详细可以参考南大软件分析课程-数据流分析。在这里,gen[B]
表示语句(结点)B
处定义的符号。迭代过程如下(遍历CFG完成):
数据依赖图相关代码在ddg包下,数据依赖图的定义为DDG类,其主要包含数据依赖边DefUseRelation类。
public class DefUseRelation
{
public Object src;
public Object dst;
public String symbol;
public DefUseRelation(Object aSrc, Object aDst, String aSymbol)
{
src = aSrc;
dst = aDst;
symbol = aSymbol;
}
@Override
public boolean equals(Object other)
{
DefUseRelation otherRel = (DefUseRelation) other;
return (src == otherRel.src) && (dst == otherRel.dst)
&& (symbol.equals(otherRel.symbol));
}
@Override
public int hashCode()
{
return symbol.hashCode();
}
}
src
和 dst
均为CFG结点,这样一条边表示在 src
中定义的符号在 dst
中被使用。在给定 CFG
和 UseDefGraph
计算 DDG
之前会计算一个中间类型DefUseCFG,DefUseCFG
包含了以下成员变量。
-
statements
:对应所有的CFG结点,包括Entry和Exit。 -
symbolsUsed
是一个MultiHashMap
:key对应某个CFG结点(语句),value对应该语句使用的symbol列表。 -
symbolsDefined
也是一个MultiHashMap
:key对应某个CFG结点(语句),value对应该语句定义的symbol列表。 -
parentBlocks
也是一个MultiHashMap
:key对应某个CFG结点(语句),value对应其CFG中的前驱结点。 -
childBlocks
也是一个MultiHashMap
:key对应某个CFG结点(语句),value对应其CFG中的后继结点。 -
symbolId
貌似没用到。
Reaching Definition分析由DDGCreator.calculateReachingDefs完成,DDGCreator
下 in, out, gen
均由 HashMap<Object, HashSet<Object>>
构成,key为一个语句(CFG结点),value对应该语句定义的symbol集合。
Reaching Definition分析完成之后,算法会调用DDGCreator.createDDGFromReachingDef 构建数据依赖关系,此时 In[S]
中保存了能活跃到语句 S
中的符号定义集合 inForBlock
。算法会逐个访问CFG结点,对于每个CFG结点,算法会找出该CFG结点中使用的符号集合 usedSymbol
,并将2个集合对应元素连上数据依赖边。
private DDG createDDGFromReachingDefs()
{
DDG ddg = new DDG();
for (Object statement : cfg.getStatements())
{
HashSet<Object> inForBlock = in.getListForKey(statement);
if (inForBlock == null)
continue;
List<Object> usedSymbols = cfg.getSymbolsUsed().getListForKey(
statement);
if (usedSymbols == null)
continue;
// inForBlock保存活跃到statement的symbol定义
for (Object d : inForBlock)
{
Definition def = (Definition) d;
// usedSymbol保存statement使用的symbol
if (usedSymbols.contains(def.identifier))
ddg.add(def.statement, statement, def.identifier);
}
}
return ddg;
}
5.2.示例
以下面代码为例
void test()
{
char* dst = (char*)malloc(sizeof(char)*100); // s1
char source[100]; // s2
memset(source, 'A', 100); // s3
source[99] = '\0'; // s4
memcpy(dst, source, 100); // s5
*dst = 'A'; // s6
}
-
gen
值如下:gen[s1] = { (dst, s1) }
gen[s2] = { (source, s2) }
gen[s4] = { (*source, s4) }
gen[s6] = { (*dst, s6) }
-
in
值如下:in[s1] = { }
in[s2] = { (dst, s1) }
in[s3] = { (source, s2), (dst, s1) }
in[s4] = { (source, s2), (dst, s1) }
in[s5] = { (*source, s4), (source, s2), (dst, s1) }
in[s6] = { (*source, s4), (source, s2), (dst, s1) }
-
数据依赖图如下,实际上symbol
*source
和*dst
并没有用上。