diff --git a/examples/how-to/09-export-callsite-cg/CMakeLists.txt b/examples/how-to/09-export-callsite-cg/CMakeLists.txt new file mode 100644 index 0000000000..2a55249f35 --- /dev/null +++ b/examples/how-to/09-export-callsite-cg/CMakeLists.txt @@ -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) diff --git a/examples/how-to/09-export-callsite-cg/README.md b/examples/how-to/09-export-callsite-cg/README.md new file mode 100644 index 0000000000..1eafec8047 --- /dev/null +++ b/examples/how-to/09-export-callsite-cg/README.md @@ -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 [cha|rta|vta|otf] +``` + +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. diff --git a/examples/how-to/09-export-callsite-cg/export_callsite_cg_streaming.cpp b/examples/how-to/09-export-callsite-cg/export_callsite_cg_streaming.cpp new file mode 100644 index 0000000000..0fd7955cee --- /dev/null +++ b/examples/how-to/09-export-callsite-cg/export_callsite_cg_streaming.cpp @@ -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 + +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 = ""; + +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] + << " [cha|rta|vta|otf] \n"; + return 1; + } + + llvm::StringRef IRFile = Argv[1]; + std::vector 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::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; +} diff --git a/examples/how-to/README.md b/examples/how-to/README.md index b84875a609..10c601fc45 100644 --- a/examples/how-to/README.md +++ b/examples/how-to/README.md @@ -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))