C++에서 LLVM/Clang 툴체인을 활용한 커스텀 컴파일러 패스 작성하기

LLVM과 Clang은 현대적인 컴파일러 인프라로, 다양한 최적화 및 코드 분석 기능을 제공합니다. 특히 LLVM은 강력한 중간 표현(IR)과 최적화 패스를 지원하며, Clang은 C, C++ 등의 소스 코드를 LLVM IR로 변환하는 프론트엔드 역할을 합니다.

개발자는 LLVM의 모듈식 설계를 활용하여 자신만의 커스텀 컴파일러 패스를 작성할 수 있습니다. 이를 통해 특정한 코드 최적화 기법을 적용하거나, 특정 코드 패턴을 감지하는 정적 분석 기능을 추가할 수 있습니다.

본 기사에서는 LLVM과 Clang을 사용하여 커스텀 컴파일러 패스를 작성하는 방법을 설명합니다. 먼저 LLVM과 Clang의 기본 개념을 살펴본 후, 간단한 패스 구현 예제를 통해 실습을 진행합니다. 이후 Clang 플러그인을 활용한 코드 분석 및 패스 디버깅 기법을 알아봅니다. 이를 통해 LLVM 툴체인을 활용하여 효율적인 코드 변환 및 최적화를 수행하는 방법을 익힐 수 있습니다.

LLVM과 Clang 개요

LLVM과 Clang은 현대적인 컴파일러 개발을 위한 강력한 툴체인으로, 다양한 프로그래밍 언어를 지원하고 최적화된 코드 생성을 가능하게 합니다.

LLVM이란?


LLVM(Low Level Virtual Machine)은 모듈형 컴파일러 인프라로, 다양한 프로그래밍 언어를 위한 백엔드를 제공하는 강력한 프레임워크입니다. LLVM의 주요 특징은 다음과 같습니다:

  • LLVM IR (Intermediate Representation): 중간 표현 계층을 활용하여 언어 독립적인 최적화 가능
  • 유연한 패스 기반 최적화 시스템: 다양한 분석 및 최적화 패스를 추가 가능
  • 다양한 타겟 아키텍처 지원: x86, ARM, RISC-V 등 여러 플랫폼을 위한 코드 생성 가능

LLVM은 전통적인 모놀리식 컴파일러 구조와 달리, 여러 독립적인 모듈로 구성되어 있어 새로운 기능을 쉽게 확장할 수 있습니다.

Clang이란?


Clang은 C, C++, Objective-C 등의 언어를 위한 프론트엔드이며, LLVM을 백엔드로 사용하여 최적화된 머신 코드를 생성합니다. Clang의 주요 장점은 다음과 같습니다:

  • 빠른 컴파일 속도: GCC보다 빠른 컴파일 타임 제공
  • 우수한 에러 메시지: 친절한 에러 메시지로 디버깅이 용이
  • 모듈형 구조: Clang 플러그인을 활용하여 기능 확장 가능

LLVM과 Clang의 관계


Clang은 소스 코드 → AST(Abstract Syntax Tree) → LLVM IR 변환 과정을 거쳐, LLVM 최적화 패스를 수행한 후 기계어 코드를 생성합니다. 이 과정에서 개발자는 커스텀 패스를 추가하여 특정한 코드 최적화 또는 분석 기능을 적용할 수 있습니다.

본 기사에서는 LLVM과 Clang의 이러한 기능을 활용하여 맞춤형 컴파일러 패스를 작성하는 방법을 단계별로 살펴보겠습니다.

커스텀 컴파일러 패스란?

컴파일러 패스(Compiler Pass)는 소스 코드가 기계어로 변환되는 과정에서 특정 분석 또는 변환 작업을 수행하는 단계를 의미합니다. LLVM에서는 이러한 패스를 자유롭게 추가 및 수정할 수 있으며, 이를 통해 특정 코드 패턴을 감지하거나 맞춤형 최적화 기법을 적용할 수 있습니다.

컴파일러 패스의 주요 역할


컴파일러 패스는 다음과 같은 역할을 수행할 수 있습니다:

  • 코드 최적화(Optimization): 불필요한 연산을 제거하고 실행 속도를 향상
  • 정적 분석(Static Analysis): 특정 패턴을 감지하여 코드 품질을 검사
  • 코드 변환(Code Transformation): 특정 언어 또는 프레임워크에 맞는 변환 수행

LLVM의 패스 기반 구조


LLVM은 패스 매니저(Pass Manager) 구조를 기반으로 동작하며, 여러 개의 패스가 체인 형태로 실행됩니다. 주요 패스 유형은 다음과 같습니다:

  • IR 분석 패스(Analysis Pass): 코드 분석 후 최적화에 필요한 정보를 제공
  • IR 변환 패스(Transformation Pass): 분석 결과를 활용하여 코드를 변환
  • IR 출력 패스(Output Pass): 변환된 IR을 출력하거나 머신 코드 생성

LLVM에서는 이러한 패스를 활용하여 사용자 맞춤형 코드 최적화 및 분석 기능을 구현할 수 있습니다.

커스텀 패스의 활용 사례


커스텀 패스는 다양한 용도로 활용될 수 있습니다:

  • 불필요한 연산 제거: x * 1 → x, y + 0 → y 등의 불필요한 연산을 제거하는 최적화 패스
  • 보안 분석: 메모리 접근 오류 및 취약점(예: 버퍼 오버플로우) 탐지
  • 코드 계측: 성능 측정을 위한 코드 삽입 (예: 함수 호출 횟수 기록)

본 기사에서는 LLVM을 활용하여 간단한 커스텀 패스를 작성하는 방법을 예제와 함께 소개합니다.

LLVM Pass 프레임워크 이해하기

LLVM은 모듈형 구조를 기반으로 다양한 컴파일러 패스(Pass) 를 실행하며, 개발자는 이 패스 프레임워크를 활용하여 커스텀 최적화 및 분석 기능을 추가할 수 있습니다.

LLVM Pass의 개념


LLVM 패스는 LLVM IR을 분석하거나 변환하는 기능을 수행하는 독립적인 모듈입니다. 이 패스는 소스 코드가 최종 기계어로 변환되는 과정에서 다양한 최적화 작업과 코드 변형을 수행합니다.

LLVM Pass의 주요 유형


LLVM 패스는 기능에 따라 다음과 같이 분류됩니다:

  1. 분석 패스(Analysis Pass)
  • 코드 구조를 분석하여 최적화 패스에 정보를 제공
  • 예: 데이터 흐름 분석, 메모리 접근 패턴 분석
  1. 변환 패스(Transformation Pass)
  • IR을 변형하여 최적화 수행
  • 예: 상수 폴딩(Constant Folding), 데드 코드 제거(Dead Code Elimination)
  1. IR 출력 패스(Output Pass)
  • IR을 출력하거나 특정 타겟 아키텍처용 기계어 생성
  • 예: Clang을 통해 AST에서 LLVM IR 생성 후 최적화 수행

LLVM Pass Manager


LLVM은 Pass Manager를 통해 여러 개의 패스를 체계적으로 실행합니다. 주요 Pass Manager는 다음과 같습니다:

  • FunctionPass: 함수 단위에서 IR을 분석 또는 변환
  • LoopPass: 루프 최적화에 특화된 패스
  • ModulePass: 모듈 단위에서 IR을 변형

LLVM의 패스 프레임워크를 이해하면 커스텀 패스를 추가하고 기존 최적화 로직을 개선할 수 있습니다. 다음 섹션에서는 커스텀 LLVM 패스를 작성하는 방법을 실습을 통해 알아보겠습니다.

커스텀 패스 개발을 위한 환경 설정

LLVM에서 커스텀 패스를 개발하려면 LLVM 및 Clang 개발 환경을 설정해야 합니다. 이 과정에는 LLVM 소스 코드 다운로드, 빌드, 개발 도구 설치 등이 포함됩니다.

1. LLVM 소스 코드 다운로드 및 빌드

LLVM 패스를 개발하려면 LLVM 소스 코드를 직접 빌드해야 합니다. 다음 절차를 따라 LLVM을 설정할 수 있습니다.

(1) 필수 패키지 설치 (Linux 기준)

sudo apt update
sudo apt install -y cmake ninja-build clang lld

(2) LLVM 소스 코드 다운로드

git clone https://github.com/llvm/llvm-project.git
cd llvm-project

(3) 빌드 디렉터리 생성 및 CMake 설정

mkdir build && cd build
cmake -G Ninja -DLLVM_ENABLE_PROJECTS="clang" -DCMAKE_BUILD_TYPE=Debug ../llvm

(4) LLVM 빌드 실행

ninja

빌드가 완료되면 bin/optbin/clang 바이너리가 생성되며, 이를 통해 LLVM 패스를 테스트할 수 있습니다.

2. 개발을 위한 기본 도구 설정

LLVM 패스를 개발할 때 필요한 도구는 다음과 같습니다:

  • Clang: 소스 코드를 LLVM IR로 변환하는 프론트엔드
  • opt: LLVM 최적화 및 패스 테스트 도구
  • llc: LLVM IR을 기계어 코드로 변환
  • lit: 테스트 자동화 도구

3. 프로젝트 디렉터리 구조

커스텀 패스를 작성할 때는 아래와 같은 구조로 프로젝트를 구성하는 것이 일반적입니다.

my-pass/
│── CMakeLists.txt
│── MyPass.cpp
└── test/
    └── sample.ll
  • MyPass.cpp: LLVM 패스를 구현하는 C++ 소스 코드
  • CMakeLists.txt: 패스 빌드를 위한 CMake 설정 파일
  • test/sample.ll: 테스트할 LLVM IR 코드

4. CMake 설정 파일 작성

LLVM 패스를 빌드하기 위해 CMakeLists.txt 파일을 생성하고 다음 내용을 추가합니다.

cmake_minimum_required(VERSION 3.16)
project(MyPass)

find_package(LLVM REQUIRED CONFIG)
add_definitions(${LLVM_DEFINITIONS})
include_directories(${LLVM_INCLUDE_DIRS})
add_library(MyPass MODULE MyPass.cpp)

이제 LLVM 빌드 환경이 준비되었으며, 다음 단계에서 커스텀 패스를 직접 구현하는 방법을 알아보겠습니다.

간단한 LLVM 패스 작성 예제

이제 간단한 LLVM 패스를 작성하여 IR을 분석하고 변환하는 방법을 실습해 보겠습니다.

1. LLVM 패스 개요


LLVM에서는 패스를 통해 IR(Intermediate Representation) 을 변형할 수 있습니다. 예제에서는 모든 함수의 이름을 출력하는 단순한 분석 패스를 구현합니다.

2. 패스 코드 작성


파일: MyPass.cpp

#include "llvm/IR/Function.h"
#include "llvm/Pass.h"
#include "llvm/Support/raw_ostream.h"

using namespace llvm;

namespace {
  struct MyPass : public FunctionPass {
    static char ID;
    MyPass() : FunctionPass(ID) {}

    bool runOnFunction(Function &F) override {
      errs() << "Function Name: " << F.getName() << "\n";
      return false; // IR을 수정하지 않으므로 false 반환
    }
  };
}

char MyPass::ID = 0;
static RegisterPass<MyPass> X("my-pass", "Simple Function Name Printer", false, false);

3. 패스 빌드


CMake를 사용하여 패스를 빌드합니다.

mkdir build && cd build
cmake ..
make

빌드가 성공하면 MyPass.so 또는 MyPass.dylib이 생성됩니다.

4. LLVM IR에서 패스 실행


패스를 실행하기 위해 Clang을 사용하여 테스트용 LLVM IR 코드를 생성합니다.

clang -O1 -emit-llvm -S -o test.ll test.c

그런 다음, opt를 사용하여 패스를 실행합니다.

opt -load ./MyPass.so -my-pass < test.ll

이제 IR에 포함된 모든 함수의 이름이 출력되는지 확인할 수 있습니다.

5. 패스 동작 확인


출력 예제:

Function Name: main
Function Name: foo
Function Name: bar

이제 LLVM 패스를 작성하고 실행하는 기본적인 방법을 익혔습니다. 다음 단계에서는 최적화 패스를 추가하여 IR을 수정하는 방법을 살펴보겠습니다.

최적화 패스 작성 및 테스트

이제 LLVM의 기능을 활용하여 코드를 최적화하는 커스텀 패스를 작성하고 테스트하는 방법을 살펴보겠습니다.

1. 최적화 패스 개요


최적화 패스는 코드를 분석하여 불필요한 연산을 제거하거나 성능을 개선하는 역할을 합니다. 예제로, x * 2 연산을 x + x로 변환하는 간단한 최적화 패스를 구현해 보겠습니다.

2. 최적화 패스 코드 작성


아래 코드는 mul 연산이 포함된 IR을 탐색하여 x * 2x + x로 변환하는 패스입니다.

파일: MyOptPass.cpp

#include "llvm/IR/Function.h"
#include "llvm/IR/Instructions.h"
#include "llvm/Pass.h"
#include "llvm/Support/raw_ostream.h"

using namespace llvm;

namespace {
  struct MyOptPass : public FunctionPass {
    static char ID;
    MyOptPass() : FunctionPass(ID) {}

    bool runOnFunction(Function &F) override {
      bool modified = false;

      for (auto &BB : F) {  
        for (auto &Inst : BB) {
          if (auto *Mul = dyn_cast<MulInst>(&Inst)) {  
            if (auto *Const = dyn_cast<ConstantInt>(Mul->getOperand(1))) {  
              if (Const->getValue() == 2) {  
                IRBuilder<> Builder(Mul);
                Value *NewAdd = Builder.CreateAdd(Mul->getOperand(0), Mul->getOperand(0));
                Mul->replaceAllUsesWith(NewAdd);
                Mul->eraseFromParent();
                modified = true;
              }
            }
          }
        }
      }
      return modified; 
    }
  };
}

char MyOptPass::ID = 0;
static RegisterPass<MyOptPass> X("my-opt-pass", "Multiply by 2 Optimization", false, false);

3. 패스 빌드 및 테스트

(1) 패스 빌드

mkdir -p build && cd build
cmake ..
make

(2) 테스트 코드 작성 (test.c)

int foo(int x) {
    return x * 2;
}

(3) LLVM IR 생성

clang -O1 -emit-llvm -S -o test.ll test.c

(4) 패스 실행

opt -load ./MyOptPass.so -my-opt-pass < test.ll -S -o optimized.ll

4. 결과 확인


기본적으로 test.ll에는 다음과 같은 IR 코드가 포함됩니다.

%1 = mul nsw i32 %x, 2
ret i32 %1

최적화 패스를 적용한 후 optimized.ll을 확인하면, mul 연산이 add 연산으로 대체됩니다.

%1 = add nsw i32 %x, %x
ret i32 %1

5. 최적화 패스 확장 가능성


이러한 방식으로 불필요한 연산을 줄이거나 특정 연산 패턴을 변환하는 다양한 최적화 패스를 추가할 수 있습니다.

다음 단계에서는 Clang 플러그인을 활용하여 프론트엔드에서 추가적인 코드 분석 기능을 적용하는 방법을 살펴보겠습니다.

Clang 플러그인을 이용한 프론트엔드 확장

LLVM의 백엔드 패스를 활용하여 최적화를 수행할 수도 있지만, 경우에 따라 프론트엔드에서 직접 코드를 분석 및 변형하는 것이 더 효율적일 수도 있습니다. 이를 위해 Clang은 플러그인 기능을 제공하며, 이를 활용하여 AST(Abstract Syntax Tree) 수준에서 코드를 분석하고 변형하는 기능을 추가할 수 있습니다.

1. Clang 플러그인이란?


Clang 플러그인은 소스 코드의 AST를 분석하고, 특정 패턴을 감지하거나 경고 메시지를 추가하는 기능을 수행합니다. 예를 들어, 안전하지 않은 함수 사용을 감지하거나, 특정 코드 스타일을 강제하는 정적 분석 도구를 개발할 수 있습니다.

2. 간단한 Clang 플러그인 작성

예제에서는 모든 변수 선언을 감지하고 출력하는 Clang 플러그인을 작성합니다.

파일: MyClangPlugin.cpp

#include "clang/AST/ASTConsumer.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Frontend/FrontendPluginRegistry.h"

using namespace clang;

namespace {
  struct MyASTVisitor : public RecursiveASTVisitor<MyASTVisitor> {
    bool VisitVarDecl(VarDecl *D) {
      llvm::errs() << "Variable Declaration: " << D->getNameAsString() << "\n";
      return true;
    }
  };

  struct MyASTConsumer : public ASTConsumer {
    void HandleTranslationUnit(ASTContext &Context) override {
      MyASTVisitor Visitor;
      Visitor.TraverseDecl(Context.getTranslationUnitDecl());
    }
  };

  struct MyPluginAction : public PluginASTAction {
    std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, llvm::StringRef) override {
      return std::make_unique<MyASTConsumer>();
    }

    bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &Args) override {
      return true;
    }
  };
}

static FrontendPluginRegistry::Add<MyPluginAction>
X("my-clang-plugin", "Detect variable declarations");

3. 플러그인 빌드

(1) CMakeLists.txt 설정

cmake_minimum_required(VERSION 3.16)
project(MyClangPlugin)

find_package(LLVM REQUIRED CONFIG)
find_package(Clang REQUIRED CONFIG)

include_directories(${LLVM_INCLUDE_DIRS} ${CLANG_INCLUDE_DIRS})
add_definitions(${LLVM_DEFINITIONS})

add_library(MyClangPlugin SHARED MyClangPlugin.cpp)

(2) 빌드 실행

mkdir build && cd build
cmake ..
make

빌드가 완료되면 MyClangPlugin.so 파일이 생성됩니다.

4. 플러그인 실행

Clang을 사용하여 변수 선언을 감지하는 플러그인을 실행할 수 있습니다.

clang -Xclang -load -Xclang ./MyClangPlugin.so -Xclang -plugin -Xclang my-clang-plugin test.c

5. 실행 결과

test.c의 내용이 다음과 같다고 가정하면,

int main() {
    int a = 10;
    float b = 3.14;
    return 0;
}

출력 결과는 다음과 같습니다.

Variable Declaration: a
Variable Declaration: b

6. Clang 플러그인의 활용

이처럼 Clang 플러그인을 사용하면 코드 스타일 검사, 정적 분석, 특정 코드 패턴 감지 등의 기능을 추가할 수 있습니다.

다음 단계에서는 LLVM의 디버깅 도구를 활용하여 커스텀 패스의 동작을 분석하고 최적화하는 방법을 살펴보겠습니다.

커스텀 패스 디버깅 및 최적화

LLVM에서 커스텀 패스를 개발할 때는 디버깅 및 최적화 과정이 필수적입니다. LLVM은 이를 위해 다양한 디버깅 도구 및 옵션을 제공하며, 이를 활용하면 패스의 동작을 확인하고 성능을 개선할 수 있습니다.

1. LLVM 디버깅 도구 및 옵션

LLVM은 패스의 동작을 분석할 수 있도록 다양한 디버깅 도구를 제공합니다. 주요 디버깅 옵션은 다음과 같습니다:

  • -debug-pass: 실행되는 패스를 출력
  • -print-before / -print-after: 패스 실행 전후의 IR을 출력
  • opt -debug-only: 특정 패스의 디버깅 메시지를 출력

2. 패스 실행 로그 확인

패스가 실행될 때 어떤 변경이 이루어졌는지 확인하려면 -debug-pass 옵션을 사용합니다.

opt -load ./MyPass.so -my-pass -debug-pass=Structure < test.ll -S -o output.ll

출력 예시:

Pass Arguments:  -my-pass
Running pass: MyPass on function @main

3. 패스 실행 전후의 IR 비교

패스가 실행되기 전후의 IR 변화를 확인하려면 -print-before-print-after 옵션을 사용합니다.

opt -load ./MyPass.so -my-pass -print-before -print-after < test.ll -S -o output.ll

출력 예시:

*** IR Before MyPass ***
define i32 @main() {
  %1 = mul i32 %x, 2
  ret i32 %1
}
--------------------------
*** IR After MyPass ***
define i32 @main() {
  %1 = add i32 %x, %x
  ret i32 %1
}

위 결과에서 mul 연산이 add 연산으로 변환된 것을 확인할 수 있습니다.

4. `errs()`를 활용한 디버깅

LLVM은 errs() 스트림을 활용하여 디버깅 출력을 지원합니다. 패스 코드 내에 다음과 같은 출력을 추가하면 동작을 쉽게 추적할 수 있습니다.

errs() << "Processing function: " << F.getName() << "\n";
errs() << "Instruction found: " << *Inst << "\n";

이렇게 하면 실행 시 함수 및 명령어 정보가 출력됩니다.

5. `opt -debug-only`를 활용한 특정 패스 디버깅

LLVM은 특정 패스의 디버깅 메시지만 출력하는 opt -debug-only 옵션을 제공합니다.

opt -load ./MyPass.so -my-pass -debug-only=my-pass < test.ll

이렇게 하면 LLVM_DEBUG() 매크로를 활용하여 특정 패스의 디버깅 정보를 상세히 확인할 수 있습니다.

6. 성능 최적화 기법

LLVM 패스의 성능을 최적화하려면 다음 기법을 활용할 수 있습니다.

  1. IR Traversal 최적화: 필요하지 않은 IR 탐색을 줄이고, 특정 블록에서만 실행하도록 개선
  2. Lazy Analysis 사용: 불필요한 데이터 분석을 방지하고 성능을 향상
  3. LLVM의 AnalysisUsage 활용: 기존 분석 정보를 재사용하여 성능 개선
void getAnalysisUsage(AnalysisUsage &AU) const override {
  AU.setPreservesAll();
}

이를 활용하면 불필요한 패스 실행을 방지하여 컴파일 시간 단축이 가능합니다.

7. 요약

  • -debug-pass, -print-before, -print-after 옵션을 활용하여 패스 실행 로그를 확인
  • errs()opt -debug-only를 사용하여 특정 패스의 디버깅 메시지를 출력
  • 성능 최적화를 위해 IR 탐색 최소화 및 Lazy Analysis 기법 활용

이제 LLVM에서 커스텀 패스를 효과적으로 디버깅하고 최적화하는 방법을 익혔습니다. 이를 활용하면 더욱 강력하고 최적화된 패스를 개발할 수 있습니다.

요약

본 기사에서는 C++에서 LLVM/Clang을 활용하여 커스텀 컴파일러 패스를 작성하는 방법을 소개했습니다.

우선 LLVM과 Clang의 개념과 역할을 살펴보고, 컴파일러 패스의 원리와 프레임워크 구조를 이해했습니다. 이후, LLVM 개발 환경을 설정하는 방법을 설명하고, 간단한 패스를 구현하여 실행하는 과정을 실습했습니다.

또한, 최적화 패스를 작성하여 IR을 변형하는 방법Clang 플러그인을 활용하여 AST 분석을 수행하는 방법을 다뤘습니다. 마지막으로 LLVM의 디버깅 도구를 활용하여 패스를 분석하고 최적화하는 기법을 정리했습니다.

LLVM의 유연한 패스 시스템을 활용하면 맞춤형 코드 최적화, 정적 분석, 보안 검사 등을 수행할 수 있습니다. 본 가이드를 바탕으로 LLVM 패스를 직접 작성하고, 다양한 프로젝트에 적용해 보시길 바랍니다.