LLVM系列第十四章:写一个简单的语义分析器Semantic Analyzer

系列文章目录

LLVM系列第一章:编译LLVM源码
LLVM系列第二章:模块Module
LLVM系列第三章:函数Function
LLVM系列第四章:逻辑代码块Block
LLVM系列第五章:全局变量Global Variable
LLVM系列第六章:函数返回值Return
LLVM系列第七章:函数参数Function Arguments
LLVM系列第八章:算术运算语句Arithmetic Statement
LLVM系列第九章:控制流语句if-else
LLVM系列第十章:控制流语句if-else-phi
LLVM系列第十一章:写一个Hello World
LLVM系列第十二章:写一个简单的词法分析器Lexer
LLVM系列第十三章:写一个简单的语法分析器Parser
LLVM系列第十四章:写一个简单的语义分析器Semantic Analyzer
LLVM系列第十五章:写一个简单的中间代码生成器IR Generator
LLVM系列第十六章:写一个简单的编译器
LLVM系列第十七章:for循环
LLVM系列第十八章:写一个简单的IR处理流程Pass
LLVM系列第十九章:写一个简单的Module Pass
LLVM系列第二十章:写一个简单的Function Pass
LLVM系列第二十一章:写一个简单的Loop Pass
LLVM系列第二十二章:写一个简单的编译时函数调用统计器(Pass)
LLVM系列第二十三章:写一个简单的运行时函数调用统计器(Pass)
LLVM系列第二十四章:用Xcode编译调试LLVM源码
LLVM系列第二十五章:简单统计一下LLVM源码行数
LLVM系列第二十六章:理解LLVMContext
LLVM系列第二十七章:理解IRBuilder
LLVM系列第二十八章:写一个JIT Hello World
LLVM系列第二十九章:写一个简单的常量加法“消除”工具(Pass)

flex&bison系列



前言

在此记录下,基于LLVM写一个简单的语义分析器(Simple Semantic Analyzer)的过程,以备查阅。

开发环境的配置请参考 《LLVM系列第一章:编译LLVM源码》。

我们再来简单复习一下,编译器前端的流程:

词法分析Lexical
语法分析Syntax
语义分析Semantic
中间代码IR

更多关于编译器前端的介绍,请参看《LLVM系列第三章:写一个简单的词法分析器Lexer》。

本章内容仅与语义分析(Semantic Analysis)有关,是一个最简单的示例而已。与词法分析(Lexical Analysis)及语法分析(Syntax Analysis)相关的文章,请参看《LLVM系列第三章:写一个简单的词法分析器Lexer》和《LLVM系列第四章:写一个简单的语法分析器Parser》。

一、SimpleLang语言

为了方便起见,我们自己定义一种很简单的语言(名为SimpleLang)如下(示例):

calc : ("with" ident ("," ident)* ":")? expr ;
expr: term(("+"|"-")term)* ;
term : factor (( "*" | "/") factor)* ;
factor : ident | number | "(" expr ")" ;
ident : ([a-zAZ])+ ;
number : ([0-9])+ ;

这也是我们在前面章节中用到的语言。

二、项目结构

我们把这个简单的项目命名为SimpleSemanticAnalyzer。项目组织结构与前一章的项目类似,具体如下(示例):

% tree -I "build|build-xcode"
.
├── CMakeLists.txt
├── README.md
└── src
    ├── AST.h
    ├── CMakeLists.txt
    ├── Lexer.cpp
    ├── Lexer.h
    ├── Parser.cpp
    ├── Parser.h
    ├── SemanticAnalyzer.cpp
    ├── SemanticAnalyzer.h
    └── SemanticAnalyzerPlayer.cpp

各文件的内容大体如下:

  1. AST.h,SimpleLang语言的抽象语法树(AST)的定义及实现代码。
  2. Lexer.h和Lexer.cpp,SimpleLang语言的词法分析器(Lexer)的定义及实现代码。
  3. Parser.h和Parser.cpp,SimpleLang语言的语法分析器(Parser)的定义及实现代码。
  4. SemanticAnalyzer.h和SemanticAnalyzer.cpp,SimpleLang语言的语义分析器(Semantic Analyzer)的定义及实现代码。这是本章的重点。
  5. SemanticAnalyzerPlayer.cpp,main函数,即SemanticAnalyzer的测试代码。

三、项目细节

1. 程序模块

这个简单的项目只包含了一个模块:

  1. SimpleSemanticAnalyzer,一个可执行程序文件

以下是跟项目组织结构相关的部分CMake脚本,与前一章的CMake脚本类似。

(1) 项目根目录(示例):

# CMakeLists.txt

...
project ("SimpleSemanticAnalyzer")
...
add_subdirectory ("src")

这里创建了一个项目(project),并把src目录下的子项目加入进来。

(2) src目录(示例):

# src/CMakeLists.txt

...
add_executable(SimpleSemanticAnalyzer ...)
...

这是src目录下的子项目,用来构建SimpleSemanticAnalyzer程序。

2. 引入LLVM

我们需要做一些与LLVM相关的配置,才能顺利地使用LLVM(示例):

# CMakeLists.txt

...
find_package(LLVM REQUIRED CONFIG)
message("Found LLVM ${LLVM_PACKAGE_VERSION}, build type ${LLVM_BUILD_TYPE}")
list(APPEND CMAKE_MODULE_PATH ${LLVM_DIR})
...
add_definitions(${LLVM_DEFINITIONS})
include_directories(SYSTEM ${LLVM_INCLUDE_DIRS})
llvm_map_components_to_libnames(llvm_libs Core)
...
# src/CMakeLists.txt

...
target_link_libraries(SimpleSemanticAnalyzer PRIVATE ${llvm_libs})

3. Simple Semantic Analyzer

词法、语法分析的相关代码已在前面章节介绍,本章的重点在语义分析上:

  1. src/SemanticAnalyzerPlayer.cpp,包含了main函数,即测试代码
  2. src/SemanticAnalyzer.h,src/SemanticAnalyzer.cpp,包含了语义分析器的定义及实现代码

main函数(示例):

#include "SemanticAnalyzer.h"
...
static llvm::cl::opt<std::string> input(llvm::cl::Positional, llvm::cl::desc("<input expression>"), llvm::cl::init(""));

int main(int argc, const char** argv)
{
    llvm::InitLLVM llvmInitializer(argc, argv);
    llvm::cl::ParseCommandLineOptions(argc, argv, "SimpleParser - a simple code parser\n");
    llvm::outs() << "Input: \"" << input << "\"\n";

    Lexer lexer(input);
    Parser parser(lexer);
    AST* tree = parser.Parse();
    ...
    SemanticAnalyzer semanticAnalyzer;
    if (semanticAnalyzer.Analysis(tree))
    {
        llvm::errs() << "Semantic errors occured\n";
        return 1;
    }
    ...
}

我们看到以上代码调用了SemanticAnalyzer来做语义分析。如果发现语义上的问题,则会打印出错误报告。SemanticAnalyzer的定义如下(示例):

class SemanticAnalyzer
{
public:

    bool Analysis(AST* tree);
};

定义是很简单的,实现如下(示例):

namespace
{
    class DeclarationChecker : public ASTVisitor
    {
    public:

        DeclarationChecker() :
            hasError(false)
        {
        }

        bool HasError()
        {
            return hasError;
        }

        void Visit(Factor& node) override
        {
            if (node.GetType() == Factor::kIdent)
            {
                if (scope.find(node.GetValue()) == scope.end())
                {
                    AddError(kNotDeclared, node.GetValue());
                }
            }
        };

        void Visit(BinaryOp& node) override
        {
            if (node.GetLeft())
            {
                node.GetLeft()->Accept(*this);
            }
            else
            {
                hasError = true;
            }

            if (node.GetRight())
            {
                node.GetRight()->Accept(*this);
            }
            else
            {
                hasError = true;
            }
        };

        void Visit(WithDeclaration& node) override
        {
            for (auto variable : node)
            {
                if (!scope.insert(variable).second)
                {
                    AddError(kDeclaredTwice, variable);
                }
            }

            if (node.GetExpr())
            {
                node.GetExpr()->Accept(*this);
            }
            else
            {
                hasError = true;
            }
        };

    private:

        enum ErrorType
        {
            kDeclaredTwice,
            kNotDeclared
        };

    private:

        void AddError(ErrorType errorType, llvm::StringRef variable)
        {
            llvm::errs() << "Variable " << variable << " " << (errorType == kDeclaredTwice ? "already" : "not")
                         << " declared\n";
            hasError = true;
        }

    private:

        llvm::StringSet<> scope;
        bool hasError;
    };
} // namespace

bool SemanticAnalyzer::Analysis(AST* tree)
{
    if (!tree)
    {
        return false;
    }

    DeclarationChecker checker;
    tree->Accept(checker);
    return checker.HasError();
}

注意到,这个语义分析器只是简单地分析了一下变量的申明,因为这仅仅是个示例程序而已。关于抽象语法树(AST)的定义及实现请参考前面章节。这个示例中,语义分析其实就是遍历AST的节点,并检查每个节点上的变量申明是否符合SimpleLang语言的规则。而具体的遍历及检查的工作,我们交给了DeclarationChecker来做:

ASTVisitor
+Visit(AST&)
+Visit(Expr&)
+Visit(Factor&)
+Visit(BinaryOp&)
+Visit(WithDeclaration&)
DeclarationChecker
+Visit(Factor&)
+Visit(BinaryOp&)
+Visit(WithDeclaration&)

四、编译

1. 生成项目文件

用CMake生成项目文件(示例):

mkdir build
cd build

cmake -G Ninja -DCMAKE_BUILD_TYPE=Debug ..

输出log如下(示例):

-- The C compiler identification is AppleClang 13.0.0.13000029
-- The CXX compiler identification is AppleClang 13.0.0.13000029
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Found ZLIB: /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.1.sdk/usr/lib/libz.tbd (found version "1.2.11") 
-- Found LibXml2: /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.1.sdk/usr/lib/libxml2.tbd (found version "2.9.4") 
Found LLVM 12.0.1, build type Release
-- Configuring done
-- Generating done
-- Build files have been written to: .../SimpleSemanticAnalyzer/build

如果要生成Xcode项目文件,我们稍微改一下cmake命令的参数即可(示例):

mkdir build-xcode
cd build-xcode

cmake -G Xcode -DCMAKE_BUILD_TYPE=Debug ..

2. 编译

在编译之前,我们可以用clang-format工具把代码美化一下(示例):

cd /path/to/SimpleSemanticAnalyzer

clang-format -i src/*.cpp src/*.h

用ninja进行编译(示例):

cd /path/to/SimpleSemanticAnalyzer/build

ninja

输出log如下(示例):

[5/5] Linking CXX executable src/SimpleSemanticAnalyzer

3. 运行

运行SimpleSemanticAnalyzer(示例):

src/SimpleSemanticAnalyzer "with abc,xyz: (abc+xyz)*3 - 10/abc"

我们用于测试的SimpleLang程序代码,就这么简单的一句而已with abc,xyz: (abc+xyz)*3 - 10/abc。输出结果如下(示例):

Input: "with abc,xyz: (abc+xyz)*3 - 10/abc"
Semantic check passed

我们再用一句有语法错误的代码测试一下(示例):

src/SimpleSemanticAnalyzer "with a,a: b*3"

输出结果如下(示例):

Input: "with a,a: b*3"
Variable a already declared
Variable b not declared
Semantic errors occured

可以看到,语义分析器可以检测到与变量申明有关的错误。其中,变量a被声明了两次,变量b缺少了申明。

五、总结

我们参考编译器设计中常用的数据结构定义及算法,基于LLVM提供的API,用C++写了一个很简单的词法分析器,并且编译运行成功。完整源码示例请参看:
https://github.com/wuzhanglin/llvm-simple-semantic-analyzer

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
污点分析是一种静态程序分析技术,用于确定程序中哪些变量可以被恶意输入或其他安全漏洞所利用。LLVM一个广泛使用的编译器基础设施,可以用于实现污点分析。下面是一个简单LLVM Pass,它实现了简单的污点分析。 首先,我们需要定义一个Pass类,该类继承自llvm::FunctionPass。然后,我们需要在runOnFunction函数中实现我们的污点分析逻辑。在这个例子中,我们将通过检查函数的参数和指令来确定哪些变量是受污染的。 ```c++ #include "llvm/IR/Function.h" #include "llvm/Pass.h" #include "llvm/Support/raw_ostream.h" #include "llvm/IR/Instructions.h" using namespace llvm; namespace { struct TaintAnalysis : public FunctionPass { static char ID; TaintAnalysis() : FunctionPass(ID) {} bool runOnFunction(Function &F) override { // 遍历函数的所有基本块 for (auto &BB : F) { // 遍历基本块的所有指令 for (auto &I : BB) { // 如果指令是一个存储指令 if (auto *SI = dyn_cast<StoreInst>(&I)) { // 如果存储指令的源操作数是一个指针类型 if (auto *Ptr = dyn_cast<PointerType>(SI->getOperand(1)->getType())) { // 如果指针指向的类型是整数类型 if (auto *IntTy = dyn_cast<IntegerType>(Ptr->getElementType())) { // 如果整数类型的位宽为8 if (IntTy->getBitWidth() == 8) { // 输出受污染的指针值和存储的值 errs() << "Tainted pointer value: " << *SI->getOperand(1) << "\n"; errs() << "Tainted value: " << *SI->getOperand(0) << "\n"; } } } } } } return false; } }; } char TaintAnalysis::ID = 0; static RegisterPass<TaintAnalysis> X("taint-analysis", "Taint Analysis Pass"); ``` 我们在runOnFunction函数中遍历函数的所有基本块和指令。我们检查每个存储指令,以确定它是否存储了一个指向整数类型的指针,并且该整数类型具有8位的位宽。如果是的话,我们输出受污染的指针值和存储的值。 最后,我们将该Pass注册到LLVM中,以便在编译时运行。我们使用static RegisterPass来注册我们的Pass,并将其命名为“taint-analysis”。 现在,我们可以使用LLVM编译器运行我们的Pass,以便对C或C++程序进行污点分析。例如,假设我们有以下C程序: ```c++ #include <stdio.h> void foo(int *ptr) { int x = *ptr; printf("The value of x is: %d\n", x); } int main() { int y = 42; foo(&y); return 0; } ``` 我们可以使用以下命令编译程序并运行我们的Pass: ``` clang -Xclang -load -Xclang MyPass.so -c test.c ``` 这将生成一个名为“test.o”的目标文件,并使用我们的Pass进行污点分析。如果程序中存在受污染的指针,我们的Pass将输出它们的值。在这个例子中,我们应该得到以下输出: ``` Tainted pointer value: i32* %ptr Tainted value: i32 42 ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值