Loading

【编译与代码分析记录】01 · Clang 静态分析:AST 提取、遍历与匹配

Clang 静态分析:AST 提取、遍历与匹配

整理自博客园旧笔记:《llvm提取c/c++ ast树》《编写基于 ASTFrontendActions 的 RecursiveASTVisitor》《[转]Clang ASTMatcher基础学习》《c项目生成compile_commands.json》
(原发布于 2022-09-13;此内容仅为个人学习记录,非工业界常用技能)

一、compile_commands.json(clang 工具链的前置依赖)

所有基于 LibTooling 的工具(clang-check、clang-tidy、自定义工具)在分析真实项目时,都需要知道每个源文件的编译参数,这就是 compile_commands.json 的作用。

1.1、CMake 项目生成

cd Main
mkdir build && cd build
cmake ../Main -DCMAKE_EXPORT_COMPILE_COMMANDS=1

生成的 build/compile_commands.json 内容:

[
  {
    "directory": "/root/data/clang-llvm/TestProject/build",
    "command": "/bin/c++ -I/root/data/clang-llvm/TestProject/Main/include -o CMakeFiles/t.dir/t.cpp.o -c /root/data/clang-llvm/TestProject/Main/t.cpp",
    "file": "/root/data/clang-llvm/TestProject/Main/t.cpp"
  }
]

CMake ≥3.5 也可在 CMakeLists.txt 里加 set(CMAKE_EXPORT_COMPILE_COMMANDS ON),一劳永逸。

1.2、非 CMake 项目(configure/make):用 bear

mkdir build && cd build
bear -- ../configure --prefix=/path/to/install
bear -- make

bear 会拦截编译命令并记录成 json(老版本写法是 intercept-build + bear 两步)。

二、用 clang 命令行直接提取 AST

源代码 main.c:

#include <iostream>
int main()
{
    double a = 1.2;
    double b = 2.4;
    std::cout << a + b << std::endl;
    return 0;
}

只解析语法、打印 AST(不编译链接):

clang -fsyntax-only -Xclang -ast-dump main.c
# 或者用 clang-check 过滤特定节点:
clang-check -p build main.c -ast-dump -ast-dump-filter main

-fsyntax-only 只做语法检查;-Xclang 传递 clang 内部参数;-ast-dump 打印 AST。

AST 输出形如(摘录):

FunctionDecl 0x55eed4d4d728 <main.c:1:1, line:6:1> line:1:5 main 'int ()'
`-CompoundStmt 0x55eed4d4e158 <col:11, line:6:1>
  |-DeclStmt ... `-VarDecl ... used a 'double'
  |-DeclStmt ... `-VarDecl ... used b 'double'
  |-CallExpr ...
  |  |-ImplicitCastExpr ... <FunctionToPointerDecay>
  |  |  `-DeclRefExpr ... 'printf'
  |  `-StringLiteral ... "%d"
  `-ReturnStmt
    `-IntegerLiteral ... 'int' 0

每个节点带:内存地址、源码位置 <file:line:col>、类型。-ast-dump=json 还能输出 JSON 版本,方便程序解析。

2.1、常用 AST 节点类型

节点 含义
TranslationUnitDecl 顶层结构,AST 树的根,遍历从这里开始
FunctionDecl 函数声明/定义
ParmVarDecl 函数参数
VarDecl 变量定义,有初始化时 getInit() 取初始化 Expr
RecordDecl struct/union 声明
FieldDecl struct 内部字段
CompoundStmt 大括号包围的语句块(函数体、for 体等)
DeclStmt 声明语句
BinaryOperator 二元运算(含赋值 a = b)
CallExpr 函数调用,子节点为参数列表
ReturnStmt 返回语句
DeclRefExpr 对已声明符号的引用
ImplicitCastExpr 编译器插入的隐式类型转换(如 LValueToRValue、ArrayToPointerDecay)

三、编写 RecursiveASTVisitor 工具(ASTFrontendAction)

参考资料:https://clang.llvm.org/docs/RAVFrontendAction.html ,源码 https://github.com/llvm/llvm-project

目标:遍历 AST,找到所有名为 n::m::C 的类声明(CXXRecordDecl)并打印位置。三层结构:

FrontendAction(入口)
  └─ ASTConsumer(每个翻译单元的处理器)
       └─ RecursiveASTVisitor(递归访问节点的钩子)

3.1、FrontendAction

FrontendAction 是 clang 工具(Plugin 或 LibTooling 独立工具)的入口接口。基于 AST 的分析直接继承 ASTFrontendAction,只需实现 CreateASTConsumer,为每个翻译单元返回一个 ASTConsumer:

class FindNamedClassAction : public clang::ASTFrontendAction {
public:
  virtual std::unique_ptr<clang::ASTConsumer> CreateASTConsumer(
    clang::CompilerInstance &Compiler, llvm::StringRef InFile) {
    return std::unique_ptr<clang::ASTConsumer>(
        new FindNamedClassConsumer(&Compiler.getASTContext()));
  }
};

3.2、ASTConsumer

ASTConsumer 提供多个入口点,常用的是 HandleTranslationUnit——AST 为翻译单元构建完成后被调用:

class FindNamedClassConsumer : public clang::ASTConsumer {
public:
  explicit FindNamedClassConsumer(ASTContext *Context) : Visitor(Context) {}

  virtual void HandleTranslationUnit(clang::ASTContext &Context) {
    // 从翻译单元根开始递归遍历整棵 AST
    Visitor.TraverseDecl(Context.getTranslationUnitDecl());
  }
private:
  FindNamedClassVisitor Visitor;
};

3.3、RecursiveASTVisitor

RecursiveASTVisitor 为大多数 AST 节点提供 bool VisitNodeType(NodeType *) 形式的钩子,返回 false 可中断对该子树的遍历:

class FindNamedClassVisitor
  : public RecursiveASTVisitor<FindNamedClassVisitor> {
public:
  explicit FindNamedClassVisitor(ASTContext *Context) : Context(Context) {}

  bool VisitCXXRecordDecl(CXXRecordDecl *Declaration) {
    if (Declaration->getQualifiedNameAsString() == "n::m::C") {
      // getFullLoc 借助 ASTContext 的 SourceManager 解析出源文件行/列
      FullSourceLoc FullLocation = Context->getFullLoc(Declaration->getBeginLoc());
      if (FullLocation.isValid())
        llvm::outs() << "Found declaration at "
                     << FullLocation.getSpellingLineNumber() << ":"
                     << FullLocation.getSpellingColumnNumber() << "\n";
    }
    return true;
  }

private:
  ASTContext *Context;
};

要点:源码位置等信息不存储在 AST 节点本身,而在 ASTContext/SourceManager 里,所以要把 ASTContext* 传进 Visitor(在 CreateASTConsumer 期间从 CompilerInstance.getASTContext() 拿到)。

3.4、完整程序(两种 main)

版本一:直接分析传入的代码片段(runToolOnCode)

#include "clang/AST/ASTConsumer.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/FrontendAction.h"
#include "clang/Tooling/Tooling.h"

using namespace clang;
// ... 上面三段类定义 ...

int main(int argc, char **argv) {
  if (argc > 1) {
    clang::tooling::runToolOnCode(new FindNamedClassAction, argv[1]);
  }
}

版本二:读取 compile_commands.json 分析整个项目(CommonOptionsParser + ClangTool)

int main(int argc, const char **argv) {
  auto ExpectedParser = tooling::CommonOptionsParser::create(argc, argv, MyToolCategory);
  if (!ExpectedParser) {
    llvm::errs() << ExpectedParser.takeError();
    return 1;
  }
  clang::tooling::CommonOptionsParser &OptionsParser = ExpectedParser.get();
  clang::tooling::ClangTool Tool(OptionsParser.getCompilations(),
                                 OptionsParser.getSourcePathList());
  Tool.run(
      clang::tooling::newFrontendActionFactory<FindNamedClassAction>().get());
}

3.5、编译与运行

作为 clang-tools-extra 子项目编译:在 llvm-project/clang-tools-extra 下新建 FindClassDecls/,源文件 FindClassDecls.cpp,CMakeLists.txt:

add_clang_executable(find-class-decls FindClassDecls.cpp)
target_link_libraries(find-class-decls clangTooling)

并在 clang-tools-extra/CMakeLists.txt 中添加 add_subdirectory(FindClassDecls)。

编译 LLVM(Ninja 生成器,只编目标比全量编译快得多):

mkdir build && cd build
cmake -DLLVM_ENABLE_PROJECTS="clang;clang-tools-extra" \
      -DCMAKE_BUILD_TYPE=RelWithDebInfo -G "Ninja" ../llvm
cmake --build . --target find-class-decls

运行:

# 版本一:直接跑代码片段
./bin/find-class-decls "namespace n { namespace m { class C {}; } }"
# Found declaration at 1:29

# 版本二:结合 compile_commands.json 跑项目
./bin/find-class-decls -p /path/to/project/build test.cpp
# Found declaration at 1:29

四、ASTMatcher:更简洁的声明式匹配

参考资料:https://xinhuang.github.io/posts/2015-02-08-clang-tutorial-the-ast-matcher.html

RecursiveASTVisitor 是"手动遍历一切",ASTMatcher 则是声明式描述模式、由库完成匹配与绑定,写复杂规则时代码量少得多。

4.1、clang-query 交互式验证

写 C++ 之前先用 query 工具验证 matcher:

clang-query test.cpp
> match functionDecl(hasName("main"))
> match cxxRecordDecl(hasName("n::m::C")).bind("classes")

常用 matcher:functionDecl / cxxRecordDecl / cxxMethodDecl / callExpr / varDecl / forStmt / ifStmt,修饰器:hasName / isDerivedFrom / hasArgument / isPublic / forEachDescendant 等。

4.2、MatchFinder 版本

#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/Tooling/Tooling.h"

using namespace clang;
using namespace clang::ast_matchers;

class ClassPrinter : public MatchFinder::MatchCallback {
public:
  virtual void run(const MatchFinder::MatchResult &Result) {
    if (const CXXRecordDecl *D =
            Result.Nodes.getNodeAs<CXXRecordDecl>("classes")) {
      llvm::outs() << "Found class "
                   << D->getQualifiedNameAsString() << " at "
                   << D->getBeginLoc().printToString(*Result.SourceManager)
                   << "\n";
    }
  }
};

int main(int argc, const char **argv) {
  // ... CommonOptionsParser 同上 ...
  ClassPrinter Printer;
  MatchFinder Finder;
  Finder.addMatcher(
      cxxRecordDecl(hasName("n::m::C")).bind("classes"), &Printer);
  ClangTool Tool(OptionsParser.getCompilations(),
                 OptionsParser.getSourcePathList());
  Tool.run(newFrontendActionFactory(&Finder).get());
}

4.3、两种方式怎么选

RecursiveASTVisitor ASTMatcher
风格 命令式,Visit 钩子逐节点处理 声明式,一条规则描述一个模式
适用 对全树做通用处理、数据流类分析 定位特定代码模式(lint 规则、API 使用检查)
组合查询 手写 if 条件链 matcher 可任意嵌套组合,还能 bind 绑定结果

五、记录小结

这套东西的典型用途:研究性分析(把 C/C++ 项目解析成 AST 做实验)、写 lint 工具原型、编译器课程作业。工程界生产环境中大多数人用现成的 clang-tidy / clangd,只有少数工具链团队直接写 LibTooling 代码。

posted @ 2026-09-19 05:45  Stellan7  阅读(14)  评论(0)    收藏  举报