// // This file is distributed under the MIT License. See LICENSE.md for details. // #include #include #include #include "llvm/ADT/DenseSet.h" #include "llvm/ADT/PostOrderIterator.h" #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/SetOperations.h" #include "llvm/ADT/SmallSet.h" #include "llvm/IR/GlobalVariable.h" #include "llvm/IR/Instruction.h" #include "llvm/IR/Module.h" #include "llvm/Support/GraphWriter.h" #include "revng/ABI/Definition.h" #include "revng/ABI/FunctionType/Layout.h" #include "revng/ADT/Queue.h" #include "revng/BasicAnalyses/GeneratedCodeBasicInfo.h" #include "revng/EarlyFunctionAnalysis/CFGAnalyzer.h" #include "revng/EarlyFunctionAnalysis/CallEdge.h" #include "revng/EarlyFunctionAnalysis/CallGraph.h" #include "revng/EarlyFunctionAnalysis/CollectFunctionsFromCalleesPass.h" #include "revng/EarlyFunctionAnalysis/CollectFunctionsFromUnusedAddressesPass.h" #include "revng/EarlyFunctionAnalysis/ControlFlowGraph.h" #include "revng/EarlyFunctionAnalysis/ControlFlowGraphCache.h" #include "revng/EarlyFunctionAnalysis/DetectABI.h" #include "revng/EarlyFunctionAnalysis/FunctionEdgeBase.h" #include "revng/EarlyFunctionAnalysis/FunctionSummaryOracle.h" #include "revng/InlineHelpers/InlineHelpers.h" #include "revng/InlineHelpers/LinkHelpersToInline.h" #include "revng/Model/Binary.h" #include "revng/Model/IRHelpers.h" #include "revng/Model/Pass/DeduplicateCollidingNames.h" #include "revng/Model/Register.h" #include "revng/Pipeline/Pipe.h" #include "revng/Pipeline/RegisterAnalysis.h" #include "revng/Pipes/Kinds.h" #include "revng/Pipes/ModelGlobal.h" #include "revng/Support/BasicBlockID.h" #include "revng/Support/CommonOptions.h" #include "revng/Support/Debug.h" #include "revng/Support/IRBuilder.h" #include "revng/Support/IRHelpers.h" #include "revng/Support/MetaAddress.h" #include "revng/Support/OpaqueRegisterUser.h" using namespace llvm; using namespace llvm::cl; static opt CallGraphOutputPath("cg-output", desc("Dump to disk the recovered " "call graph."), value_desc("filename")); enum ABIEnforcementOption { NoABIEnforcement = 0, SoftABIEnforcement, FullABIEnforcement, }; using ABIOpt = ABIEnforcementOption; static opt ABIEnforcement("abi-enforcement-level", desc("ABI refinement preferences."), values(clEnumValN(NoABIEnforcement, "no", "Do not enforce " "ABI-specific " "information."), clEnumValN(SoftABIEnforcement, "soft", "Enforce ABI-specific " "information, but allows " "for incompatibility, if " "unsure about the ABI."), clEnumValN(FullABIEnforcement, "full", "Enforce ABI-specific " "information, and do not " "allow for any possible " "violations of the ABI " "found.")), init(ABIOpt::FullABIEnforcement)); static opt DumpPostInline("detect-abi-dump-post-helpers-inlining", desc("Path of a file the whole " "module is written to right " "after helper inlining inside " "`analyzeABI` (for debugging " "and testing)"), init("")); static Logger Log("detect-abi"); struct Changes { bool Function = false; std::set Callees; }; class DetectABIAnalysis { public: static constexpr auto Name = "detect-abi"; std::vector> AcceptedKinds = { { &revng::kinds::Root } }; void run(const pipeline::ExecutionContext &EC, pipeline::LLVMContainer &ModuleContainer) { revng::pipes::CFGMap CFGs(""); llvm::legacy::PassManager Manager; using namespace revng; auto Global = cantFail(EC.getContext() .getGlobal(ModelGlobalName)); Manager.add(new LoadModelWrapperPass(ModelWrapper(Global->get()))); Manager.add(new CollectFunctionsFromCalleesWrapperPass()); Manager.add(new ControlFlowGraphCachePass(CFGs)); Manager.add(new LinkHelpersToInlinePass()); Manager.add(new efa::DetectABIPass()); Manager.add(new CollectFunctionsFromUnusedAddressesWrapperPass()); Manager.add(new efa::DetectABIPass()); Manager.run(ModuleContainer.getModule()); } }; static pipeline::RegisterAnalysis A1; namespace efa { static model::Architecture::Values getCodeArchitecture(const MetaAddress &MA) { const auto Architecture = MetaAddressType::arch(MA.type()); revng_assert(Architecture != model::Architecture::Invalid, "The architecture is available for code addresses"); return Architecture; } static bool isWritingToMemory(llvm::Instruction &I) { if (auto *StoreInst = dyn_cast(&I)) { auto *Destination = StoreInst->getPointerOperand(); return isMemory(Destination); } return false; } using BasicBlockQueue = UniquedQueue; class DetectABI { private: using BasicBlockToNodeMap = llvm::DenseMap; private: llvm::Module &M; llvm::LLVMContext &Context; GeneratedCodeBasicInfo &GCBI; ControlFlowGraphCache &FMC; TupleTree &Binary; FunctionSummaryOracle &Oracle; CFGAnalyzer &Analyzer; CallGraph ApproximateCallGraph; BasicBlockToNodeMap BasicBlockNodeMap; public: DetectABI(llvm::Module &M, GeneratedCodeBasicInfo &GCBI, ControlFlowGraphCache &FMC, TupleTree &Binary, FunctionSummaryOracle &Oracle, CFGAnalyzer &Analyzer) : M(M), Context(M.getContext()), GCBI(GCBI), FMC(FMC), Binary(Binary), Oracle(Oracle), Analyzer(Analyzer) {} public: void run() { llvm::Task Task(6, "DetectABI"); Task.advance("computeApproximateCallGraph"); computeApproximateCallGraph(); // Perform a preliminary analysis of the function // // We're interested: // // 1. the function being noreturn or not; // 2. the list of callee-saved registers; // 3. the final stack offset; // 4. the CFG (specifically, which indirect jumps are returns); Task.advance("preliminaryFunctionAnalysis"); preliminaryFunctionAnalysis(); // Run the (fixed-point) analysis of the ABI of each function Task.advance("analyzeABI"); analyzeABI(); // Refine results with ABI-specific information Task.advance("applyABIDeductions"); applyABIDeductions(); // Commit the results onto the model. A non-const model is taken as // argument to be written. Task.advance("finalizeModel"); finalizeModel(); // Propagate prototypes Task.advance("propagatePrototypes"); propagatePrototypes(); } private: void computeApproximateCallGraph(); void preliminaryFunctionAnalysis(); void analyzeABI(); Changes analyzeFunctionABI(const model::Function &Function, OutlinedFunction &OutlinedFunction, OpaqueRegisterUser &Clobberer); void applyABIDeductions(); /// Finish the population of the model by building the prototype void finalizeModel(); // Calls propagatePrototypesInFunction for all functions in Binary void propagatePrototypes(); /// Propagate prototypes to callers void propagatePrototypesInFunction(model::Function &Function); private: void recordRegisters(const efa::CSVSet &CSVs, auto Inserter); CSVSet computePreservedCSVs(const CSVSet &ClobberedRegisters) const; TrackingSortedVector computePreservedRegisters(const CSVSet &ClobberedRegisters) const; Changes runAnalyses(MetaAddress EntryAddress, OutlinedFunction &OutlinedFunction); CSVSet findWrittenRegisters(llvm::Function *F); model::UpcastableType buildPrototypeForIndirectCall(const FunctionSummary &CallerSummary, const efa::BasicBlock &CallerBlock); bool getRegisterState(model::Register::Values, const CSVSet &); }; void DetectABI::computeApproximateCallGraph() { using llvm::BasicBlock; // Temporary worklist to collect the function entrypoints llvm::SmallVector Worklist; // Create an over-approximated call graph for (const auto &Function : Binary->Functions()) { auto *Entry = GCBI.getBlockAt(Function.Entry()); BasicBlockNode Node{ Function.Entry() }; BasicBlockNode *GraphNode = ApproximateCallGraph.addNode(Node); BasicBlockNodeMap[Entry] = GraphNode; } for (const auto &Function : Binary->Functions()) { llvm::SmallSet Visited; auto *Entry = GCBI.getBlockAt(Function.Entry()); revng_assert(Entry != nullptr); BasicBlockNode *StartNode = BasicBlockNodeMap[Entry]; revng_assert(StartNode != nullptr); Worklist.emplace_back(Entry); while (!Worklist.empty()) { BasicBlock *Current = Worklist.pop_back_val(); Visited.insert(Current); if (hasMarker(Current, "function_call")) { // If not an indirect call, add the node to the CG if (BasicBlock *Callee = getFunctionCallCallee(Current)) { BasicBlockNode *GraphNode = nullptr; auto It = BasicBlockNodeMap.find(Callee); if (It != BasicBlockNodeMap.end()) { auto IsCurrentSuccessor = [&It](auto &Successor) { return Successor == It->second; }; if (not any_of(StartNode->successors(), IsCurrentSuccessor)) StartNode->addSuccessor(It->second); } } BasicBlock *Next = getFallthrough(Current); revng_assert(Next != nullptr); if (!Visited.contains(Next)) Worklist.push_back(Next); } else { for (BasicBlock *Successor : successors(Current)) { if (not isPartOfRootDispatcher(Successor) && !Visited.contains(Successor)) { revng_assert(Successor != nullptr); Worklist.push_back(Successor); } } } } } // Create a root entry node for the call-graph, connect all the nodes to it, // and perform a post-order traversal. Keep in mind that adding a root node // as a predecessor to all nodes does not affect POT of any node, except the // root node itself. BasicBlockNode *RootNode = ApproximateCallGraph.addNode(MetaAddress()); ApproximateCallGraph.setEntryNode(RootNode); for (const auto &[_, Node] : BasicBlockNodeMap) RootNode->addSuccessor(Node); // Dump the call-graph, if requested if (CallGraphOutputPath.getNumOccurrences() == 1) { std::error_code EC; raw_fd_ostream OutputCG(CallGraphOutputPath, EC); revng_assert(!EC); llvm::WriteGraph(OutputCG, &ApproximateCallGraph); } } void DetectABI::preliminaryFunctionAnalysis() { revng_log(Log, "Running the preliminary function analysis"); LoggerIndent LodIndent(Log); BasicBlockQueue EntrypointsQueue; // // Populate queue of entry points // // Create an over-approximated call graph of the program. A queue of all the // function entrypoints is maintained. for (auto *Node : llvm::post_order(&ApproximateCallGraph)) { // Ignore entry node if (Node == ApproximateCallGraph.getEntryNode()) continue; // The intraprocedural analysis will be scheduled only for those functions // which have `Invalid` as type. auto &Function = Binary->Functions().at(Node->Address); EntrypointsQueue.insert(Node); } revng_assert(Binary->Functions().size() == EntrypointsQueue.size()); // // Process the queue // while (!EntrypointsQueue.empty()) { const BasicBlockNode *EntryNode = EntrypointsQueue.pop(); MetaAddress EntryPointAddress = EntryNode->Address; revng_log(Log, "Analyzing " << EntryPointAddress.toString()); LoggerIndent Indent(Log); FunctionSummary AnalysisResult = Analyzer.analyze(EntryNode->Address); if (Log.isEnabled()) { AnalysisResult.dump(Log); Log << DoLog; } // Serialize CFG in the ControlFlowGraphCache { TupleTree New; New->Entry() = EntryNode->Address; New->Blocks() = AnalysisResult.CFG; New->simplify(*Binary); FMC.set(std::move(New)); } // Perform some early sanity checks once the CFG is ready revng_assert(AnalysisResult.CFG.size() > 0); if (not Binary->Functions()[EntryPointAddress].Prototype().isEmpty()) { Oracle.getLocalFunction(EntryPointAddress) .CFG = std::move(AnalysisResult.CFG); continue; } bool Changed = Oracle.registerLocalFunction(EntryPointAddress, std::move(AnalysisResult)); revng_assert(Oracle.getLocalFunction(EntryPointAddress).CFG.size() > 0); // If we got improved results for a function, we need to recompute its // callers, and if a caller turns out to be an inline function, the // callers of the inline function too. if (Changed) { revng_log(Log, "Entry " << EntryPointAddress.toString() << " has changed"); LoggerIndent Indent(Log); OnceQueue InlineFunctionWorklist; InlineFunctionWorklist.insert(EntryNode); while (!InlineFunctionWorklist.empty()) { const BasicBlockNode *Node = InlineFunctionWorklist.pop(); MetaAddress NodeAddress = Node->Address; revng_log(Log, "Re-enqueuing callers of " << NodeAddress.toString() << ":"); LoggerIndent Indent(Log); for (auto *Caller : Node->predecessors()) { // Root node? if (Caller->Address.isInvalid()) continue; // If it's inline, re-enqueue its callers too MetaAddress CallerPC = Caller->Address; const auto &CallerSummary = Oracle.getLocalFunction(CallerPC); using namespace model::FunctionAttribute; if (CallerSummary.Attributes.contains(AlwaysInline)) InlineFunctionWorklist.insert(Caller); if (Binary->Functions().at(CallerPC).Prototype().isEmpty()) { revng_log(Log, CallerPC.toString()); EntrypointsQueue.insert(Caller); } } } } } // Register all indirect call sites in all functions, we'll need to fill in // their prototypes for (auto &Function : Binary->Functions()) { auto &CFG = Oracle.getLocalFunction(Function.Entry()).CFG; revng_log(Log, "Registering indirect call sites of " << Function.Entry().toString()); for (efa::BasicBlock &Block : CFG) { for (auto &Edge : Block.Successors()) { if (auto *Call = dyn_cast(Edge.get())) { if (Call->isIndirect()) { revng_log(Log, "Registering " << Function.Entry().toString() << " " << Block.ID().toString()); Oracle.registerCallSite(Function.Entry(), Block.ID(), FunctionSummary(), Call->IsTailCall()); } } } } } } void DetectABI::analyzeABI() { revng_log(Log, "Running ABI analyses"); LoggerIndent Indent(Log); llvm::Task Task(3, "analyzeABI"); std::map> Functions; // Create all temporary functions Task.advance("Create temporary functions"); for (model::Function &Function : Binary->Functions()) { const MetaAddress &Entry = Function.Entry(); auto NewFunction = std::make_unique(Analyzer .outline(Entry)); Functions[Function.Entry()] = std::move(NewFunction); } // Inline `revng_inline`-tagged helper calls so the subsequent ABI dataflow // analysis can observe the helpers' register reads and writes directly. // `InlineHelpersPass` only inlines into `Isolated` functions, so we tag the // outlined stubs as such. These stubs are temporary and are discarded with // the cloned module once the analysis ends, so the tag never escapes. // Helpers whose per-call critical arguments are not LLVM constants here // survive un-inlined and are consumed as opaque calls by the analysis. for (auto &[Entry, OutlinedFn] : Functions) FunctionTags::Isolated.addTo(OutlinedFn->Function.get()); Task.advance("Inline helpers"); { llvm::legacy::PassManager PM; PM.add(new InlineHelpersPass()); PM.run(M); } // When `--debug-names` is set, rename each outlined stub from the // metaaddress-based identifier to its source-symbol name, using the same // `llvmName` scheme as the rest of the pipeline. if (DebugNames) { for (auto &[Entry, OutlinedFn] : Functions) { llvm::Function *F = OutlinedFn->Function.get(); F->setName(llvmName(Binary->Functions().at(Entry))); } } // Dump the whole module right after helper inlining, for debugging and // testing. The outlined stubs live in the module at this point, so a single // module dump captures them all. if (not DumpPostInline.empty()) dumpModule(&M, DumpPostInline.c_str()); // Push this into analyzeFunction OpaqueRegisterUser RegisterUser(&M); // TODO: this really needs to become a monotone framework Task.advance("Run fixed-point analyses"); llvm::Task FixedPointTask({}, "Fixed-point analysis"); UniquedQueue ToAnalyze; for (model::Function &Function : Binary->Functions()) ToAnalyze.insert(&Function); // Change the oracle default prototype to have no arguments nor return values { auto NewDefault = Oracle.getDefault().clone(); NewDefault.ABIResults.ArgumentsRegisters = {}; NewDefault.ABIResults.ReturnValuesRegisters = {}; Oracle.setDefault(std::move(NewDefault)); } unsigned Runs = 0; while (not ToAnalyze.empty()) { model::Function &Function = *ToAnalyze.pop(); revng_log(Log, "Analyzing " << Function.Entry().toString()); FixedPointTask.advance(llvmName(Function)); OutlinedFunction &OutlinedFunction = *Functions.at(Function.Entry()); Changes Changes = analyzeFunctionABI(Function, OutlinedFunction, RegisterUser); if (Changes.Function) { revng_log(Log, "The function has changed, re-enqueing all callers:"); LoggerIndent Indent(Log); // The prototype of the function we analyzed has changed, reanalyze // callers auto &FunctionNode = BasicBlockNodeMap[GCBI.getBlockAt(Function.Entry())]; for (auto &CallerNode : FunctionNode->predecessors()) { if (CallerNode->Address.isValid()) { revng_log(Log, CallerNode->Address.toString()); ToAnalyze.insert(&Binary->Functions().at(CallerNode->Address)); } } } // Register for re-analysis all the callees for which we have new // information for (const MetaAddress &ToReanalyze : Changes.Callees) { revng_assert(ToReanalyze.isValid()); revng_log(Log, "Re-enqueing callee " << ToReanalyze.toString()); ToAnalyze.insert(&Binary->Functions().at(ToReanalyze)); } } } Changes DetectABI::analyzeFunctionABI(const model::Function &Function, OutlinedFunction &OutlinedFunction, OpaqueRegisterUser &RegisterReader) { revng_log(Log, "Analyzing the ABI of " << Function.Entry().toString()); // // Enrich all the call sites // // Collect all calls to precall_hook and postcall_hook SmallVector> Hooks; for (Instruction &I : instructions(OutlinedFunction.Function.get())) { if (CallInst *CallToPreHook = getCallTo(&I, Analyzer.preCallHook())) { Hooks.emplace_back(CallToPreHook, true); } else if (CallInst *CallToPostHook = getCallTo(&I, Analyzer.postCallHook())) { Hooks.emplace_back(CallToPostHook, false); } } // TODO: the checks should be enabled conditionally based on the user. revng::NonDebugInfoCheckingIRBuilder Builder(M.getContext()); for (auto &[Call, IsPreHook] : Hooks) { auto CallerBlock = BasicBlockID::fromValue(Call->getArgOperand(0)); auto CalleeAddress = MetaAddress::fromValue(Call->getArgOperand(1)); auto ExtractString = extractFromConstantStringPtr; StringRef CalleeSymbol = ExtractString(Call->getArgOperand(2)); auto &&[Summary, _] = Oracle.getCallSite(Function.Entry(), CallerBlock, CalleeAddress, CalleeSymbol); auto &ABIResults = Summary->ABIResults; auto *BB = Call->getParent(); if (IsPreHook) { Builder.SetInsertPoint(Call); for (auto *CSV : ABIResults.ArgumentsRegisters) { // Inject a virtual read of the arguments of the callee // TODO: drop const_cast. Unfortunately it requires a significant // refactoring. RegisterReader.read(Builder, const_cast(CSV)); } // Ensure the precall_hook is the first instruction of the block BB->splitBasicBlockBefore(Call); } else { // Ensure the postcall_hook is the last instruction of the block BB->splitBasicBlockBefore(Call->getNextNode()); Builder.SetInsertPoint(BB, BB->getFirstInsertionPt()); for (auto *CSV : ABIResults.ReturnValuesRegisters) { // Inject a virtual write to the return values of the callee // TODO: drop const_cast. Unfortunately it requires a significant // refactoring. RegisterReader.write(Builder, const_cast(CSV)); } } } // Perform the analysis Changes Changes = runAnalyses(Function.Entry(), OutlinedFunction); RegisterReader.purgeCreated(); return Changes; } // TODO: drop this. // Since function type conversion is capable of handling the holes // internally, there's not much reason to push such invasive changes // this early in the pipeline. void DetectABI::applyABIDeductions() { if (ABIEnforcement == NoABIEnforcement) return; auto ABI = abi::Definition::get(Binary->DefaultABI()); for (const model::Function &Function : Binary->Functions()) { auto &Summary = Oracle.getLocalFunction(Function.Entry()); abi::Definition::RegisterSet Arguments; abi::Definition::RegisterSet RValues; model::Architecture::Values Architecture = Binary->Architecture(); for (const auto &Register : model::Architecture::registers(Architecture)) { auto Name = model::Register::getCSVName(Register); if (llvm::GlobalVariable *CSV = M.getGlobalVariable(Name, true)) { if (Summary.ABIResults.ArgumentsRegisters.contains(CSV)) Arguments.emplace(Register); if (Summary.ABIResults.ReturnValuesRegisters.contains(CSV)) RValues.emplace(Register); } } if (ABIEnforcement == FullABIEnforcement) { Arguments = ABI.enforceArgumentRegisterState(std::move(Arguments)); RValues = ABI.enforceReturnValueRegisterState(std::move(RValues)); } else { if (auto R = ABI.tryDeducingArgumentRegisterState(std::move(Arguments))) Arguments = *R; else continue; // Register deduction failed. if (auto R = ABI.tryDeducingReturnValueRegisterState(std::move(RValues))) RValues = *R; else continue; // Register deduction failed. } efa::CSVSet ResultingArguments; efa::CSVSet ResultingReturnValues; for (const auto &Register : model::Architecture::registers(Architecture)) { auto Name = model::Register::getCSVName(Register); if (llvm::GlobalVariable *CSV = M.getGlobalVariable(Name, true)) { if (Arguments.contains(Register)) ResultingArguments.insert(CSV); if (RValues.contains(Register)) ResultingReturnValues.insert(CSV); } } for (auto &Block : Summary.CFG) { for (auto &Edge : Block.Successors()) { if (efa::FunctionEdgeType::isCall(Edge->Type()) && Edge->Type() != efa::FunctionEdgeType::FunctionCall) { revng_assert(Block.ID().isValid()); auto &CSSummary = Summary.ABIResults.CallSites.at(Block.ID()); CSSummary.ArgumentsRegisters = ResultingArguments; CSSummary.ReturnValuesRegisters = ResultingReturnValues; } } } Summary.ABIResults.ArgumentsRegisters = std::move(ResultingArguments); Summary.ABIResults.ReturnValuesRegisters = std::move(ResultingReturnValues); if (Log.isEnabled()) { Log << "Summary for " << Function.Name() << ":\n"; Summary.dump(Log); Log << DoLog; } } } void DetectABI::recordRegisters(const efa::CSVSet &CSVs, auto Inserter) { for (auto *CSV : CSVs) { auto Reg = model::Register::fromCSVName(CSV->getName(), Binary->Architecture()); Inserter.emplace(Reg).Type() = model::PrimitiveType::makeGeneric(Reg); } } void DetectABI::finalizeModel() { using namespace model; // Fill up the model and build its prototype for each function std::set Functions; for (model::Function &Function : Binary->Functions()) { // Ignore if we already have a prototype if (not Function.Prototype().isEmpty()) continue; MetaAddress EntryPC = Function.Entry(); revng_assert(EntryPC.isValid()); auto &Summary = Oracle.getLocalFunction(EntryPC); // Replace function attributes Function.Attributes() = Summary.Attributes; auto &&[Prototype, NewType] = Binary->makeRawFunctionDefinition(); Prototype.Architecture() = getCodeArchitecture(EntryPC); // Record arguments and return values recordRegisters(Summary.ABIResults.ArgumentsRegisters, Prototype.Arguments().batch_insert()); recordRegisters(Summary.ABIResults.ReturnValuesRegisters, Prototype.ReturnValues().batch_insert()); // Preserved registers const auto &ClobberedRegisters = Summary.ClobberedRegisters; auto PreservedRegisters = computePreservedRegisters(ClobberedRegisters); Prototype.PreservedRegisters() = std::move(PreservedRegisters); // Final stack offset Prototype.FinalStackOffset() = Summary.ElectedFSO.value_or(0); Function.Prototype() = std::move(NewType); Functions.insert(&Function); } // Build prototype for indirect function calls for (auto &Function : Functions) { auto &Summary = Oracle.getLocalFunction(Function->Entry()); for (auto &Block : Summary.CFG) { // TODO: we do not detect prototypes for inlined call sites if (Block.ID().isInlined()) continue; MetaAddress BlockAddress = Block.ID().notInlinedAddress(); for (auto &Edge : Block.Successors()) { if (auto *CE = llvm::dyn_cast(Edge.get())) { auto &CallSitePrototypes = Function->CallSitePrototypes(); bool IsDirect = CE->Destination().isValid(); bool IsDynamic = not CE->DynamicFunction().empty(); bool HasInfoOnEdge = CallSitePrototypes.contains(BlockAddress); if (not IsDynamic and not IsDirect and not HasInfoOnEdge) { // It's an indirect call for which we have now call site information auto Prototype = buildPrototypeForIndirectCall(Summary, Block); // This analysis does not have the power to detect whether an // indirect call site is a tail call, noreturn or inline bool IsTailCall = false; // Register new prototype Function->CallSitePrototypes().emplace(BlockAddress, std::move(Prototype), IsTailCall); } } } } } for (auto &Function : Functions) { auto &Summary = Oracle.getLocalFunction(Function->Entry()); efa::ControlFlowGraph FM(Function->Entry(), "", Summary.CFG); FM.verify(*Binary, true); } revng_check(Binary->verify(true)); } void DetectABI::propagatePrototypes() { for (model::Function &Function : Binary->Functions()) { propagatePrototypesInFunction(Function); } } // TODO: is this still necessary after the new EFA? void DetectABI::propagatePrototypesInFunction(model::Function &Function) { const MetaAddress &Entry = Function.Entry(); revng_log(Log, "Trying to propagate prototypes for " << Entry.toString()); LoggerIndent Indent(Log); FunctionSummary &Summary = Oracle.getLocalFunction(Entry); RUAResults &ABI = Summary.ABIResults; SortedVector &CFG = Summary.CFG; CSVSet &WrittenRegisters = Summary.WrittenRegisters; bool IsSingleNode = CFG.size() == 1; revng_log(Log, "IsSingleNode: " << IsSingleNode); if (not IsSingleNode) return; efa::BasicBlock &Block = *CFG.begin(); bool HasSingleSuccessor = Block.Successors().size() == 1; revng_log(Log, "HasSingleSuccessor: " << HasSingleSuccessor); if (not HasSingleSuccessor) return; auto &Successor = *Block.Successors().begin(); const auto *Call = dyn_cast(Successor.get()); bool SuccessorIsCall = Call != nullptr; revng_log(Log, "SuccessorIsCall: " << SuccessorIsCall); // Select new prototype for wrapper function if (SuccessorIsCall) { auto Prototype = getPrototype(*Binary, Entry, Block, *Call); using abi::FunctionType::Layout; // Get layout of wrapped function Layout CalleeLayout = Layout::make(*Prototype); // Verify that wrapper function: // - don't write stack pointer // - don't write to callee arguments // - every store instruction writes to registers (not memory) llvm::BasicBlock *BB = GCBI.getBlockAt(Block.ID().start()); GlobalVariable *StackPointer = GCBI.spReg(); // Check if wrapper writes to Stack Pointer const bool WritesSP = WrittenRegisters.contains(StackPointer); using std::ranges::count_if; auto IsWrittenByCaller = [this, &WrittenRegisters](auto &Argument) { auto *CSV = M.getGlobalVariable(model::Register::getName(Argument)); return WrittenRegisters.count(CSV); }; const auto &Arguments = CalleeLayout.argumentRegisters(); bool WritesCalleeArgs = any_of(Arguments, IsWrittenByCaller); const auto &ReturnValues = CalleeLayout.returnValueRegisters(); bool WritesCalleeReturnValues = any_of(ReturnValues, IsWrittenByCaller); bool WritesToMemory = count_if(*BB, isWritingToMemory) > 0; bool WritesOnlyRegisters = WritesToMemory == 0; revng_log(Log, "WritesSP: " << WritesSP); revng_log(Log, "WritesCalleeArgs: " << WritesCalleeArgs); revng_log(Log, "WritesOnlyRegisters: " << WritesOnlyRegisters); // When above conditions are met, overwrite wrappers prototype with // wrapped function prototype (`CABIFunctionDefinition` or // `RawFunctionDefinition`) if (not WritesSP and not WritesCalleeArgs and not WritesCalleeReturnValues and WritesOnlyRegisters) { if (Log.isEnabled()) { Log << "Overwriting " << Entry.toString() << " prototype "; if (!Function.Prototype().isEmpty()) Log << "(" << toString(Function.Prototype()) << ") "; Log << "with wrapped function's prototype: " << toString(model::copyTypeDefinition(*Prototype)) << DoLog; } Function.Prototype() = Binary->makeType(Prototype->key()); if (Function.Name().empty()) { if (not Call->DynamicFunction().empty()) { Function.Name() = Call->DynamicFunction(); } else if (Call->Destination().isValid()) { const model::Function &Callee = Binary->Functions() .at(Call->Destination() .notInlinedAddress()); if (not Callee.Name().empty()) Function.Name() = Callee.Name(); else if (not Callee.Name().empty()) Function.Name() = Callee.Name(); } } } } // TODO: should this be done at a higher abstraction level? model::deduplicateCollidingNames(Binary); } model::UpcastableType DetectABI::buildPrototypeForIndirectCall(const FunctionSummary &CallerSummary, const efa::BasicBlock &CallerBlock) { using namespace model; auto &&[Prototype, NewType] = Binary->makeRawFunctionDefinition(); Prototype.Architecture() = getCodeArchitecture(CallerBlock.ID().start()); bool Found = false; for (const auto &[PC, CallSites] : CallerSummary.ABIResults.CallSites) { if (PC != CallerBlock.ID()) continue; revng_assert(!Found); Found = true; recordRegisters(CallSites.ArgumentsRegisters, Prototype.Arguments().batch_insert()); recordRegisters(CallSites.ReturnValuesRegisters, Prototype.ReturnValues().batch_insert()); } revng_assert(Found); // Import FinalStackOffset and CalleeSavedRegisters from the default // prototype const FunctionSummary &DefaultSummary = Oracle.getDefault(); const auto &Clobbered = DefaultSummary.ClobberedRegisters; Prototype.PreservedRegisters() = computePreservedRegisters(Clobbered); Prototype.FinalStackOffset() = DefaultSummary.ElectedFSO.value_or(0); return std::move(NewType); } static void combineCrossCallSites(auto &CallSite, auto &Callee) { // TODO: why not return values? for (auto *CSV : CallSite.ArgumentsRegisters) { Callee.ArgumentsRegisters.insert(CSV); } } bool DetectABI::getRegisterState(model::Register::Values RegisterValue, const CSVSet &ABIRegisterMap) { auto Name = model::Register::getCSVName(RegisterValue); if (llvm::GlobalVariable *CSV = M.getGlobalVariable(Name, true)) { return ABIRegisterMap.contains(CSV); } return false; } CSVSet DetectABI::findWrittenRegisters(llvm::Function *F) { using namespace llvm; CSVSet WrittenRegisters; for (auto &BB : *F) { for (auto &I : BB) { if (auto *SI = dyn_cast(&I)) { Value *Ptr = skipCasts(SI->getPointerOperand()); if (auto *GV = dyn_cast(Ptr)) WrittenRegisters.insert(GV); } } } return WrittenRegisters; } CSVSet DetectABI::computePreservedCSVs(const CSVSet &ClobberedRegisters) const { using llvm::GlobalVariable; using std::set; CSVSet PreservedRegisters(Analyzer.abiCSVs().begin(), Analyzer.abiCSVs().end()); for (GlobalVariable *ClobberedRegister : ClobberedRegisters) PreservedRegisters.erase(ClobberedRegister); return PreservedRegisters; } TrackingSortedVector DetectABI::computePreservedRegisters(const CSVSet &ClobberedRegisters) const { using namespace model; TrackingSortedVector Result; CSVSet PreservedRegisters = computePreservedCSVs(ClobberedRegisters); auto PreservedRegistersInserter = Result.batch_insert(); for (auto *CSV : PreservedRegisters) { auto RegisterID = model::Register::fromCSVName(CSV->getName(), Binary->Architecture()); if (RegisterID == Register::Invalid) continue; PreservedRegistersInserter.insert(RegisterID); } return Result; } static void suppressCalleeSaved(RUAResults &ABIResults, const CSVSet &CalleeSavedRegs) { // Suppress from arguments for (const auto &Reg : CalleeSavedRegs) ABIResults.ArgumentsRegisters.erase(Reg); // Suppress from return values for (const auto &Reg : CalleeSavedRegs) ABIResults.ReturnValuesRegisters.erase(Reg); // Suppress from call-sites for (const auto &[K, _] : ABIResults.CallSites) { for (const auto &Reg : CalleeSavedRegs) { ABIResults.CallSites[K].ArgumentsRegisters.erase(Reg); ABIResults.CallSites[K].ReturnValuesRegisters.erase(Reg); } } } template static auto zeroOrOne(const T &Range, const F &Predicate) -> decltype(&*Range.begin()) { decltype(&*Range.begin()) Result = nullptr; for (auto &E : Range) { if (Predicate(E)) { revng_assert(not Result); Result = &E; } } return Result; } Changes DetectABI::runAnalyses(MetaAddress EntryAddress, OutlinedFunction &OutlinedFunction) { using namespace llvm; using llvm::BasicBlock; revng::IRBuilder Builder(M.getContext()); RUAResults ABIResults; // Find registers that may be target of at least one store. This helps // refine the final results. auto WrittenRegisters = findWrittenRegisters(OutlinedFunction.Function.get()); // Run ABI-independent data-flow analyses ABIResults = analyzeRegisterUsage(OutlinedFunction.Function.get(), GCBI, Binary->Architecture(), Analyzer.preCallHook(), Analyzer.postCallHook(), Analyzer.retHook()); // We say that a register is callee-saved when, besides being preserved by // the callee, there is at least a write onto this register. FunctionSummary &Summary = Oracle.getLocalFunction(EntryAddress); auto CalleeSavedRegs = computePreservedCSVs(Summary.ClobberedRegisters); auto ActualCalleeSavedRegs = llvm::set_intersection(CalleeSavedRegs, WrittenRegisters); // Refine ABI analyses results by suppressing callee-saved and stack // pointer registers. suppressCalleeSaved(ABIResults, ActualCalleeSavedRegs); // Commit ABI analysis results to the oracle auto Old = Summary.ABIResults; Summary.ABIResults.combine(ABIResults); Summary.WrittenRegisters = WrittenRegisters; Changes Changes; Changes.Function = Old != Summary.ABIResults; for (auto &[BlockID, CallSite] : ABIResults.CallSites) { // TODO: why are we ignoring inlined call sites? if (BlockID.isInlined()) continue; MetaAddress Callee = CallSite.CalleeAddress; // TODO: eventually we'll want to add arguments/return values to dynamic // functions too FunctionSummary *Summary = nullptr; if (Callee.isValid()) { Summary = &Oracle.getLocalFunction(Callee); revng_assert(Summary != nullptr); } else { Summary = Oracle.getExactCallSite(EntryAddress, BlockID).first; } // TODO: are longjmps calls? Do we want to collect prototype for them? // Right now, they are in the IR, but we do not register them as call // sites in the Oracle. if (Summary != nullptr) { bool Changed = false; RUAResults &ToAdjust = Summary->ABIResults; for (auto *CSV : CallSite.ArgumentsRegisters) Changed = Changed or ToAdjust.ArgumentsRegisters.insert(CSV).second; for (auto *CSV : CallSite.ReturnValuesRegisters) Changed = Changed or ToAdjust.ReturnValuesRegisters.insert(CSV).second; if (Changed and Callee.isValid()) Changes.Callees.insert(Callee); } } return Changes; } static void runDetectABI(Module &M, GeneratedCodeBasicInfo &GCBI, ControlFlowGraphCache &FMC, TupleTree &Binary) { using FSOracle = FunctionSummaryOracle; FSOracle Oracle = FSOracle::importFullPrototypes(M, GCBI, *Binary); CFGAnalyzer Analyzer(M, GCBI, Binary, Oracle); DetectABI ABIDetector(M, GCBI, FMC, Binary, Oracle, Analyzer); ABIDetector.run(); } bool DetectABIPass::runOnModule(Module &M) { revng_log(PassesLog, "Starting EarlyFunctionAnalysis"); if (not M.getFunction("root") or M.getFunction("root")->isDeclaration()) return false; auto &GCBI = getAnalysis().getGCBI(); auto &FMC = getAnalysis().get(); auto &LMP = getAnalysis().get(); TupleTree &Binary = LMP.getWriteableModel(); runDetectABI(M, GCBI, FMC, Binary); return false; } char DetectABIPass::ID = 0; using ABIDetectionPass = RegisterPass; static ABIDetectionPass X("detect-abi", "ABI Detection Pass", true, false); } // namespace efa namespace revng::pypeline::analyses { llvm::Error DetectABI::run(Model &Model, const Request &Incoming, llvm::StringRef Configuration, LLVMRootContainer &ModuleContainer) { llvm::Module &Module = ModuleContainer.getModule(); TupleTree &TupleModel = Model.get(); model::Binary &Binary = *TupleModel; GeneratedCodeBasicInfo GCBI(Binary, Module); revng::pipes::CFGMap CFGs(""); ControlFlowGraphCache FMC(CFGs); // Link helper bodies once at the start of the analysis, to avoid relinking // for every `inline-helpers` call { llvm::legacy::PassManager PM; PM.add(new LinkHelpersToInlinePass()); PM.run(Module); } collectFunctionsFromCallees(Module, GCBI, Binary); efa::runDetectABI(Module, GCBI, FMC, TupleModel); collectFunctionsFromUnusedAddresses(Module, GCBI, Binary, FMC); efa::runDetectABI(Module, GCBI, FMC, TupleModel); return llvm::Error::success(); } } // namespace revng::pypeline::analyses