This post demonstrates how to create a minimal working example (MWE) of an LLVM pass that prints function names during compilation.

Getting LLVM

Install via package manager

Assuming Debian/Ubuntu:

  1. Update your package lists:
    sudo apt update
    
  2. Install LLVM and Clang:
    sudo apt install llvm clang
    
  3. Verify the installation with:
    clang --version
    

Build your own Yocto-based LLVM/Clang SDK

For an example of how to use Yocto to build your own, tailored, standalone LLVM/Clang SDK, see the Clang SDK Community Project.

The path to the SDK installer is something like: /path/to/kas_build/build/tmp/deploy/sdk/clang-sdk-glibc-x86_64-clang-sdk-image-core2-64-genericx86-64-toolchain-1.0.sh

Run the SDK installer; the default installation directory is /opt/clang-sdk/1.0/.

After installation, source the SDK environment file (typically located at /opt/clang-sdk/1.0/environment-setup-*) to set up variables like SDK_ENV_FILE and OECORE_NATIVE_SYSROOT.

Creating the Pass

This pass implements a simple function pass that iterates through all functions in the LLVM IR and prints their names to stderr. It serves as a minimal example demonstrating the new LLVM pass manager API and plugin system.

Create the pass source file MWEPass.cpp:

// MWEPass.cpp
#include "llvm/Passes/PassBuilder.h"
#include "llvm/Passes/PassPlugin.h"

using namespace llvm;

// A simple pass that prints the name of each function it processes
// PassInfoMixin provides the boilerplate for the new pass manager API
struct PrintFunctionNamesPass : PassInfoMixin<PrintFunctionNamesPass> {
  // This method is called for each function in the module
  PreservedAnalyses run(Function &F, FunctionAnalysisManager &) {
    errs() << "[MWEPass] Function: " << F.getName() << "\n";
    // Return PreservedAnalyses::all() since we don't modify the IR
    return PreservedAnalyses::all();
  }
};

// Plugin registration - allows the pass to be loaded dynamically at runtime
// The LLVM_ATTRIBUTE_WEAK allows the symbol to be overridden if needed
extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo
llvmGetPassPluginInfo() {
  return {LLVM_PLUGIN_API_VERSION, "MWEPass", "0.1", [](PassBuilder &PB) {
            // Register a callback to parse the pass name from command line
            PB.registerPipelineParsingCallback(
                [](StringRef Name, FunctionPassManager &FPM,
                   ArrayRef<PassBuilder::PipelineElement>) {
                  // The pass is invoked with: -mllvm -print-fns
                  if (Name == "print-fns") {
                    FPM.addPass(PrintFunctionNamesPass());
                    return true;
                  }
                  return false;
                });
          }};
}

Building the Pass

We need to get some LLVM build flags from the SDK’s llvm-config. So first we need to source the SDK environment file and set up build variables, in particular OECORE_NATIVE_SYSROOT, then under the native sysroot we can find llvm-config which helps us to get the LLVM build flags:

source "${SDK_ENV_FILE}"

NATIVE_LLVM_CONFIG="${OECORE_NATIVE_SYSROOT}/usr/bin/llvm-config"

LLVM_CXXFLAGS=$("${NATIVE_LLVM_CONFIG}" --cxxflags)
LLVM_LDFLAGS=$("${NATIVE_LLVM_CONFIG}" --ldflags)
LLVM_LIBDIR=$("${NATIVE_LLVM_CONFIG}" --libdir)

# GCC lib dir where libgcc_s.so, libgcc_s.so.1, are installed
GCC_LIB_DIR="${OECORE_NATIVE_SYSROOT}/lib"

Then build the pass as a shared library:

clang++ \
    -fPIC \
    -shared \
    ${LLVM_CXXFLAGS} \
    ${GCC_LIB_DIR:+-L"${GCC_LIB_DIR}"} \
    -o libMWEPass.so \
    MWEPass.cpp \
    ${LLVM_LDFLAGS} \
    -Wl,-rpath,"${LLVM_LIBDIR}" \
    ${GCC_LIB_DIR:+-Wl,-rpath,"${GCC_LIB_DIR}"}

We can double-check that the pass is actually linked against the native sysroot’s libraries:

${OECORE_NATIVE_SYSROOT}/lib/ld-linux-x86-64.so.2 --list ./libMWEPass.so

Output should be something like:

linux-vdso.so.1 (0x00007fff307ae000)
libstdc++.so.6 => /opt/clang-sdk/1.0/sysroots/x86_64-pokysdk-linux/usr/lib/libstdc++.so.6 (0x0000789be2600000)
libm.so.6 => /opt/clang-sdk/1.0/sysroots/x86_64-pokysdk-linux/lib/libm.so.6 (0x0000789be2920000)
libgcc_s.so.1 => /opt/clang-sdk/1.0/sysroots/x86_64-pokysdk-linux/lib/libgcc_s.so.1 (0x0000789be28f2000)
libc.so.6 => /opt/clang-sdk/1.0/sysroots/x86_64-pokysdk-linux/lib/libc.so.6 (0x0000789be2410000)
/opt/clang-sdk/1.0/sysroots/x86_64-pokysdk-linux/lib/ld-linux-x86-64.so.2 (0x0000789be2a14000)

Using the Pass

After building the pass, you can use it with Clang. First, create a simple test file:

// test.c
#include <stdio.h>

void hello() { printf("Hello, world!\n"); }

int add(int a, int b) { return a + b; }

int main() {
  hello();
  printf("2 + 3 = %d\n", add(2, 3));
  return 0;
}

Generate LLVM IR (Intermediate Representation) to inspect the intermediate code that LLVM works with. IR is a low-level, platform-independent representation of code that serves as the input to LLVM’s optimization passes and code generation:

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

This creates a programmer-readable .ll file containing the LLVM IR, helpful for understanding how the C code is represented internally:

; ModuleID = '/tmp/MWEPass/test.c'
source_filename = "/tmp/MWEPass/test.c"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-linux-gnu"

@.str.1 = private unnamed_addr constant [12 x i8] c"2 + 3 = %d\0A\00", align 1
@str = private unnamed_addr constant [14 x i8] c"Hello, world!\00", align 1

; Function Attrs: nofree nounwind uwtable
define dso_local void @hello() local_unnamed_addr #0 {
  %1 = tail call i32 @puts(ptr nonnull dereferenceable(1) @str)
  ret void
}

...

Run opt with plugin - order matters: load plugin BEFORE passes. This analyzes/transforms the IR using your custom pass.

opt \
    --load-pass-plugin ./libMWEPass.so \
    -passes=print-fns \
    -disable-output \
    ./test.ll

The pass will print the names of all functions to stderr during compilation. You should see output like:

[MWEPass] Function: hello
[MWEPass] Function: add
[MWEPass] Function: main

Alternatively, you can compile your code while running the pass plugin. The -fpass-plugin flag tells Clang to load your custom pass plugin, and -mllvm -print-fns instructs LLVM to run the pass named “print-fns” (as registered in the plugin). This allows you to see the function names printed during the compilation process:

clang -fpass-plugin=./libMWEPass.so -mllvm -print-fns test.c -o test

Note: The pass runs during the optimization phase, so you’ll see the output even if you’re just compiling (not optimizing). The pass doesn’t modify the IR, so it’s safe to use during normal compilation.