很多简单的Check可以通过查看程序语法树和捕获不需要的代码模式来实现,不使用CSA的path-sensitive engine的Checker很快,而且通常会有一个很高的正报几率,但是只能捕获非常有限的一些问题
比如AST visitors和AST matchers:
基于AST的check不是Clang Static Analyzer的强项,如果只用基于AST的信息,那么可以用基于clang的任何工具来配合
在官方的CSA里有一些基础AST的check,但是通常基于AST的checker会转到用clang-tidy 工具
CSA的Path-sensitive引擎只有在至少一个Checker调用了需要进行Path-sensitive分析的回调函数后才开始执行
如果单纯使用基于AST的checker而不用path-sensitive,会快很多。因为基于AST的checker不会构造需要分析的一些数据结构,只是定义了几个AST的回调函数,最有用且不会触发Path-sensitive引擎的checker是 check::EndOfTranslationUnit和check::ASTCodeBody
void checkEndOfTranslationUnit(const TranslationUnitDecl *TU,AnalysisManager &AM,BugReporter &BR) const;
在这个回调函数里,程序的完整AST可以用来被分析
访问AST的入口点第一个参数TU来提供
当不仅需要检查可执行代码,还需要检查声明的时候经常使用这个回调函数
void checkASTCodeBody(const Decl *D,AnalysisManager &AM,BugReporter &BR)const;
在这个回调函数中,每次调用都会提供函数的申明和分析器分析函数的代码段
需要分析的函数体可以作为D->getBody()使用
当只需要分析可执行代码的时候,这个回调函数就非常方便
void checkASTDecl(const T*D,AnalysisManager &Mgr,BugReporter &BR)const;
对于类型为T的所有AST声明(例如,如果T是VarDecl或者则调用所有变量,如果T的FieldDecl类,则调用所有类字段)
对于申明visitros来说,这个操作比较方便简单
AST visitors的机制是探索clang的AST最好用的工具,Clang提供了大量用简单语法的visitors给AST
对于实现checkers,两种visitors是最有用的
ConstmtVisitor | 检查代码使用范围最广的 |
---|---|
ConstDeclVisitor | 有时用来检查代码体外部的申明(如全局变量) |
如果要使用visitor,需要从中继承一个类并为不同类型的AST节点实现visitor回调。如果没有为特定节点实现回调,就会调用更多通用节点的回调函数
例如: 将会在以下回调函数之一访问:CXXOperatorCallExpr
VisitCXXOperatorCallExpr(...),
VisitCallExpr(...),
VisitExpr(...),
VisitStmt(...),
调用最先确定的回调函数
例如:我们可以尝试下能否重写alpha.core.MainCallChecker把它变为一个AST visitor
首先申明一个AST visitor,这个visitor存储了对BugReporter的引用来为了方便抛出Path-insensitive的报告和当前的AnalysisDeclContext,它是生成错误报告的判断位置所必需的。AnalysisDeclContext还封装了正在分析的原始函数
namespace {
class WalkAST : public ConstStmtVisitor < WalkAST > {
BugReporter & BR ;
AnalysisDeclContext * ADC ;
void VisitChildren (const Stmt *S);
public :
WalkAST (BugReporter &Reporter, AnalysisDeclContext *Context )
: BR(Reporter),ADC(Context) {}
void VisitStmt (const Stmt * S);
void VisitCallExpr (const CallExpr *CE);
};
}
这个visitor定义了两个public回调函数:VisitCallExpr(...)函数用于特殊处理函数调用表达式,VisitStmt(...)用于访问所有其他类型的语句
这些回调函数有一个共同点:每次访问完他们的语句后他们需要去访问子语句,这个操作通常由一个子函数VisitChildren(...)来处理
void WalkAST :: VisitChildren(const Stmt *S){
for (Stmt::const_child_iterator I=S->child_begin(),E=S->child_end();I!=E; ++ I )
{
if ( const Stmt * Child = *I)
Visit (Child);
}
}
这里还暂时用不到它,所以直接在内部调用Children函数就好了
void WalkAST::VisitStmt(const Stmt *S)
{
VisitChildren(S);
}
大多数的check逻辑存储在VisitCallExpr(...)中,获取得到当前调用表达式的函数声明,获取其标识符,并查看该标识符是否与"main"一致,如果有就抛出一个Path-insensitive的报告
需要注意的是,与路径敏感的checker不同,仅语法的检查器没有方便的CheckerContext使用,因此需要直接访问BugReporter对象,并且在获取必要的源代码位置付出一些努力
void WalkAST::VisitCallExpr(const CallExpr *CE)
{
if(const FunctionDecl *FD = CE->getDirectCallee())
if(const IdentifierInfo *II=FD->getIdentifier())
if(II->isStr(" main "))
{
SourceRange R = CE - > getSourceRange ();
PathDiagnosticLocation ELoc =
PathDiagnosticLocation::createBegin(CE,BR.getSourceManager(),ADC);
BR.EmitBasicReport(ADC->getDecl(),"Call to main","Example checker","Call to main",ELoc,R);
}
VisitChildren ( CE );
}
以上就是visitor的实现
我们只需要创建它并给他一些代码来实现它,因为上面的所有代码都都只是编写好了,但是没有被调用起来,所以需要编写check::ASTCodeBody回调函数
namespace {
class MainCallCheckerAST : public Checker<check::ASTCodeBody>
{
public:
void checkASTCodeBody(const Decl*D,AnalysisManager &AM,BugReporter &B) const;
}
}
void MainCallCheckerAST::checkASTCodeBody(const Decl*D,AnalysisManager &AM,
BugReporter &B)const
{
WalkAST Walker(BR,AM.getAnalysisDeclContext(D));
Walker.Visit(D->getBody());
}
通过这种方式,访问者从表示函数体的复合语句开始,然后下降到子语句中实施
Checker写好了,但是我们写的是通过AST来检测的,所以只能检测有没有main()这样的函数调用,而不能检测到函数指针调用main,这是AST的原因
有时想把申明和实现放在一起,就可以用多继承来实现
class WalkAST:public ConstStmtVisitor<WalkAST>,public ConstDeclVisitor<WalkAST>
{
public:
using ConstStmtVisitor<WalkAST>::Visit;
using ConstDeclVisitor<WalkAST>::Visit;
};
AST matchers是在clang AST中用来查找简单代码模式的新API,他们允许为这些模式编写极其简洁的声明性定义,几乎和用自然语言用文字描述它们一样简短-并为对发现的每个模式采取行动提供了一个接口,但是由于简单的代码模式、简单性和代码可读性导致AST matcher并不像AST visitors那样无所不能
这里我们可以重写最开始的alpha.core.MainCallChecker的checker来举例
检查器需要查找名为"main"的函数调用,那么就可以编写一个匹配器来查找这样的调用
callExpr(callee(functionDecl(hasName("main")))).bind("call")
完整的检查器逻辑现在只需要一行代码,剩下的就是写出checker实施和抛出错误报告,需要注意的是bind(...)指令会给AST节点分配名字作为后面调用
首先我们需要定义一个matcher的回调函数,每当这个matcher找到问题的时候回调函数都会被调用
Matcher回调函数需要继承MatcherFinder::MatchCallBack并实现run()方法
namespace {
class Callback : public MatchFinder::MatchCallback {
BugReporter &BR ;
AnalysisDeclContext*ADC ;
public :
void run(const MatchFinder::MatchResult & Result);
Callback(BugReporter &Reporter,AnalysisDeclContext *Context):BR(Reporter),ADC( Context ){}
};
}
理想情况下,match的回调函数唯一需要做的就是抛出基本的bug报告,但是有时matcher并不能覆盖整个checker逻辑,而很自然地将一些最终检查留给回调函数处理
void Callback::run(const MatchFinder::MatchResult &Result)
{
const CallExpr *CE=Result.Nodes.getStmtAs<CallExpr>("call");
assert(CE);
SourceRange R = CE->getSourceRange();
PathDiagnosticLocation ELoc =
PathDiagnosticLocation::createBegin(CE,BR.getSourceManager(),ADC);
BR.EmitBasicReport(ADC->getDecl(),"Call to main","Example checker","Call to main", ELoc,R);
}
在这个回调函数里面,我们通过前面用bind(...)函数定义的名字来获得函数的调用表达式
查看matcher,我们可以确定这样的调用表达式是存在的,所以可以肯定的是通过名称获得了语句
namespace {
class MainCallCheckerMatchers:public Checker<check::EndOfTranslationUnit>
{
public:
void checkEndOfTranslationUnit(const TranslationUnitDecl *TU,
AnalysisManager &AM,BugReporter &B)const ;
};
}
最后在checker回调中,需要构造matcher并使用它查找bug
void MainCallCheckerMatchers::checkEndOfTranslationUnit(
const TranslationUnitDecl *TU,AnalysisManager &AM,BugReporter &B) const
{
MatchFinder F;
Callback CB(B,AM.getAnalysisDeclContext(TU));
F.addMatcher(stmt(hasDescendant(callExpr(callee(functionDecl(hasName("main")))).bind ("call"))) ,&CB);
F.matchAST(AM.getASTContext());
}
这里的MatchFinder下的matchAST()函数会检查单元的整个AST,检查完毕输出类似于checker的visitor版本
我们从AnalysisManager获得的ASTContext结构包含程序的整个AST,以及有关AST的各种信息
如果某个子模式在matcher中重复多次,则可以存储并重复使用它
TypeMatcher TypeM = templateSpecializationType().bind("type");
DeclarationMatcher VarDeclM =varDecl(hasType(TypeM)).bind("decl");
StatementMatcher TempObjM = temporaryObjectExpr(hasType(TypeM)).bind("stmt");
这里的TypeM被存储,然后又在另外两个matcher中重复使用两次,然后这里的两个matcher又被存储起来
有时组合预定义的匹配器不足以实现所需的检查,这种情况下实现自定义的AST matcher就很关键了
实现AST matcher只需要几行代码,在ASTMatcher.h可以找到很多例子
在checker中实现自定义AST matcher时,只需要把它放到 clang::ast_matchers namespace里面就好
例如:
namespace clang {
namespace ast_matchers {
AST_MATCHER (RecordDecl,isUnion){
return Node.isUnion();
}
?
} //这里在clang的命名空间中的ast_matchers来处理
}
这个例子定义了一个自定义的matcher
MatchFinder的matchAST()方法匹配了整个的AST
如果你想要匹配一部分的AST你可以用match()来处理
例如:用check::ASTCodeBody写一个MainCallChecker,需要把D->getBody()和MatchFinder配对
然而,match(...)和matchAST(...)语义有区别:match尝试匹配语句本身,而matchAST尝试匹配子语句
所以需要修改matcher,使其手动查找子语句
void MainCallCheckerMatchers::checkASTCodeBody(const Decl *D,AnalysisManager &AM,BugReporter &BR) const
{
MatchFinder F ;
Callback CB(BR,AM.getAnalysisDeclContext(D));
F.addMatcher(stmt(hasDescendant(callExpr(callee(functionDecl(hasName("main")))).bind ("call"))),&CB);
F.matchAST(*(D->getBody()),AM.getASTContext());
}
从一个表达式的AST来看,这个表达式实际的值是一个常量值,为了方便可以直接把表达式的值对于修改为具体的值。有一个现场的解决方案,使用Expr的evaluatesint(...)方法可以处理
const Expr *E=/* some AST expression you are interested in */
llvm :: APSInt Result;
if(E->EvaluateAsInt(Result,ACtx,Expr::SE_AllowSideEffects)) {
/* we managed to obtain the value of the expression */
uint64_t IntResult =Result.getLimitedValue();
/* ... */
} else {
/* the expression doesn ’t fold to into a constant value */
}
Clang Static Analyzer-使用手册-基于AST的Checkers
原文:https://www.cnblogs.com/Sna1lGo/p/14649672.html