From b272da3de0dd2f52018ece36350bdccd4873c2f9 Mon Sep 17 00:00:00 2001 From: Sebastian Kreutzer Date: Wed, 22 Jul 2026 15:25:58 +0200 Subject: [PATCH 1/2] [CaGe] Add embedding plugin --- .../cage/generator/CallGraphEmbedder.h | 32 ++++++++++++ tools/cage/src/CaGe.cpp | 6 +++ tools/cage/src/generator/CMakeLists.txt | 7 ++- .../cage/src/generator/CallGraphEmbedder.cpp | 52 +++++++++++++++++++ tools/cage/test/runCaGeTests.sh.in | 17 ++++++ utils/CMakeLists.txt | 2 + utils/extract_metacg | 2 + 7 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 tools/cage/include/cage/generator/CallGraphEmbedder.h create mode 100644 tools/cage/src/generator/CallGraphEmbedder.cpp create mode 100755 utils/extract_metacg diff --git a/tools/cage/include/cage/generator/CallGraphEmbedder.h b/tools/cage/include/cage/generator/CallGraphEmbedder.h new file mode 100644 index 00000000..fb56ba66 --- /dev/null +++ b/tools/cage/include/cage/generator/CallGraphEmbedder.h @@ -0,0 +1,32 @@ +/** + * File: CallGraphEmbedder.h + * License: Part of the MetaCG project. Licensed under BSD 3 clause license. See LICENSE.txt file at + * https://github.com/tudasc/metacg/LICENSE.txt + */ +#ifndef METACG_CALLGRAPHEMBEDDER_H +#define METACG_CALLGRAPHEMBEDDER_H + +#include "cage/interface/CaGePlugin.h" + +#include + +namespace llvm { +class Module; +} + +namespace cage { + +class GraphEmbedder : public Plugin { + public: + explicit GraphEmbedder(llvm::Module& M) : GraphEmbedder(M, "metacg") {} + GraphEmbedder(llvm::Module& M, const std::string& sectionName) : M(M), sectionName(sectionName) {} + void consumeCallGraph(const metacg::Callgraph&) override; + + private: + llvm::Module& M; + std::string sectionName; +}; + +} // namespace cage + +#endif // METACG_CALLGRAPHEMBEDDER_H diff --git a/tools/cage/src/CaGe.cpp b/tools/cage/src/CaGe.cpp index a8011e55..6cb6c18d 100644 --- a/tools/cage/src/CaGe.cpp +++ b/tools/cage/src/CaGe.cpp @@ -6,6 +6,7 @@ #include "cage/CaGe.h" #include "cage/interface/CaGePlugin.h" +#include "cage/generator/CallGraphEmbedder.h" #include "cage/generator/CallgraphGenerator.h" #include "cage/generator/FileExporter.h" @@ -27,6 +28,8 @@ static opt pta( static opt cgout("cg-file", desc("Output file for the generated call graph"), cat(cageOpts), init("")); +static opt embed("embed", desc("Embed output graph into binary"), cat(cageOpts), init(true)); + static list pluginPaths("plugin-paths", desc("option list"), cat(cageOpts), CommaSeparated); namespace cage { @@ -72,6 +75,9 @@ PreservedAnalyses CaGe::run(Module& M, ModuleAnalysisManager& MA) { Generator gen(pta); gen.addPlugin(std::make_unique(outfile)); + if (embed) { + gen.addPlugin(std::make_unique(M)); + } // Load external plugins for (const auto& pluginPath : pluginPaths) { diff --git a/tools/cage/src/generator/CMakeLists.txt b/tools/cage/src/generator/CMakeLists.txt index 72fdae68..403f2b43 100644 --- a/tools/cage/src/generator/CMakeLists.txt +++ b/tools/cage/src/generator/CMakeLists.txt @@ -1,4 +1,9 @@ -add_library(cage STATIC CallGraphGenerator.cpp FileExporter.cpp) +add_library( + cage STATIC + CallGraphEmbedder.cpp + CallGraphGenerator.cpp + FileExporter.cpp +) set_target_properties(cage PROPERTIES POSITION_INDEPENDENT_CODE ON) target_include_directories(cage PRIVATE $) target_include_directories(cage PUBLIC $) diff --git a/tools/cage/src/generator/CallGraphEmbedder.cpp b/tools/cage/src/generator/CallGraphEmbedder.cpp new file mode 100644 index 00000000..aacf4bfa --- /dev/null +++ b/tools/cage/src/generator/CallGraphEmbedder.cpp @@ -0,0 +1,52 @@ +/** + * File: CallGraphEmbedder.cpp + * License: Part of the MetaCG project. Licensed under BSD 3 clause license. See LICENSE.txt file at + * https://github.com/tudasc/metacg/LICENSE.txt + */ + +#include "cage/generator/CallGraphEmbedder.h" + +#include "llvm/IR/Constants.h" +#include "llvm/IR/GlobalValue.h" +#include "llvm/IR/LLVMContext.h" +#include "llvm/IR/Module.h" +#include "llvm/IR/Type.h" +#include "llvm/Support/raw_ostream.h" +#include "llvm/Transforms/Utils/ModuleUtils.h" + +#include "metacg/config.h" +#include "metacg/io/MCGWriter.h" +#include "metacg/io/VersionFourMCGWriter.h" + +namespace cage { + +using namespace llvm; + +static void embed(Module& M, const std::string& sectionName, const std::string& cgStr) { + LLVMContext& C = M.getContext(); + + auto* CgStrData = ConstantDataArray::getString(C, cgStr); + + auto* GV = new GlobalVariable(M, CgStrData->getType(), true, GlobalValue::PrivateLinkage, CgStrData, "__cage_cg"); + + GV->setSection(sectionName); + GV->setAlignment(Align(1)); + + appendToCompilerUsed(M, GV); +} + +void GraphEmbedder::consumeCallGraph(const metacg::Callgraph& graph) { + metacg::io::JsonSink jsSink; + metacg::io::VersionFourMCGWriter mcgw({{4, 0}, {"CaGe", 0, 1, MetaCG_GIT_SHA}}, true, true); + mcgw.write(&graph, jsSink); + + llvm::outs() << "Embedding generated call graph into ELF section " << sectionName << "\n"; + + std::stringstream ss; + + ss << jsSink.getJson().dump(); + ss.flush(); + embed(M, sectionName, ss.str()); +} + +} // namespace cage diff --git a/tools/cage/test/runCaGeTests.sh.in b/tools/cage/test/runCaGeTests.sh.in index 7744545f..506d07b4 100755 --- a/tools/cage/test/runCaGeTests.sh.in +++ b/tools/cage/test/runCaGeTests.sh.in @@ -5,12 +5,24 @@ mcgconfig="@METACG_CONFIG@" build_dir="@CMAKE_BINARY_DIR@" cgdiff="$build_dir/tools/cgdiff/cgdiff" cgmerge="$build_dir/tools/cgmerge2/cgmerge2" +extract_mcg="@CMAKE_SOURCE_DIR@/utils/extract_metacg" suite_pattern=".*" verbose=0 mkdir -p log + +# Check whether embedded CG matches the exported file. +# Param 1: Binary with embedded CG +# Param 2: GT file +function checkEmbedded { + if [[ $verbose -eq 1 ]]; then + echo "Checking embedded graph using $extract_mcg" + fi + "$extract_mcg" "$1" | cmp -s <(jq -S . /dev/stdin) <(jq -S . "$2") +} + # Run single source test case. # Param 1: Name of the test case. # Param 2: Path to the test case. @@ -75,6 +87,11 @@ function runTestCase { echo "Linking failed but call graph was generated." fi + if ! checkEmbedded "$tfile_out" "$cg_outfile"; then + echo "Embedded call graph does not match the exported version!" + return 1 + fi + if [[ $regenerate_gt -eq 1 ]]; then echo "Using $cg_outfile as new ground truth" cp "$cg_outfile" "$cg_gtfile" diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index 9040988f..d8763bc2 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -1 +1,3 @@ add_subdirectory(config) + +install(PROGRAMS extract_metacg DESTINATION bin) diff --git a/utils/extract_metacg b/utils/extract_metacg new file mode 100755 index 00000000..271fa525 --- /dev/null +++ b/utils/extract_metacg @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +objcopy -O binary --only-section=metacg "$1" /dev/stdout | strings -n 1 From f21fd48d20047f1f6ec62662c2ba534be5e6da39 Mon Sep 17 00:00:00 2001 From: Sebastian Kreutzer Date: Fri, 24 Jul 2026 13:42:54 +0200 Subject: [PATCH 2/2] fixup! [CaGe] Add embedding plugin --- tools/cage/src/CaGe.cpp | 25 +++++++++++++++++++------ tools/cage/test/runCaGeTests.sh.in | 4 ++-- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/tools/cage/src/CaGe.cpp b/tools/cage/src/CaGe.cpp index 6cb6c18d..aaf8b177 100644 --- a/tools/cage/src/CaGe.cpp +++ b/tools/cage/src/CaGe.cpp @@ -26,9 +26,14 @@ static opt pta( "Treat all available valid function signatures for a given function pointer as potential call target ")), cat(cageOpts), init(cage::PTAType::No)); -static opt cgout("cg-file", desc("Output file for the generated call graph"), cat(cageOpts), init("")); +enum class OutputTypes { File, Embed }; +static bits outputTypeBits(desc("Where to output the graph"), + values(clEnumValN(OutputTypes::File, "file", " Output into separate file"), + clEnumValN(OutputTypes::Embed, "embed", + "Embed the graph into the metacg section of the binary")), + cat(cageOpts)); -static opt embed("embed", desc("Embed output graph into binary"), cat(cageOpts), init(true)); +static opt cgout("cg-file", desc("Output file for the generated call graph"), cat(cageOpts), init("")); static list pluginPaths("plugin-paths", desc("option list"), cat(cageOpts), CommaSeparated); @@ -61,9 +66,15 @@ PreservedAnalyses CaGe::run(Module& M, ModuleAnalysisManager& MA) { outs() << "Running CaGe in verbose mode\n"; } - // First check explicit option + // Write to file if: + // (1) Output file name is explicitly given + // (2) Option for file output is set, or + // (3) No alternative output option is specified. + bool writeToFile = !cgout.empty() || outputTypeBits.isSet(OutputTypes::File) || outputTypeBits.getBits() == 0; + + // To determine output file, first check explicit option std::string outfile = cgout.getValue(); - if (outfile.empty()) { + if (writeToFile && outfile.empty()) { // If empty, check environment variable if (const auto* cgNameEnv = std::getenv("CAGE_CG")) { outfile = cgNameEnv; @@ -74,8 +85,10 @@ PreservedAnalyses CaGe::run(Module& M, ModuleAnalysisManager& MA) { } Generator gen(pta); - gen.addPlugin(std::make_unique(outfile)); - if (embed) { + if (writeToFile) { + gen.addPlugin(std::make_unique(outfile)); + } + if (outputTypeBits.isSet(OutputTypes::Embed)) { gen.addPlugin(std::make_unique(M)); } diff --git a/tools/cage/test/runCaGeTests.sh.in b/tools/cage/test/runCaGeTests.sh.in index 506d07b4..39a2a277 100755 --- a/tools/cage/test/runCaGeTests.sh.in +++ b/tools/cage/test/runCaGeTests.sh.in @@ -75,9 +75,9 @@ function runTestCase { failed=0 tfile_out="$run_dir/$testCaseName.exe" rm -f "$tfile_out" - clang++ -g `$mcgconfig --cage-ldflags --cage-pass-option "-cg-file=${cg_outfile}" ${addOptions}` ${addFlags} -fno-exceptions ${o_files[@]} -o "$tfile_out" >> log/testrun.log 2>&1 || failed=1 + clang++ -g `$mcgconfig --cage-ldflags --cage-pass-option -embed --cage-pass-option "-cg-file=${cg_outfile}" ${addOptions}` ${addFlags} -fno-exceptions ${o_files[@]} -o "$tfile_out" >> log/testrun.log 2>&1 || failed=1 if [[ $failed -ne 0 ]] || [[ $verbose -eq 1 ]]; then - echo "Link command: clang++ -g `$mcgconfig --cage-ldflags --cage-pass-option "-cg-file=${cg_outfile}" ${addOptions}` ${addFlags} -fno-exceptions ${o_files[@]} -o $tfile_out" + echo "Link command: clang++ -g `$mcgconfig --cage-ldflags --cage-pass-option -embed --cage-pass-option "-cg-file=${cg_outfile}" ${addOptions}` ${addFlags} -fno-exceptions ${o_files[@]} -o $tfile_out" fi if [[ $failed -ne 0 ]]; then if [[ ! -f "$cg_outfile" ]]; then