SVF-based call graph analysis tool that performs Andersen-style pointer analysis on LLVM bitcode and outputs a topological call graph tree.
svf-api-demo/src/main.cpp— CLI driver: parses flags, loads LLVM bitcode, runs the pointer analysis, and streams per-entry call graphs as tab-separated lines.svf-api-demo/src/callgraph_analysis.cpp/svf-api-demo/include/callgraph_analysis.h— Analysis core (analyzeCallGraph) andAnalysisConfig.svf-api-demo/CMakeLists.txt— CMake build (out-of-source intobuild/): builds the analyzer, the native test program, and the test bitcode.svf-api-demo/build/clang_analyzer— Compiled analyzer binary (CMake build output).
| File | Description |
|---|---|
svf-api-demo/tests/test_prog.c |
Simple C program with direct function calls (foo, bar, zoo) |
svf-api-demo/tests/test_complex.cpp |
Comprehensive C++ test covering all indirect call patterns |
The complex test covers the following call patterns that SVF's pointer analysis must resolve:
directCall()→printf— trivial direct call
- Basic function pointer:
FuncPtr fp = calleeViaPtr; fp(); - Function pointer array:
FuncPtr table[3] = {fa, fb, fc}; table[i](); - Callback parameter:
sortWithCallback(arr, n, cmp)wherecmpis a function pointer parameter, called withcompareAsc/compareDesc - Pointer to function pointer (double/triple indirection):
FuncPtr* fpp = &fp; FuncPtr** fppp = &fpp; - Struct member function pointer:
Handlerstruct withHandlerFunc onEventmember, including runtime swap between instances - Function returning function pointer:
chooseHandler(kind)returnsfaorfb; caller invokes the returned pointer - Array of pointers to function pointers:
FuncPtr* fptrs[2] = {&fns[0], &fns[1]}; (*fptrs[i])(); - Nested callback:
middleWare()receives both aComparatorand avoid (*report)(int)function pointer, demonstrating multi-layer indirect call chains
overloaded(int)andoverloaded(double)— resolved via LLVM mangling
- Single virtual inheritance:
Base* b = new Derived(); b->virtualMethod(); - Polymorphic array:
Animal* animals[3]holdingAnimal,Dog,Catinstances, iterated in a loop - Multiple inheritance:
MultiDerivedinherits from bothMixinAandMixinB, each with virtual methods; calls dispatch through each base pointer with this-pointer adjustment - Diamond virtual inheritance:
DiamondTipinherits fromMidLeftandMidRight, both virtually derived fromDiamondBase; virtual base pointer resolution - Abstract base class dispatch:
AbstractBasewith pure virtualdoit()/report(), dispatched through array ofConcreteA/ConcreteBpointers - Multi-level virtual chain:
VBase → VMid → VDerivedthree-level override hierarchy dispatched through base pointer array - Virtual calls in constructor/destructor:
TraceDerivedcalls virtuallog()during construction and destruction (dynamic type changes during these contexts) - Pure virtual call in base destructor:
AbstractBase::~AbstractBase()dispatches viallvm.trap(undefined behavior guard)
- Stateless lambda:
[](){} - Capture-by-value lambda with return:
[x](int y) -> int - Capture-by-reference lambda calling another function
- Generic lambda (C++14):
[](auto a, auto b){}— templated call operator instantiated withintanddouble - Capture by move (C++14):
[p = ptr](){}— lambda captures a dynamically allocated pointer via move - Nested lambda: Lambda
makeAdder()returns a capturing lambda; closure-in-closure call chain - Lambda wrapped in std::function: Stateless lambda assigned to
std::function<void()>, invoked through the type-erased wrapper
- std::function: Wraps free functions (
stdFuncTarget1,stdFuncTarget2), a lambda, and supports reassignment between targets - std::bind: Binds arguments to
bindTarget(a,b,c)usingstd::placeholders, creating callable objects with fewer parameters - std::function + bind: A
std::function<void()>wrapping astd::bindexpression - std::invoke (C++17): Unified call syntax invoking a free function, a member function (
Calculator::addvia pointer-to-member), and a lambda
- Pointer to member function:
int (Calculator::*MathOp)(int,int)called via.*and->*syntax on instances ofCalculator, selectingadd,sub,mulat runtime - Functor (function object):
Greeterclass withoperator()(const char*)— different instances hold different state; callinghello("World")vsgoodbye("World")dispatches to the same operator with different internal data
- Ternary/conditional dispatch:
FuncPtr f = flag ? condTrue : condFalse;— runtime condition selects between two function pointers - State machine with function pointer table:
StateHandler states[3] = {stateIdle, stateRunning, stateStopped};dispatched in a loop - Function pointer reassignment in loop:
FuncPtr fpreassigned each iteration through a loop overfns[]array - Struct dispatch table:
DispatchEntrystruct array withnamestring andhandlerfunction pointer; iterated and invoked - Function pointer on struct with operator():
CallableOpsstruct wrapping a function pointer, called viaoperator()()overload
- Direct recursion:
factorial(n)calls itself withn-1 - Mutual recursion:
mutualA()andmutualB()call each other alternately with decreasing counter
- Function templates:
maxOf<int>(3,7)andmaxOf<double>(3.14,2.72)— different template instantiations produce distinct call targets - Variadic function template:
varargSink(1,2,3,4,5)— recursive template instantiation over parameter pack - CRTP (Curiously Recurring Template Pattern):
ShapeBase<Derived>::draw()callsstatic_cast<Derived*>(this)->drawImpl(), resolved at compile time toCircle::drawImpl()orSquare::drawImpl()
- Default arguments:
defaultArgsFunc(1),defaultArgsFunc(2,20),defaultArgsFunc(3,30,"explicit")— calls with different argument counts expand differently at IR level - Operator overloading: Built-in
operator+called onintoperands to verify operator call edges
- std::mem_fn:
std::mem_fn(&Calculator::add)wraps a member function pointer into a callable, invoked via_Mem_fn::operator()with__invoke_memfun_ref/__invoke_memfun_derefpaths - std::bind with member function:
std::bind(&Calculator::add, &calc, _1, _2)— binds a member function pointer with an instance pointer and placeholders, creating a complex nested call chain through_Bind::__call→__invoke_memfun_deref - Delegate pattern:
Buttonclass storesstd::function<void()>/std::function<void(int)>as members. Callbacks (delegateClickA/B,delegateKeyHandler) are dynamically registered withsetOnClick/setOnKeyand dispatched throughoperator()— includes runtime reassignment - std::map dispatch table:
std::map<int, FuncPtr>storingmapCmdStart/mapCmdStop/mapCmdStatus, looked up viafind()and invoked throughit->second() - if constexpr dispatch (C++17):
algoDispatch<true>()vsalgoDispatch<false>()— compile-time branch elimination viaif constexpr, only the selected path survives in IR - Fold expression call (C++17):
(foldTarget(args), ...)expands a parameter pack over the comma operator into sequential calls;(fns(), ...)calls a pack of function pointers - std::apply (C++17):
std::apply(applyTarget, tuple)unpacks a tuple into function arguments; also tested with a lambda - Overloaded lambda pattern (C++17):
Overloaded{[](int){}, [](const char*){}}uses variadicusing Ts::operator()...and CTAD deduction guide to create a multi-overload callable; dispatched through the synthesizedoperator() - std::variant + std::visit:
VariantType = std::variant<int, double, const char*>visited in a loop with anOverloadedvisitor, dispatching tovariantIntHandler/variantDoubleHandler/variantStrHandlerper alternative - Recursive lambda via std::function: Lambda that captures itself by reference (
[&fib]) in astd::function<int(int)>and callsfib(n-1) + fib(n-2)recursively to compute Fibonacci, relying on type-erased self-reference
- SVF framework at
/home/test/src/SVF(or-DSVF_ROOT=<path>) - LLVM 21 toolchain at
/usr/lib/llvm-21(or-DLLVM_PREFIX=<path>) - Z3 solver
CMake, out-of-source into svf-api-demo/build/:
cmake -S svf-api-demo -B svf-api-demo/build
cmake --build svf-api-demo/buildThis produces:
svf-api-demo/build/clang_analyzer— analyzer binarysvf-api-demo/build/test_complex— native test program executablesvf-api-demo/build/tests/test_complex.bc,test_prog.bc— test bitcode (generated by thebitcodetarget with the LLVM-21 clang; the system clang 18 cannot produce/read bitcode compatible with the LLVM-21 SVF build)
SVF_ROOT (default /home/test/src/SVF) and LLVM_PREFIX (default
/usr/lib/llvm-21) can be overridden at configure time:
cmake -S svf-api-demo -B svf-api-demo/build -DSVF_ROOT=/path/to/SVFThe analyzer supports multiple analysis modes via CLI flags. The default
is tuned for the most complete + precise call graph: flow-sensitive
PTA + context-sensitive SVFG (heap-object model off). Enabling the heap
model resolves indirect calls through local fn-ptr arrays (e.g.
void (*fns[2])() = {regA, regB}) that the default misses, but it also
measurably under-resolves virtual dispatch through base-class arrays and
struct dispatch tables — a net completeness loss on the test suite — so it
is opt-in.
The output call graph excludes library code by default to keep the tree focused on user code:
- C++ standard library / runtime functions by name:
std::,__gnu_cxx::,__cxa_*exception ABI,operator new/delete; - any function defined in a system include header (libstdc++/glibc
templates under
/usr/include, etc.) — judged by its debug-info file path, which catches libstdc++ functions whose demangled names carry a return-type prefix (void std::...,decltype(...) std::...) and would slip past a name-only filter; - library/runtime functions with no debug info (
printf, LLVM intrinsics,__dynamic_cast) — when the module does carry debug info elsewhere.
User callbacks invoked from inside library functions (comparators, lambdas,
functors, std::function targets) are still shown, grafted onto the caller
that reached them. Pass --include-stdlib to keep everything in the output.
| Flag | Description |
|---|---|
| (default) | FlowSensitive + context-sensitive SVFG, library code excluded — most complete & precise |
--fs |
FlowSensitive (FSSPARSE_WPA) — sparse flow-sensitive analysis [default on] |
--vfs |
VersionedFlowSensitive (VFS_WPA) — versioned flow-sensitive |
--ander |
Downgrade to flow-insensitive AndersenWaveDiff |
--cs |
Context-sensitive SVFG (SVF default; flag is descriptive) [default on] |
--heap-model |
Heap object model (ModelConsts, ModelArrays) — opt-in |
--include-stdlib |
Keep C++ std / system-header / no-debug-info library functions in the call graph [default: excluded] |
# Default: flow-sensitive + context-sensitive (most complete & precise)
LD_LIBRARY_PATH=/home/test/src/SVF/Release-build/lib:/usr/lib/llvm-21/lib \
./svf-api-demo/build/clang_analyzer svf-api-demo/build/tests/test_complex.bc
# Flow-insensitive Andersen (faster, less precise)
LD_LIBRARY_PATH=/home/test/src/SVF/Release-build/lib:/usr/lib/llvm-21/lib \
./svf-api-demo/build/clang_analyzer --ander svf-api-demo/build/tests/test_complex.bc
# Also resolve indirect calls through local fn-ptr arrays (may lose
# virtual/struct dispatch targets)
LD_LIBRARY_PATH=/home/test/src/SVF/Release-build/lib:/usr/lib/llvm-21/lib \
./svf-api-demo/build/clang_analyzer --heap-model svf-api-demo/build/tests/test_complex.bc
# Versioned flow-sensitive (object versioning)
LD_LIBRARY_PATH=/home/test/src/SVF/Release-build/lib:/usr/lib/llvm-21/lib \
./svf-api-demo/build/clang_analyzer --vfs svf-api-demo/build/tests/test_complex.bcConvenience target — build and run the analyzer on the test-complex bitcode
with the default config. The bundled runtime libs in build/lib/ are picked
up via the $ORIGIN rpath, so no LD_LIBRARY_PATH is needed:
cmake --build svf-api-demo/build --target analyzeclang_analyzer links against shared SVF/LLVM/z3 libraries and carries an
$ORIGIN-relative rpath, so the runtime libraries travel with the binary.
Build a self-contained deployment folder:
cmake --build svf-api-demo/build --target deployThis assembles svf-api-demo/build/deploy/:
deploy/
├── clang_analyzer
└── lib/
├── libSvfCore.so.3
├── libSvfLLVM.so.3
├── libLLVM.so.21.1
├── libz3.so.4
└── extapi.bc # SVF's external-API model (located beside libSvfCore.so)
Copy that folder to any x86-64 Linux machine and run — no installs, no
LD_LIBRARY_PATH:
./deploy/clang_analyzer my_program.bcThe target machine only needs the usual base system libraries (glibc,
libstdc++, zlib, zstd, ICU …), which are present on essentially every Linux
distro. Fully static linking is not possible without rebuilding SVF itself
as a static library (it is built shared-only); the $ORIGIN bundle is the
pragmatic equivalent.
./svf-api-demo/build/test_complexThe report (default <bitcode-stem>.callgraph.tsv) is line-oriented so it can be
streamed both when writing and when parsing — nothing is materialized in
memory. #-prefixed lines are metadata/markers; every other line is one span,
tab-separated, in pre-order (a span's parent always appears before it):
#schema_version 1
#bitcode test_prog.bc
#analysis pta=FlowSensitive context_sensitive=1 heap_model=0 exclude_std=1
#entries main
#entry main
0 root 0 0 main main test_prog.c 3 6
1 0 direct 1 0 foo() _Z3foov test_prog.c 8 10 4
2 1 indirect 2 0 bar() _Z3barv test_prog.c 12 14 9
3 1 direct 2 1 foo() _Z3foov test_prog.c 8 10 13
#end main spans=4 reachable_funcs=3
Span-line columns: span_id, parent_span_id, kind (root/direct/indirect),
depth, cycle (0/1), function (demangled, params stripped), mangled,
file, start_line, end_line, call_line. Empty fields mean "absent" (the
root has no parent; functions without debug info have no line columns). A cycle
span is a recursion back-edge whose subtree is not expanded. #end carries the
entry's span count and the number of unique reachable functions.
===== Call Graph Topology =====
main
├── directCall()
│ └── printf
├── testFuncPtr()
│ └── [Indirect]
│ └── calleeViaPtr()
│ └── printf
├── testOverloaded()
│ ├── overloaded(int)
│ │ └── printf
│ └── overloaded(double)
│ └── printf
├── testVirtual()
│ ├── operator new(unsigned long)
│ ├── Derived::Derived()
│ │ └── Base::Base()
│ ├── [Indirect]
│ │ └── Derived::virtualMethod()
│ │ └── printf
│ └── [Indirect]
│ └── Derived::~Derived()
│ ├── Derived::~Derived()
│ │ └── Base::~Base()
│ └── operator delete(void*, unsigned long)
├── testLambda()
│ └── testLambda()::$_0::operator()() const
│ └── printf
├── testCallbackParam()
│ └── sortWithCallback(int*, int, int (*)(int, int))
│ ├── [Indirect] → compareAsc(int, int)
│ └── [Indirect] → compareDesc(int, int)
├── testPtrToFuncPtr()
│ └── [Indirect] → targetViaDoublePtr() → printf
├── testStructFuncPtr()
│ └── [Indirect] → handleEventA(int) → printf
├── testReturnFuncPtr()
│ └── chooseHandler(int)
│ └── [Indirect] → fa() → printf
└── testNestedCallback()
└── middleWare(int, int, int (*)(int, int), void (*)(int))
├── [Indirect] → compareAsc(int, int)
├── [Indirect] → compareDesc(int, int)
└── [Indirect] → reportResult(int) → printf
Indirect calls (function pointers, virtual dispatch) are marked [Indirect]. The tree uses ├──/└── connectors with │ vertical lines for hierarchy. If a call graph cycle is detected, [cycle] is printed to prevent infinite recursion.