Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions examples/how-to/09-export-callsite-cg/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
cmake_minimum_required(VERSION 3.14...3.28)

project(export-callsite-cg)

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

find_package(phasar REQUIRED CONFIG)

add_executable(export-callsite-cg-streaming export_callsite_cg_streaming.cpp)
target_link_libraries(export-callsite-cg-streaming PRIVATE phasar::phasar)
40 changes: 40 additions & 0 deletions examples/how-to/09-export-callsite-cg/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Export a Callsite-Level Call Graph

Exports PhASAR's call graph as callsite-resolved, source-located CSV: one row per
resolved edge, with caller/callee function name plus file/line/column for each
side, written incrementally as edges are discovered.

This differs from `CallGraph::printAsDot()` / `printAsJson()` in two ways:

- **Callsite-level.** Each row is a single call instruction resolved to its callee(s), rather than an aggregated function-to-function edge.
This is what, e.g., correlating a static call graph against runtime instrumentation data needs.
The runtime side reports individual call sites, not just "A calls B somewhere".
- **Streaming output.** `exportICFGAsJson()` and `printAsJson()` build one in-memory `nlohmann::json` object holding every edge before writing anything to disk.
On a large enough input (validated against a ~1.2M-edge FFmpeg call graph) this easily goes out-of-memory.
This driver writes each edge to disk immediately and discards it, so memory stays roughly constant regardless of total edge count.

It also documents a (current) source-location bug it works around: PhASAR's `getSrcCodeInfoFromIR` resolves `File` and `Line` through separate helper calls,
each with its own fallback logic for instructions lacking direct `!dbg` metadata, which can pair a `File` from one resolution path with a `Line` from an unrelated one.

## Build

```bash
# Invoked from the 09-export-callsite-cg root folder:
$ mkdir -p build && cd build
$ cmake ..
$ cmake --build .
```

## Usage

```bash
./export-callsite-cg-streaming <bitcode.bc> <entry-point> [cha|rta|vta|otf] <out.csv>
```

Output is CSV with one row per resolved call edge:

```
caller_function,caller_file,caller_line,caller_column,callee_function,callee_file,callee_line,callee_column,caller_loc_approximate
```

`caller_loc_approximate` is `true` when the call instruction had no direct debug location and the row fell back to the enclosing function's declaration site instead of the real call site.
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
#include "phasar/ControlFlow/CallGraphAnalysisType.h"
#include "phasar/PhasarLLVM/ControlFlow/LLVMBasedICFG.h"
#include "phasar/PhasarLLVM/DB/LLVMProjectIRDB.h"
#include "phasar/PhasarLLVM/HelperAnalyses.h"
#include "phasar/PhasarLLVM/HelperAnalysisConfig.h"
#include "phasar/PhasarLLVM/Utils/LLVMIRToSrc.h"

#include "llvm/ADT/StringRef.h"
#include "llvm/IR/DebugInfoMetadata.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/Instructions.h"
#include "llvm/Support/WithColor.h"
#include "llvm/Support/raw_ostream.h"

#include <system_error>

namespace {
struct ResolvedLoc {
uint32_t Line{};
uint32_t Column{};
llvm::StringRef FileName{};
bool Approximate{}; // true = fell back to enclosing function's declaration
// site, not the real call site

friend llvm::raw_ostream &operator<<(llvm::raw_ostream &OS,
const ResolvedLoc &Loc) {
OS.write_escaped(Loc.FileName) << ',';
return OS << Loc.Line << ',' << Loc.Column;
}
};

constexpr llvm::StringLiteral NoDebugInfo = "<no-debug-info>";

ResolvedLoc resolveLocation(const llvm::Instruction *I) {
if (auto Loc = psr::getDebugLocation(I)) {
return {
.Line = Loc->Line,
.Column = Loc->Column,
.FileName = Loc->File->getFilename(),
.Approximate = false,
};
}

// No direct location on this instruction -- fall back to the
// enclosing function's declared file
if (const auto *SP = I->getFunction()->getSubprogram()) {
return {
.Line = SP->getLine(),
.Column = 0,
.FileName = SP->getFile()->getFilename(),
.Approximate = true,
};
}
return {.FileName = NoDebugInfo, .Approximate = true};
}

ResolvedLoc resolveLocation(const llvm::Function *F) {
if (const auto *SP = F->getSubprogram()) {
return {
.Line = SP->getLine(),
.Column = 0,
.FileName = SP->getFile()->getFilename(),
.Approximate = false,
};
}
return {.FileName = NoDebugInfo, .Approximate = true};
}
} // namespace

int main(int Argc, char **Argv) {
if (Argc < 5) {
llvm::errs() << "USAGE: " << Argv[0]
<< " <bitcode.bc> <entry-point> [cha|rta|vta|otf] <out.csv>\n";
return 1;
}

llvm::StringRef IRFile = Argv[1];
std::vector<std::string> EntryPoints = {Argv[2]};
auto CGTy = psr::toCallGraphAnalysisType(Argv[3]);
llvm::StringRef OutFile = Argv[4];

if (CGTy == psr::CallGraphAnalysisType::Invalid) {
llvm::WithColor::error() << "Invalid call-graph type '" << Argv[3] << "'\n";
return 1;
}

std::error_code EC;
llvm::raw_fd_ostream OS(OutFile, EC);
if (EC) {
llvm::WithColor::error()
<< "While opening output file '" << OutFile << "':\n";
llvm::WithColor::error() << EC.message() << '\n';
return 1;
}

// CSV header:
OS << "caller_function,caller_file,caller_line,caller_column,"
"callee_function,callee_file,callee_line,callee_column,"
"caller_loc_approximate\n";
OS.flush();

// Basic phasar pipeline:
psr::HelperAnalyses HA{
std::make_unique<psr::LLVMProjectIRDB>(
psr::LLVMProjectIRDB::loadOrExit(IRFile)),
std::move(EntryPoints),
psr::HelperAnalysisConfig{.CGTy = CGTy},
};

auto &ICF = HA.getICFG();

size_t EdgeCount = 0;
size_t FnCount = 0;

// Walk every function PhASAR's call-graph knows about, and ask it for all
// known callers, writing (and forgetting) each result immediately instead of
// accumulating all of them.
for (const llvm::Function *Fun : ICF.getAllVertexFunctions()) {
auto FunName = Fun->getName();
++FnCount;

ResolvedLoc CalleeLoc = resolveLocation(Fun);
for (const auto *CS : ICF.getCallersOf(Fun)) {
ResolvedLoc CallerLoc = resolveLocation(CS);

OS.write_escaped(CS->getFunction()->getName()) << ',' << CallerLoc << ',';
OS.write_escaped(FunName) << ',' << CalleeLoc << ',';
OS << (CallerLoc.Approximate ? "true" : "false") << '\n';

++EdgeCount;
if (EdgeCount % 50000 == 0) {
OS.flush(); // periodic flush -- makes progress visible on
// disk instead of buffered invisibly
llvm::WithColor::note()
<< "[export_callsite_cg_streaming] " << EdgeCount
<< " edges written, " << FnCount << '/'
<< ICF.getNumVertexFunctions() << " functions processed so far\n";
}
}
}

OS.close();

llvm::WithColor::note() << "[export_callsite_cg_streaming] DONE: "
<< EdgeCount << " total edges across " << FnCount
<< " functions -> "
<< (OutFile == "-" ? llvm::StringRef("stdout")
: OutFile)
<< "\n";
return 0;
}
1 change: 1 addition & 0 deletions examples/how-to/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ Currently supporting:
- [x] Run an IDE analysis ([here](./05-run-ide-analysis/README.md))
- [x] Write an IFDS analysis ([here](./07-write-ifds-analysis/README.md))
- [x] Write an IDE analysis ([here](./08-write-ide-analysis/))
- [x] Export a callsite-level call graph ([here](./09-export-callsite-cg/README.md))
Loading