/// \file EarlyFunctionAnalysis.cpp // // This file is distributed under the MIT License. See LICENSE.md for details. // #include #include #include #include #include #include #include "llvm/ADT/DepthFirstIterator.h" #include "llvm/ADT/PostOrderIterator.h" #include "llvm/ADT/SCCIterator.h" #include "llvm/ADT/Twine.h" #include "llvm/Analysis/BasicAliasAnalysis.h" #include "llvm/Analysis/ScopedNoAliasAA.h" #include "llvm/CodeGen/UnreachableBlockElim.h" #include "llvm/IR/Attributes.h" #include "llvm/IR/Constants.h" #include "llvm/IR/Dominators.h" #include "llvm/IR/Function.h" #include "llvm/IR/GlobalVariable.h" #include "llvm/IR/IRBuilder.h" #include "llvm/IR/Instruction.h" #include "llvm/IR/Instructions.h" #include "llvm/IR/LLVMContext.h" #include "llvm/IR/Value.h" #include "llvm/IR/Verifier.h" #include "llvm/Passes/PassBuilder.h" #include "llvm/Support/raw_ostream.h" #include "llvm/Transforms/InstCombine/InstCombine.h" #include "llvm/Transforms/Scalar/EarlyCSE.h" #include "llvm/Transforms/Scalar/GVN.h" #include "llvm/Transforms/Scalar/JumpThreading.h" #include "llvm/Transforms/Scalar/MergedLoadStoreMotion.h" #include "llvm/Transforms/Scalar/SROA.h" #include "llvm/Transforms/Scalar/SimplifyCFG.h" #include "llvm/Transforms/Utils.h" #include "llvm/Transforms/Utils/BasicBlockUtils.h" #include "llvm/Transforms/Utils/Cloning.h" #include "llvm/Transforms/Utils/CodeExtractor.h" #include "llvm/Transforms/Utils/Mem2Reg.h" #include "revng/ABI/FunctionType.h" #include "revng/ADT/KeyedObjectTraits.h" #include "revng/ADT/Queue.h" #include "revng/ADT/SortedVector.h" #include "revng/ADT/ZipMapIterator.h" #include "revng/BasicAnalyses/RemoveHelperCalls.h" #include "revng/BasicAnalyses/RemoveNewPCCalls.h" #include "revng/EarlyFunctionAnalysis/EarlyFunctionAnalysis.h" #include "revng/EarlyFunctionAnalysis/FunctionMetadata.h" #include "revng/Model/Architecture.h" #include "revng/Model/Binary.h" #include "revng/Model/NamedTypedRegister.h" #include "revng/Model/Register.h" #include "revng/Model/TypedRegister.h" #include "revng/Support/Assert.h" #include "revng/Support/CommandLine.h" #include "revng/Support/IRHelpers.h" #include "revng/Support/MetaAddress.h" #include "ABIAnalyses/ABIAnalysis.h" using llvm::ArrayRef; using llvm::BasicBlock; using llvm::Function; using llvm::GlobalVariable; using llvm::Instruction; using llvm::Module; using llvm::raw_fd_ostream; using llvm::RegisterPass; using llvm::SmallVectorImpl; using llvm::Type; using FunctionEdgeTypeValue = efa::FunctionEdgeType::Values; using FunctionTypeValue = model::FunctionType::Values; using GCBI = GeneratedCodeBasicInfo; using namespace llvm::cl; static Logger<> EarlyFunctionAnalysisLog("earlyfunctionanalysis"); struct BasicBlockNodeData { BasicBlockNodeData(llvm::BasicBlock *BB) : BB(BB){}; llvm::BasicBlock *BB; }; using BasicBlockNode = BidirectionalNode; using SmallCallGraph = GenericGraph; template<> struct llvm::DOTGraphTraits : public llvm::DefaultDOTGraphTraits { using EdgeIterator = llvm::GraphTraits::ChildIteratorType; DOTGraphTraits(bool IsSimple = false) : DefaultDOTGraphTraits(IsSimple) {} static std::string getNodeLabel(const BasicBlockNode *Node, const SmallCallGraph *Graph) { if (Node->BB == nullptr) return "null"; return Node->BB->getName().str(); } static std::string getEdgeAttributes(const BasicBlockNode *Node, const EdgeIterator EI, const SmallCallGraph *Graph) { return "color=black,style=dashed"; } }; namespace EarlyFunctionAnalysis { template<> char EarlyFunctionAnalysis::ID = 0; template<> char EarlyFunctionAnalysis::ID = 0; using ABIDetectionPass = RegisterPass>; static ABIDetectionPass X("detect-abi", "ABI Detection Pass", true, false); using CFGCollectionPass = RegisterPass>; static CFGCollectionPass Y("collect-cfg", "CFG Collection Pass", true, true); static opt CallGraphOutputPath("cg-output", desc("Dump to disk the recovered " "call graph."), value_desc("filename")); static opt IndirectBranchInfoSummaryPath("indirect-branch-info-" "summary", desc("Write the results " "of SA2 on disk."), value_desc("filename")); static opt AAWriterPath("aa-writer", desc("Dump to disk the outlined functions " "with annotated alias info."), value_desc("filename")); /// A summary of the analysis of a function. /// /// For each function detected, the following information are included: /// its type ("regular", "noreturn" or "fake"), which ABI registers are /// overwritten, its control-flow graph, and an elected stack offset (to /// tell if the stack pointer is restored at its original position). struct FunctionSummary { public: model::FunctionType::Values Type; std::set ClobberedRegisters; ABIAnalyses::ABIAnalysesResults ABIResults; SortedVector CFG; std::optional ElectedFSO; llvm::Function *FakeFunction; public: FunctionSummary(model::FunctionType::Values Type, std::set ClobberedRegisters, ABIAnalyses::ABIAnalysesResults ABIResults, SortedVector CFG, std::optional ElectedFSO, llvm::Function *FakeFunction) : Type(Type), ClobberedRegisters(std::move(ClobberedRegisters)), ABIResults(std::move(ABIResults)), CFG(std::move(CFG)), ElectedFSO(ElectedFSO), FakeFunction(FakeFunction) { if (FakeFunction != nullptr) revng_assert(CFG.empty()); } FunctionSummary() = delete; FunctionSummary(const FunctionSummary &) = delete; FunctionSummary(FunctionSummary &&) = default; FunctionSummary &operator=(const FunctionSummary &) = delete; FunctionSummary &operator=(FunctionSummary &&) = default; public: static bool compare(const FunctionSummary &Old, const FunctionSummary &New) { if (New.Type == Old.Type) return std::includes(Old.ClobberedRegisters.begin(), Old.ClobberedRegisters.end(), New.ClobberedRegisters.begin(), New.ClobberedRegisters.end()); return New.Type <= Old.Type; } void dump() const debug_function { dump(dbg); } template void dump(T &Output) const { Output << "Dumping summary \n" << " Type: " << Type << "\n" << " ElectedFSO: " << (ElectedFSO.has_value() ? *ElectedFSO : -1) << "\n" << " Clobbered registers: \n"; for (auto *Reg : ClobberedRegisters) Output << " " << Reg->getName().str() << "\n"; Output << " ABI info: \n"; ABIResults.dump(); } }; /// A cache holding the results of the analyses of functions. /// /// Leaf subroutines are analyzed first; non-leaf afterwards. The whole /// process is repeated until a fixed point is reached, and no further /// refinement can be achieved. The cache can be queried through specific /// methods to retrieve the stored information, acting as a oracle. class FunctionAnalysisResults { private: /// For each function, the result of the intraprocedural analysis std::map FunctionsBucket; /// A default summary for indirect function calls is maintained. It has /// `Regular` type, it clobbers all the registers and it has default FSO. FunctionSummary DefaultSummary; public: FunctionAnalysisResults(FunctionSummary DefaultSummary) : DefaultSummary(std::move(DefaultSummary)) {} FunctionSummary &insert(const MetaAddress &PC, FunctionSummary &&Summary) { auto It = FunctionsBucket.find(PC); revng_assert(It == FunctionsBucket.end()); return FunctionsBucket.emplace(PC, std::move(Summary)).first->second; } FunctionSummary &at(MetaAddress PC) { return FunctionsBucket.at(PC); } const FunctionSummary &at(MetaAddress PC) const { return FunctionsBucket.at(PC); } model::FunctionType::Values getFunctionType(MetaAddress PC) const { return get(PC).Type; } bool isFakeFunction(MetaAddress PC) const { return getFunctionType(PC) == model::FunctionType::Values::Fake; } llvm::Function *getFakeFunction(MetaAddress PC) const { return get(PC).FakeFunction; } const auto &getRegistersClobbered(MetaAddress PC) const { return get(PC).ClobberedRegisters; } std::optional getElectedFSO(MetaAddress PC) const { return get(PC).ElectedFSO; } bool registerFunction(MetaAddress PC, FunctionSummary &&F) { revng_assert(PC.isValid()); auto It = FunctionsBucket.find(PC); if (It != FunctionsBucket.end()) { bool Changed = FunctionSummary::compare(It->second, F); It->second = std::move(F); return not Changed; } else { FunctionsBucket.emplace(PC, std::move(F)); return true; } } private: const FunctionSummary &get(MetaAddress PC) const { auto It = FunctionsBucket.find(PC); if (It != FunctionsBucket.end()) return It->second; return DefaultSummary; } }; /// An outlined function helper object. struct OutlinedFunction { /// The actual LLVM outlined function llvm::Function *F = nullptr; /// The marker that detects returns and regular jumps llvm::Function *IndirectBranchInfoMarker = nullptr; llvm::BasicBlock *AnyPCCloned = nullptr; llvm::BasicBlock *UnexpectedPCCloned = nullptr; OutlinedFunction() = default; OutlinedFunction(const OutlinedFunction &Other) = delete; OutlinedFunction(OutlinedFunction &&Other) : F(Other.F) { Other.F = nullptr; } OutlinedFunction &operator=(OutlinedFunction &&) = delete; OutlinedFunction &operator=(const OutlinedFunction &) = delete; llvm::Function *extractFunction() { auto *ToReturn = F; F = nullptr; return ToReturn; } ~OutlinedFunction() { if (F != nullptr) { revng_assert(F->use_empty() && "Failed to remove all users of the outlined function."); F->eraseFromParent(); } if (IndirectBranchInfoMarker != nullptr) IndirectBranchInfoMarker->eraseFromParent(); } }; struct TemporaryOpaqueFunction { llvm::Function *F = nullptr; llvm::FunctionType *FTy; llvm::StringRef Name; llvm::Module *M; TemporaryOpaqueFunction(llvm::FunctionType *FTy, llvm::StringRef Name, llvm::Module *M) : FTy(FTy), Name(Name), M(M) { F = Function::Create(FTy, llvm::GlobalValue::ExternalLinkage, Name, M); revng_assert(F != nullptr); F->addFnAttr(llvm::Attribute::ReadOnly); F->addFnAttr(llvm::Attribute::NoUnwind); F->addFnAttr(llvm::Attribute::WillReturn); } ~TemporaryOpaqueFunction() { if (F != nullptr) { revng_assert(F->use_empty() && "Failed to remove all users of the temporary opaque " "function."); F->eraseFromParent(); } } }; using BasicBlockQueue = UniquedQueue; /// An intraprocedural analysis storage. /// /// Implementation of the intraprocedural stack analysis. It holds the /// necessary information to detect the boundaries of a function, track /// how the stack evolves within those functions, and detect the /// callee-saved registers. class FunctionEntrypointAnalyzer { private: llvm::Module &M; llvm::LLVMContext &Context; GeneratedCodeBasicInfo *GCBI; ArrayRef ABICSVs; BasicBlockQueue *EntrypointsQueue; FunctionAnalysisResults &Oracle; const TupleTree &Binary; /// PreHookMarker and PostHookMarker mark the presence of an original /// function call, and surround a basic block containing the registers /// clobbered by the function called. They take the MetaAddress of the /// callee and the call-site. TemporaryOpaqueFunction PreHookMarker; TemporaryOpaqueFunction PostHookMarker; TemporaryOpaqueFunction RetHookMarker; /// UnexpectedPCMarker is used to indicate that `unexpectedpc` basic /// block of fake functions need to be adjusted to jump to /// `unexpectedpc` of their caller. TemporaryOpaqueFunction UnexpectedPCMarker; std::unique_ptr OutputIBI; std::unique_ptr OutputAAWriter; OpaqueFunctionsPool RegistersClobberedPool; OpaqueFunctionsPool OpaqueBranchConditionsPool; const llvm::CodeExtractorAnalysisCache CEAC; const ProgramCounterHandler *PCH; public: FunctionEntrypointAnalyzer(llvm::Module &, GeneratedCodeBasicInfo *GCBI, ArrayRef, BasicBlockQueue *, FunctionAnalysisResults &, const TupleTree &); public: void importModel(); void runInterproceduralAnalysis(); void interproceduralPropagation(); void finalizeModel(TupleTree &); void recoverCFG(); void serializeFunctionMetadata(); private: /// The `analyze` method is the entry point of the intraprocedural analysis, /// and it is called on each function entrypoint until a fixed point is /// reached. It is responsible for performing the whole computation. FunctionSummary analyze(llvm::BasicBlock *BB, bool ShouldAnalyzeABI); private: OutlinedFunction outlineFunction(llvm::BasicBlock *BB); void integrateFunctionCallee(llvm::BasicBlock *BB, MetaAddress); UpcastablePointer handleFunctionCallForCFG(llvm::CallInst *); SortedVector collectDirectCFG(OutlinedFunction *F); void initMarkersForABI(OutlinedFunction *F, llvm::SmallVectorImpl &, llvm::IRBuilder<> &); std::set findWrittenRegisters(llvm::Function *F); void createIBIMarker(OutlinedFunction *F, llvm::SmallVectorImpl &, llvm::IRBuilder<> &); void opaqueBranchConditions(llvm::Function *F, llvm::IRBuilder<> &); void materializePCValues(llvm::Function *F, llvm::IRBuilder<> &); void runOptimizationPipeline(llvm::Function *F); FunctionSummary milkInfo(OutlinedFunction *F, SortedVector &, ABIAnalyses::ABIAnalysesResults &, const std::set &, bool ShouldAnalyzeABI); llvm::Function *createFakeFunction(llvm::BasicBlock *BB); UpcastablePointer buildPrototype(TupleTree &, const FunctionSummary &, const efa::BasicBlock &); FunctionSummary importPrototype(model::FunctionType::Values, model::TypePath); private: static auto *markerType(llvm::Module &M) { return llvm::FunctionType::get(Type::getVoidTy(M.getContext()), { MetaAddress::getStruct(&M), MetaAddress::getStruct(&M) }, false); } static auto *unexpectedPCMarkerType(llvm::Module &M) { return llvm::FunctionType::get(Type::getVoidTy(M.getContext()), false); } }; using TOF = TemporaryOpaqueFunction; using FEA = FunctionEntrypointAnalyzer; FEA::FunctionEntrypointAnalyzer(llvm::Module &M, GeneratedCodeBasicInfo *GCBI, ArrayRef ABICSVs, BasicBlockQueue *EntrypointsQueue, FunctionAnalysisResults &Oracle, const TupleTree &Binary) : M(M), Context(M.getContext()), GCBI(GCBI), ABICSVs(ABICSVs), EntrypointsQueue(EntrypointsQueue), Oracle(Oracle), Binary(Binary), // Initialize hook markers for subsequent ABI analyses on function calls PreHookMarker(TOF(markerType(M), "precall_hook", &M)), PostHookMarker(TOF(markerType(M), "postcall_hook", &M)), RetHookMarker(TOF(markerType(M), "retcall_hook", &M)), // Initialize marker to adjust `unexpectedpc` basic block for fake functions UnexpectedPCMarker(TOF(unexpectedPCMarkerType(M), "unexpectedpc_hook", &M)), RegistersClobberedPool(&M, false), OpaqueBranchConditionsPool(&M, false), // Initialize the cache for the `CodeExtractor` analysis on `root` CEAC(llvm::CodeExtractorAnalysisCache(*M.getFunction("root"))), PCH(GCBI->programCounterHandler()) { // Open streams for dumping results if (IndirectBranchInfoSummaryPath.getNumOccurrences() == 1) { std::ifstream File(IndirectBranchInfoSummaryPath.c_str()); if (File.is_open()) { int Status = std::remove(IndirectBranchInfoSummaryPath.c_str()); revng_assert(Status == 0); } std::error_code EC; OutputIBI = std::make_unique(IndirectBranchInfoSummaryPath, EC, llvm::sys::fs::OF_Append); revng_assert(!EC); *OutputIBI << "name,ra,fso,address"; for (const auto &Reg : ABICSVs) *OutputIBI << "," << Reg->getName(); *OutputIBI << "\n"; } if (AAWriterPath.getNumOccurrences() == 1) { std::ifstream File(AAWriterPath.c_str()); if (File.is_open()) { int Status = std::remove(AAWriterPath.c_str()); revng_assert(Status == 0); } std::error_code EC; OutputAAWriter = std::make_unique(AAWriterPath, EC, llvm::sys::fs::OF_Append); revng_assert(!EC); } } void FunctionEntrypointAnalyzer::serializeFunctionMetadata() { using namespace llvm; for (const auto &Function : Binary->Functions) { if (Function.Type == FunctionTypeValue::Invalid || Function.Type == FunctionTypeValue::Fake) continue; auto &CFG = Oracle.at(Function.Entry).CFG; BasicBlock *BB = GCBI->getBlockAt(Function.Entry); std::string Buffer; { efa::FunctionMetadata FM(Function.Entry); raw_string_ostream Stream(Buffer); for (efa::BasicBlock Edge : CFG) FM.ControlFlowGraph.insert(Edge); FM.verify(*Binary, true); serialize(Stream, FM); } Instruction *Term = BB->getTerminator(); MDNode *Node = MDNode::get(Context, MDString::get(Context, Buffer)); Term->setMetadata(FunctionMetadataMDName, Node); } } FunctionSummary FunctionEntrypointAnalyzer::importPrototype(model::FunctionType::Values Type, model::TypePath Prototype) { using namespace llvm; using namespace model; using Register = model::Register::Values; using State = abi::RegisterState::Values; FunctionSummary Summary(Type, { ABICSVs.begin(), ABICSVs.end() }, ABIAnalyses::ABIAnalysesResults(), {}, 0, nullptr); for (GlobalVariable *CSV : ABICSVs) { Summary.ABIResults.ArgumentsRegisters[CSV] = State::No; Summary.ABIResults.FinalReturnValuesRegisters[CSV] = State::No; } auto Layout = abi::FunctionType::Layout::make(Prototype); for (const auto &ArgumentLayout : Layout.Arguments) { for (Register ArgumentRegister : ArgumentLayout.Registers) { StringRef Name = model::Register::getCSVName(ArgumentRegister); if (GlobalVariable *CSV = M.getGlobalVariable(Name, true)) Summary.ABIResults.ArgumentsRegisters.at(CSV) = State::Yes; } } for (Register ReturnValueRegister : Layout.ReturnValue.Registers) { StringRef Name = model::Register::getCSVName(ReturnValueRegister); if (GlobalVariable *CSV = M.getGlobalVariable(Name, true)) Summary.ABIResults.FinalReturnValuesRegisters.at(CSV) = State::Yes; } std::set PreservedRegisters; for (Register CalleeSavedRegister : Layout.CalleeSavedRegisters) { StringRef Name = model::Register::getCSVName(CalleeSavedRegister); if (GlobalVariable *CSV = M.getGlobalVariable(Name, true)) PreservedRegisters.insert(CSV); } std::erase_if(Summary.ClobberedRegisters, [&](const auto &E) { return PreservedRegisters.count(E) != 0; }); Summary.ElectedFSO = Layout.FinalStackOffset; return Summary; } void FunctionEntrypointAnalyzer::importModel() { // Import existing functions from model for (const model::Function &Function : Binary->Functions) { if (Function.Type == model::FunctionType::Invalid) continue; Oracle.insert(Function.Entry, importPrototype(Function.Type, Function.Prototype)); } // Re-create fake functions, should they exist for (const model::Function &Function : Binary->Functions) { if (Function.Type != FunctionTypeValue::Fake) continue; auto &Summary = Oracle.at(Function.Entry); revng_assert(Summary.Type == FunctionTypeValue::Fake); Summary.FakeFunction = createFakeFunction(GCBI->getBlockAt(Function.Entry)); } // Import dynamic functions from model. Dynamic functions will take the // precedence over local functions. for (BasicBlock &BB : *M.getFunction("root")) { if (getType(&BB) != BlockType::JumpTargetBlock) continue; llvm::StringRef SymbolName = getDynamicSymbol(&BB); if (SymbolName.empty()) continue; auto [Entry, _] = getPC(&*BB.begin()); // TODO: should we assert that all dynamic function have been imported? auto It = Binary->ImportedDynamicFunctions.find(SymbolName.str()); if (It == Binary->ImportedDynamicFunctions.end()) continue; model::FunctionType::Values FunctionType; if (It->Attributes.count(model::FunctionAttribute::NoReturn) != 0) FunctionType = model::FunctionType::NoReturn; else FunctionType = model::FunctionType::Regular; Oracle.insert(Entry, importPrototype(FunctionType, getPrototype(*Binary, *It))); } } UpcastablePointer FEA::buildPrototype(TupleTree &OutputBinary, const FunctionSummary &Summary, const efa::BasicBlock &Block) { using namespace model; using RegisterState = abi::RegisterState::Values; auto NewType = makeType(); auto &CallType = *llvm::cast(NewType.get()); { auto ArgumentsInserter = CallType.Arguments.batch_insert(); auto ReturnValuesInserter = CallType.ReturnValues.batch_insert(); bool Found = false; for (const auto &[PC, CallSites] : Summary.ABIResults.CallSites) { if (PC != Block.Start) continue; revng_assert(!Found); Found = true; for (const auto &[Arg, RV] : zipmap_range(CallSites.ArgumentsRegisters, CallSites.ReturnValuesRegisters)) { auto *CSV = Arg == nullptr ? RV->first : Arg->first; RegisterState RSArg = Arg == nullptr ? RegisterState::Maybe : Arg->second; RegisterState RSRV = RV == nullptr ? RegisterState::Maybe : RV->second; auto RegisterID = model::Register::fromCSVName(CSV->getName(), Binary->Architecture); if (RegisterID == Register::Invalid || CSV == GCBI->spReg()) continue; auto *CSVType = CSV->getType()->getPointerElementType(); auto CSVSize = CSVType->getIntegerBitWidth() / 8; if (abi::RegisterState::shouldEmit(RSArg)) { NamedTypedRegister TR(RegisterID); TR.Type = { OutputBinary->getPrimitiveType(PrimitiveTypeKind::Generic, CSVSize), {} }; ArgumentsInserter.insert(TR); } if (abi::RegisterState::shouldEmit(RSRV)) { TypedRegister TR(RegisterID); TR.Type = { OutputBinary->getPrimitiveType(PrimitiveTypeKind::Generic, CSVSize), {} }; ReturnValuesInserter.insert(TR); } } } revng_assert(Found); CallType.PreservedRegisters = {}; CallType.FinalStackOffset = 0; } return NewType; } /// Finish the population of the model by building the prototype void FEA::finalizeModel(TupleTree &OutputBinary) { using namespace model; using RegisterState = abi::RegisterState::Values; // Fill up the model and build its prototype for each function std::set Functions; for (model::Function &Function : OutputBinary->Functions) { if (Function.Type != model::FunctionType::Invalid) continue; MetaAddress EntryPC = Function.Entry; revng_assert(EntryPC.isValid()); auto &Summary = Oracle.at(EntryPC); Function.Type = Summary.Type; auto NewType = makeType(); auto &FunctionType = *llvm::cast(NewType.get()); { auto ArgumentsInserter = FunctionType.Arguments.batch_insert(); auto ReturnValuesInserter = FunctionType.ReturnValues.batch_insert(); auto PreservedRegistersInserter = FunctionType.PreservedRegisters .batch_insert(); // Argument and return values for (const auto &[Arg, RV] : zipmap_range(Summary.ABIResults.ArgumentsRegisters, Summary.ABIResults.FinalReturnValuesRegisters)) { auto *CSV = Arg == nullptr ? RV->first : Arg->first; RegisterState RSArg = Arg == nullptr ? RegisterState::Maybe : Arg->second; RegisterState RSRV = RV == nullptr ? RegisterState::Maybe : RV->second; auto RegisterID = model::Register::fromCSVName(CSV->getName(), Binary->Architecture); if (RegisterID == Register::Invalid || CSV == GCBI->spReg()) continue; auto *CSVType = CSV->getType()->getPointerElementType(); auto CSVSize = CSVType->getIntegerBitWidth() / 8; if (abi::RegisterState::shouldEmit(RSArg)) { NamedTypedRegister TR(RegisterID); TR.Type = { OutputBinary->getPrimitiveType(PrimitiveTypeKind::Generic, CSVSize), {} }; ArgumentsInserter.insert(TR); } if (abi::RegisterState::shouldEmit(RSRV)) { TypedRegister TR(RegisterID); TR.Type = { OutputBinary->getPrimitiveType(PrimitiveTypeKind::Generic, CSVSize), {} }; ReturnValuesInserter.insert(TR); } } // Preserved registers std::set PreservedRegisters(ABICSVs.begin(), ABICSVs.end()); std::erase_if(PreservedRegisters, [&](const auto &E) { auto End = Summary.ClobberedRegisters.end(); return Summary.ClobberedRegisters.find(E) != End; }); for (auto *CSV : PreservedRegisters) { auto RegisterID = model::Register::fromCSVName(CSV->getName(), Binary->Architecture); if (RegisterID == Register::Invalid) continue; PreservedRegistersInserter.insert(RegisterID); } // Final stack offset FunctionType.FinalStackOffset = Summary.ElectedFSO.has_value() ? *Summary.ElectedFSO : 0; } Function.Prototype = OutputBinary->recordNewType(std::move(NewType)); Functions.insert(&Function); } // Build prototype for indirect function calls for (auto &Function : Functions) { if (Function->Type == FunctionTypeValue::Fake) continue; auto &Summary = Oracle.at(Function->Entry); for (auto &Block : Summary.CFG) { for (auto &Edge : Block.Successors) { if (efa::FunctionEdgeType::isCall(Edge->Type)) { auto *CE = llvm::cast(Edge.get()); if (not CE->DynamicFunction.empty()) { // It's a dynamic function call revng_assert(CE->Type == efa::FunctionEdgeType::FunctionCall); // The prototype must not exist among the ones of the call sites of // the function, it is implicitly the one of the callee. revng_assert(not Function->CallSitePrototypes.count(Block.Start)); } else if (CE->Destination.isValid()) { // It's a simple direct function call revng_assert(CE->Type == efa::FunctionEdgeType::FunctionCall); // The prototype must not exist among the ones of the call sites of // the function, it is implicitly the one of the callee. revng_assert(not Function->CallSitePrototypes.count(Block.Start)); } else { // It's an indirect call: forge a new prototype auto Prototype = buildPrototype(OutputBinary, Summary, Block); auto Path = OutputBinary->recordNewType(std::move(Prototype)); auto PrototypeInserter = Function->CallSitePrototypes .batch_insert(); PrototypeInserter.insert({ Block.Start, Path }); } } } } efa::FunctionMetadata FM(Function->Entry, Summary.CFG); FM.verify(*Binary, true); } revng_check(Binary->verify(true)); } static void combineCrossCallSites(auto &CallSite, auto &Callee) { using namespace ABIAnalyses; using RegisterState = abi::RegisterState::Values; for (auto &[FuncArg, CSArg] : zipmap_range(Callee.ArgumentsRegisters, CallSite.ArgumentsRegisters)) { auto *CSV = FuncArg == nullptr ? CSArg->first : FuncArg->first; auto RSFArg = FuncArg == nullptr ? RegisterState::Maybe : FuncArg->second; auto RSCSArg = CSArg == nullptr ? RegisterState::Maybe : CSArg->second; Callee.ArgumentsRegisters[CSV] = combine(RSFArg, RSCSArg); } } /// Perform cross-call site propagation void FunctionEntrypointAnalyzer::interproceduralPropagation() { for (const model::Function &Function : Binary->Functions) { auto &Summary = Oracle.at(Function.Entry); for (auto &[PC, CallSite] : Summary.ABIResults.CallSites) { if (PC == Function.Entry) combineCrossCallSites(CallSite, Summary.ABIResults); } } } /// Elect a final stack offset to tell whether the function is leaving /// the stack pointer higher than it was at the function entry. static std::optional electFSO(const auto &MaybeReturns) { auto It = std::min_element(MaybeReturns.begin(), MaybeReturns.end(), [](const auto &LHS, const auto &RHS) { return LHS.second < RHS.second; }); if (It == MaybeReturns.end()) return {}; return It->second; } static UpcastablePointer makeEdge(MetaAddress Destination, efa::FunctionEdgeType::Values Type) { efa::FunctionEdge *Result = nullptr; using ReturnType = UpcastablePointer; if (efa::FunctionEdgeType::isCall(Type)) return ReturnType::make(Destination, Type); else return ReturnType::make(Destination, Type); }; static MetaAddress getFinalAddressOfBasicBlock(llvm::BasicBlock *BB) { auto [End, Size] = getPC(BB->getTerminator()); return End + Size; } UpcastablePointer FunctionEntrypointAnalyzer::handleFunctionCallForCFG(llvm::CallInst *Call) { using namespace llvm; auto *CalleePC = Call->getArgOperand(1); MetaAddress AddressPC = MetaAddress::invalid(); auto It = Binary->Functions.end(); StringRef SymbolName; // Direct or indirect call? if (isa(CalleePC)) { AddressPC = MetaAddress::fromConstant(CalleePC); It = Binary->Functions.find(AddressPC); BasicBlock *OriginalEntry = GCBI->getBlockAt(AddressPC); SymbolName = getDynamicSymbol(OriginalEntry); } MetaAddress Destination; efa::FunctionEdgeType::Values Type; const auto IDF = Binary->ImportedDynamicFunctions; bool IsDynamicCall = (not SymbolName.empty() and IDF.count(SymbolName.str()) != 0); if (It != Binary->Functions.end()) { // The function exist within the model Destination = AddressPC; Type = efa::FunctionEdgeType::FunctionCall; } else if (IsDynamicCall) { // The function exist within the imported dynamic functions Destination = MetaAddress::invalid(); Type = efa::FunctionEdgeType::FunctionCall; } else { // The function is an indirect call, we will forge a new prototype later in // `finalizeModel`. Destination = MetaAddress::invalid(); Type = efa::FunctionEdgeType::IndirectCall; } auto Edge = makeEdge(Destination, Type); auto DestTy = Oracle.getFunctionType(Destination); if (DestTy == FunctionTypeValue::NoReturn) { auto *CE = cast(Edge.get()); CE->Attributes.insert(model::FunctionAttribute::NoReturn); } if (IsDynamicCall) { auto *CE = cast(Edge.get()); CE->DynamicFunction = SymbolName.str(); } return Edge; } SortedVector FunctionEntrypointAnalyzer::collectDirectCFG(OutlinedFunction *F) { using namespace llvm; SortedVector CFG; for (BasicBlock &BB : *F->F) { if (GCBI::isJumpTarget(&BB)) { MetaAddress Start = getBasicBlockPC(&BB); efa::BasicBlock Block{ Start }; Block.End = getFinalAddressOfBasicBlock(&BB); OnceQueue Queue; Queue.insert(&BB); // A JT with no successors? if (isa(BB.getTerminator())) { auto Type = efa::FunctionEdgeType::Unreachable; Block.Successors.insert(makeEdge(MetaAddress::invalid(), Type)); } while (!Queue.empty()) { BasicBlock *Current = Queue.pop(); MetaAddress CurrentBlockEnd = getFinalAddressOfBasicBlock(Current); if (CurrentBlockEnd > Block.End) Block.End = CurrentBlockEnd; for (BasicBlock *Succ : successors(Current)) { if (GCBI::isJumpTarget(Succ)) { MetaAddress Destination = getBasicBlockPC(Succ); auto Edge = makeEdge(Destination, efa::FunctionEdgeType::DirectBranch); Block.Successors.insert(Edge); } else if (F->UnexpectedPCCloned == Succ && succ_size(Current) == 1) { // Need to create an edge only when `unexpectedpc` is the unique // successor of the current basic block. auto Edge = makeEdge(MetaAddress::invalid(), efa::FunctionEdgeType::LongJmp); Block.Successors.insert(Edge); } else { Instruction *I = &(*Succ->begin()); if (isa(I)) { // Did we meet the end of the cloned function? Do nothing revng_assert(Succ->getInstList().size() == 1); } else if (auto *Call = getCallTo(I, PreHookMarker.F)) { // Handle edge for direct function calls, dynamic function calls // as well as indirect ones. auto Edge = handleFunctionCallForCFG(Call); Block.Successors.insert(Edge); } else if (auto *Call = getCallTo(I, "function_call")) { // Handle edge for fake function calls. The marker `function_call` // has been left to signal the presence of an edge of type // FakeFunctionCall (still used in IsolateFunction). It can be // safely erased now. Call->eraseFromParent(); auto Destination = getBasicBlockPC(Succ); auto Edge = makeEdge(Destination, efa::FunctionEdgeType::FakeFunctionCall); Block.Successors.insert(Edge); } else { // Not one of the cases above? Enqueue the successor basic block. Queue.insert(Succ); } } } } CFG.insert(Block); } } return CFG; } void FEA::initMarkersForABI(OutlinedFunction *OutlinedFunction, SmallVectorImpl &SV, llvm::IRBuilder<> &IRB) { using namespace llvm; StructType *MetaAddressTy = MetaAddress::getStruct(&M); SmallVector IndirectBranchPredecessors; if (OutlinedFunction->AnyPCCloned) { for (auto *Pred : predecessors(OutlinedFunction->AnyPCCloned)) { auto *Term = Pred->getTerminator(); IndirectBranchPredecessors.emplace_back(Term); } } // Initialize ret-hook marker (needed for the ABI analyses on return values) // and fix pre-hook marker upon encountering a jump to `anypc`. Since we don't // know in advance whether it will be classified as a return or indirect tail // call, ABIAnalyses (e.g., RAOFC) need to run on this potential call-site as // well. The results will not be merged eventually, if the indirect jump turns // out to be a proper return. for (auto *Term : IndirectBranchPredecessors) { auto *BB = Term->getParent(); MetaAddress IndirectRetBBAddress = GCBI->getJumpTarget(BB); revng_assert(IndirectRetBBAddress.isValid()); auto *Split = BB->splitBasicBlock(Term, BB->getName() + Twine("_anypc")); auto *JumpToAnyPC = Split->getTerminator(); revng_assert(isa(JumpToAnyPC)); IRB.SetInsertPoint(JumpToAnyPC); IRB.CreateCall(PreHookMarker.F, { IndirectRetBBAddress.toConstant(MetaAddressTy), MetaAddress::invalid().toConstant(MetaAddressTy) }); IRB.CreateCall(RetHookMarker.F, { IndirectRetBBAddress.toConstant(MetaAddressTy), MetaAddress::invalid().toConstant(MetaAddressTy) }); SV.emplace_back(JumpToAnyPC); } } std::set FunctionEntrypointAnalyzer::findWrittenRegisters(llvm::Function *F) { using namespace llvm; std::set 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; } void FEA::createIBIMarker(OutlinedFunction *OutlinedFunction, SmallVectorImpl &SV, llvm::IRBuilder<> &IRB) { using namespace llvm; StructType *MetaAddressTy = MetaAddress::getStruct(&M); IRB.SetInsertPoint(&OutlinedFunction->F->getEntryBlock().front()); auto *IntPtrTy = GCBI->spReg()->getType(); auto *IntTy = GCBI->spReg()->getType()->getElementType(); // At the entry of the function, load the initial value of stack pointer, // program counter and ABI registers used within this function. auto *SPI = IRB.CreateLoad(GCBI->spReg()); auto *SPPtr = IRB.CreateIntToPtr(SPI, IntPtrTy); Value *RA = IRB.CreateLoad(GCBI->raReg() ? GCBI->raReg() : SPPtr); auto ToLLVMArchitecture = model::Architecture::toLLVMArchitecture; auto LLVMArchitecture = ToLLVMArchitecture(Binary->Architecture); std::array DissectedPC = PCH->dissectJumpablePC(IRB, RA, LLVMArchitecture); auto *PCI = MetaAddress::composeIntegerPC(IRB, DissectedPC[0], DissectedPC[1], DissectedPC[2], DissectedPC[3]); SmallVector CSVI; Type *IsRetTy = Type::getInt128Ty(Context); SmallVector ArgTypes = { IsRetTy, IntTy, MetaAddressTy }; for (auto *CSR : ABICSVs) { auto *V = IRB.CreateLoad(CSR); CSVI.emplace_back(V); ArgTypes.emplace_back(IntTy); } auto *FTy = llvm::FunctionType::get(IntTy, ArgTypes, false); OutlinedFunction ->IndirectBranchInfoMarker = Function::Create(FTy, GlobalValue::ExternalLinkage, "indirect_branch_info", M); OutlinedFunction->IndirectBranchInfoMarker->addFnAttr(Attribute::NoUnwind); OutlinedFunction->IndirectBranchInfoMarker->addFnAttr(Attribute::NoReturn); // When an indirect jump is encountered (possible exit point), a dedicated // basic block is created, and the values of the stack pointer, program // counter and ABI registers are loaded. for (auto *Term : SV) { auto *IBIBlock = BasicBlock::Create(Context, Term->getParent()->getName() + Twine("_indirect_branch_info"), OutlinedFunction->F, nullptr); Term->replaceUsesOfWith(OutlinedFunction->AnyPCCloned, IBIBlock); IRB.SetInsertPoint(IBIBlock); auto *PCE = PCH->composeIntegerPC(IRB); SmallVector CSVE; for (auto *CSR : ABICSVs) { auto *V = IRB.CreateLoad(CSR); CSVE.emplace_back(V); } auto *SPE = IRB.CreateLoad(GCBI->spReg()); // Compute the difference between the program counter at entry and exit // function. Should it turn out to be zero, the function jumps to its return // address. auto *JumpsToReturnAddress = IRB.CreateSub(PCE, PCI); // Compute the difference between the stack pointer values to evaluate the // stack height. Functions leaving the stack pointer higher than it was at // function entry (i.e., in an irregular state) will be marked as fake // functions. auto *StackPointerDifference = IRB.CreateSub(SPE, SPI); // Save the MetaAddress of the final jump target auto NewPCJT = GCBI->getJumpTarget(Term->getParent()); revng_assert(NewPCJT.isValid()); SmallVector ArgValues = { JumpsToReturnAddress, StackPointerDifference, NewPCJT.toConstant(MetaAddressTy) }; // Compute the difference between the initial and final values of the CSV // ABI registers. Should it turn out to be zero, the CSV is preserved across // the function call (callee-saved). for (const auto &[Initial, End] : zip(CSVI, CSVE)) { auto *ABIRegistersDifference = IRB.CreateSub(Initial, End); ArgValues.emplace_back(ABIRegistersDifference); } // Install the `indirect_branch_info` call IRB.CreateCall(OutlinedFunction->IndirectBranchInfoMarker, ArgValues); IRB.CreateUnreachable(); } } void FEA::opaqueBranchConditions(llvm::Function *F, llvm::IRBuilder<> &IRB) { using namespace llvm; for (auto &BB : *F) { auto *Term = BB.getTerminator(); if ((isa(Term) && cast(Term)->isConditional()) || isa(Term)) { Value *Condition = isa(Term) ? cast(Term)->getCondition() : cast(Term)->getCondition(); OpaqueBranchConditionsPool.addFnAttribute(Attribute::NoUnwind); OpaqueBranchConditionsPool.addFnAttribute(Attribute::ReadOnly); OpaqueBranchConditionsPool.addFnAttribute(Attribute::WillReturn); auto *FTy = llvm::FunctionType::get(Condition->getType(), { Condition->getType() }, false); auto *OpaqueTrueCallee = OpaqueBranchConditionsPool.get(FTy, FTy, "opaque_true"); IRB.SetInsertPoint(Term); auto *RetVal = IRB.CreateCall(OpaqueTrueCallee, { Condition }); if (isa(Term)) cast(Term)->setCondition(RetVal); else cast(Term)->setCondition(RetVal); } } } void FEA::materializePCValues(llvm::Function *F, llvm::IRBuilder<> &IRB) { using namespace llvm; for (auto &BB : *F) { for (auto &I : BB) { if (auto *Call = getCallTo(&I, "newpc")) { MetaAddress NewPC = GCBI::getPCFromNewPC(Call); IRB.SetInsertPoint(Call); PCH->setPC(IRB, NewPC); } } } } template struct TemporaryOption { public: TemporaryOption(const char *Name, const T &Value) : Name(Name), Options(llvm::cl::getRegisteredOptions()) { OldValue = Opt(Options, Name)->getValue(); Opt(Options, Name)->setInitialValue(Value); } ~TemporaryOption() { Opt(Options, Name)->setInitialValue(OldValue); } private: T OldValue; const char *Name; llvm::StringMap &Options; static constexpr const auto &Opt = getOption; }; void FunctionEntrypointAnalyzer::runOptimizationPipeline(llvm::Function *F) { using namespace llvm; // Some LLVM passes used later in the pipeline scan for cut-offs, meaning that // further computation may not be done when they are reached; making some // optimizations opportunities missed. Hence, we set the involved thresholds // (e.g., the maximum value that MemorySSA uses to take into account // stores/phis) to have initial unbounded value. static constexpr const char *MemSSALimit = "memssa-check-limit"; static constexpr const char *MemDepBlockLimit = "memdep-block-scan-limit"; using TemporaryUOption = TemporaryOption; TemporaryUOption MemSSALimitOption(MemSSALimit, UINT_MAX); TemporaryUOption MemDepBlockLimitOption(MemDepBlockLimit, UINT_MAX); // TODO: break it down in the future, and check if some passes can be dropped { FunctionPassManager FPM; // First stage: simplify the IR, promote the CSVs to local variables, // compute subexpressions elimination and resolve redundant expressions in // order to compute the stack height. FPM.addPass(RemoveNewPCCallsPass()); FPM.addPass(RemoveHelperCallsPass()); FPM.addPass(PromoteGlobalToLocalPass()); FPM.addPass(SimplifyCFGPass()); FPM.addPass(SROA()); FPM.addPass(EarlyCSEPass(true)); FPM.addPass(JumpThreadingPass()); FPM.addPass(UnreachableBlockElimPass()); FPM.addPass(InstCombinePass(true)); FPM.addPass(EarlyCSEPass(true)); FPM.addPass(SimplifyCFGPass()); FPM.addPass(MergedLoadStoreMotionPass()); FPM.addPass(GVN()); // Second stage: add alias analysis info and canonicalize `i2p` + `add` into // `getelementptr` instructions. Since the IR may change remarkably, another // round of passes is necessary to take more optimization opportunities. FPM.addPass(SegregateDirectStackAccessesPass()); FPM.addPass(EarlyCSEPass(true)); FPM.addPass(InstCombinePass(true)); FPM.addPass(GVN()); // Third stage: if enabled, serialize the results and dump the functions on // disk with the alias information included as comments. if (IndirectBranchInfoSummaryPath.getNumOccurrences() == 1) FPM.addPass(IndirectBranchInfoPrinterPass(*OutputIBI)); if (AAWriterPath.getNumOccurrences() == 1) FPM.addPass(AAWriterPass(*OutputAAWriter)); ModuleAnalysisManager MAM; FunctionAnalysisManager FAM; FAM.registerPass([] { AAManager AA; AA.registerFunctionAnalysis(); AA.registerFunctionAnalysis(); return AA; }); FAM.registerPass([this] { using LMA = LoadModelAnalysis; return LMA::fromModelWrapper(Binary); }); FAM.registerPass([&] { return GeneratedCodeBasicInfoAnalysis(); }); FAM.registerPass([&] { return ModuleAnalysisManagerFunctionProxy(MAM); }); PassBuilder PB; PB.registerFunctionAnalyses(FAM); PB.registerModuleAnalyses(MAM); FPM.run(*F, FAM); } } llvm::Function * FunctionEntrypointAnalyzer::createFakeFunction(llvm::BasicBlock *Entry) { using namespace llvm; // Recreate outlined function OutlinedFunction FakeFunction = outlineFunction(Entry); // Adjust `anypc` and `unexpectedpc` BBs of the fake function revng_assert(FakeFunction.AnyPCCloned != nullptr); // Fake functions must have one and only one broken return revng_assert(FakeFunction.AnyPCCloned->hasNPredecessors(1)); // Replace the broken return with a `ret` auto *Br = FakeFunction.AnyPCCloned->getUniquePredecessor()->getTerminator(); auto *Ret = ReturnInst::Create(Context); ReplaceInstWithInst(Br, Ret); if (FakeFunction.UnexpectedPCCloned != nullptr) { CallInst::Create(UnexpectedPCMarker.F, "", FakeFunction.UnexpectedPCCloned->getTerminator()); } return FakeFunction.extractFunction(); } void FunctionEntrypointAnalyzer::recoverCFG() { for (const auto &Function : Binary->Functions) { // No CFG will be recovered for `Fake` or `Invalid` functions if (Function.Type == FunctionTypeValue::Invalid || Function.Type == FunctionTypeValue::Fake) continue; auto *Entry = GCBI->getBlockAt(Function.Entry); // Recover the control-flow graph of the function auto &Summary = Oracle.at(Function.Entry); Summary.CFG = std::move(analyze(Entry, false).CFG); } } FunctionSummary FunctionEntrypointAnalyzer::analyze(BasicBlock *Entry, bool ShouldAnalyzeABI) { using namespace llvm; using namespace ABIAnalyses; IRBuilder<> Builder(M.getContext()); ABIAnalysesResults ABIResults; SmallVector BranchesForIBI; // Detect function boundaries struct OutlinedFunction OutlinedFunction = outlineFunction(Entry); // Recover the control-flow graph of the function auto CFG = collectDirectCFG(&OutlinedFunction); // Initalize markers for ABI analyses and set up the branches on which // `indirect_branch_info` will be installed. initMarkersForABI(&OutlinedFunction, BranchesForIBI, Builder); // Find registers that may be target of at least one store. This helps // refine the final results. auto WrittenRegisters = findWrittenRegisters(OutlinedFunction.F); if (ShouldAnalyzeABI) { // Run ABI-independent data-flow analyses ABIResults = analyzeOutlinedFunction(OutlinedFunction.F, *GCBI, PreHookMarker.F, PostHookMarker.F, RetHookMarker.F); } // Recompute the DomTree for the current outlined function due to split // basic blocks. GCBI->purgeDomTree(OutlinedFunction.F); // The analysis aims at identifying the callee-saved registers of a function // and establishing if a function returns properly, i.e., it jumps to the // return address (regular function). In order to achieve this, the IR is // crafted by loading the program counter, the stack pointer, as well as the // ABI registers CSVs respectively at function prologue / epilogue. When the // subtraction between their entry and end values is found to be zero (after // running an LLVM optimization pipeline), we may infer if the function // returns correctly, the stack is left unanaltered, etc. Hence, upon every // original indirect jump (candidate exit point), a marker of this kind is // installed: // // jumps to RA, SP, rax, rbx, rbp // call i64 @indirect_branch_info(i128 0, i64 8, i64 %8, i64 0, i64 0) // // Here, subsequently the opt pipeline computation, we may tell that the // function jumps to its return address (thus, it is not a longjmp / tail // call), `rax` register has been clobbered by the callee, whereas `rbx` and // `rbp` are callee-saved registers. createIBIMarker(&OutlinedFunction, BranchesForIBI, Builder); // Prevent DCE by making branch conditions opaque opaqueBranchConditions(OutlinedFunction.F, Builder); // Store the values that build up the program counter in order to have them // constant-folded away by the optimization pipeline. materializePCValues(OutlinedFunction.F, Builder); // Execute the optimization pipeline over the outlined function runOptimizationPipeline(OutlinedFunction.F); // Squeeze out the results obtained from the optimization passes auto FunctionInfo = milkInfo(&OutlinedFunction, CFG, ABIResults, WrittenRegisters, ShouldAnalyzeABI); // Does the outlined function basically represent a function prologue? If so, // the function is said to be fake, and a copy of the unoptimized outlined // function is returned. When analyzing the caller, this function will be // inlined in its call-site. if (FunctionInfo.Type == FunctionTypeValue::Fake) FunctionInfo.FakeFunction = createFakeFunction(Entry); // Reset the DomTree for the current outlined function GCBI->purgeDomTree(OutlinedFunction.F); return FunctionInfo; } static void suppressCSAndSPRegisters(ABIAnalyses::ABIAnalysesResults &ABIResults, const std::set &CalleeSavedRegs) { using RegisterState = abi::RegisterState::Values; // Suppress from arguments for (const auto &Reg : CalleeSavedRegs) { auto It = ABIResults.ArgumentsRegisters.find(Reg); if (It != ABIResults.ArgumentsRegisters.end()) It->second = RegisterState::No; } // Suppress from return values for (const auto &[K, _] : ABIResults.ReturnValuesRegisters) { for (const auto &Reg : CalleeSavedRegs) { auto It = ABIResults.ReturnValuesRegisters[K].find(Reg); if (It != ABIResults.ReturnValuesRegisters[K].end()) It->second = RegisterState::No; } } // Suppress from call-sites for (const auto &[K, _] : ABIResults.CallSites) { for (const auto &Reg : CalleeSavedRegs) { if (ABIResults.CallSites[K].ArgumentsRegisters.count(Reg) != 0) ABIResults.CallSites[K].ArgumentsRegisters[Reg] = RegisterState::No; if (ABIResults.CallSites[K].ReturnValuesRegisters.count(Reg) != 0) ABIResults.CallSites[K].ReturnValuesRegisters[Reg] = RegisterState::No; } } } static void discardBrokenReturns(ABIAnalyses::ABIAnalysesResults &ABIResults, const auto &IBIResult) { for (const auto &[CI, EdgeType] : IBIResult) { if (EdgeType != FunctionEdgeTypeValue::Return && EdgeType != FunctionEdgeTypeValue::IndirectTailCall) { auto PC = MetaAddress::fromConstant(CI->getOperand(2)); auto It = ABIResults.ReturnValuesRegisters.find(PC); if (It != ABIResults.ReturnValuesRegisters.end()) ABIResults.ReturnValuesRegisters.erase(PC); } } } static std::set intersect(const std::set &First, const std::set &Last) { std::set Output; std::set_intersection(First.begin(), First.end(), Last.begin(), Last.end(), std::inserter(Output, Output.begin())); return Output; } FunctionSummary FEA::milkInfo(OutlinedFunction *OutlinedFunction, SortedVector &CFG, ABIAnalyses::ABIAnalysesResults &ABIResults, const std::set &WrittenRegisters, bool ShouldAnalyzeABI) { using namespace llvm; SmallVector, 4> MaybeReturns; SmallVector, 4> NotReturns; SmallVector, 4> IBIResult; std::set CalleeSavedRegs; std::set ClobberedRegs(ABICSVs.begin(), ABICSVs.end()); for (CallBase *CI : callers(OutlinedFunction->IndirectBranchInfoMarker)) { if (CI->getParent()->getParent() == OutlinedFunction->F) { bool JumpsToReturnAddress = false; auto MayJumpToReturnAddress = dyn_cast(CI->getArgOperand(0)); if (MayJumpToReturnAddress) JumpsToReturnAddress = MayJumpToReturnAddress->getSExtValue() == 0; auto *StackPointerOffset = dyn_cast(CI->getArgOperand(1)); if (StackPointerOffset) { int64_t FSO = StackPointerOffset->getSExtValue(); if (JumpsToReturnAddress) { if (FSO >= 0) MaybeReturns.emplace_back(CI, FSO); else IBIResult.emplace_back(CI, FunctionEdgeTypeValue::BrokenReturn); } else { NotReturns.emplace_back(CI, FSO); } } else { if (JumpsToReturnAddress) IBIResult.emplace_back(CI, FunctionEdgeTypeValue::LongJmp); } } } // Elect a final stack offset auto WinFSO = electFSO(MaybeReturns); // Did we find at least a valid return instruction? for (const auto &[CI, FSO] : MaybeReturns) { if (FSO == *WinFSO) { IBIResult.emplace_back(CI, FunctionEdgeTypeValue::Return); unsigned ArgumentsCount = CI->getNumArgOperands(); if (ArgumentsCount > 3) { for (unsigned Idx = 3; Idx < ArgumentsCount; ++Idx) { auto *Register = dyn_cast(CI->getArgOperand(Idx)); if (Register && Register->getZExtValue() == 0) CalleeSavedRegs.insert(ABICSVs[Idx - 3]); } } } else { IBIResult.emplace_back(CI, FunctionEdgeTypeValue::BrokenReturn); } } // Neither a return nor a broken return? Re-elect a FSO taking into account no // returns indirect jumps only. if (!WinFSO.has_value()) WinFSO = electFSO(NotReturns); for (CallBase *CI : callers(OutlinedFunction->IndirectBranchInfoMarker)) { if (CI->getParent()->getParent() == OutlinedFunction->F) { auto MayJumpToReturnAddress = dyn_cast(CI->getArgOperand(0)); if (MayJumpToReturnAddress && MayJumpToReturnAddress->getSExtValue() == 0) continue; // We have an indirect jump and we classify it depending on the status of // the stack pointer. auto *StackOffset = dyn_cast(CI->getArgOperand(1)); if (WinFSO.has_value() && StackOffset != nullptr && StackOffset->getSExtValue() == *WinFSO) IBIResult.emplace_back(CI, FunctionEdgeTypeValue::IndirectTailCall); else IBIResult.emplace_back(CI, FunctionEdgeTypeValue::LongJmp); } } bool FoundReturn = false; bool FoundBrokenReturn = false; int BrokenReturnCount = 0, NoReturnCount = 0; for (const auto &[CI, EdgeType] : IBIResult) { if (EdgeType == FunctionEdgeTypeValue::Return) { FoundReturn = true; } else if (EdgeType == FunctionEdgeTypeValue::BrokenReturn) { FoundBrokenReturn = true; BrokenReturnCount++; } else { NoReturnCount++; } } // Function is elected fake if there is one and only one broken return FunctionTypeValue Type; if (FoundReturn) { Type = FunctionTypeValue::Regular; } else if (FoundBrokenReturn && BrokenReturnCount == 1 && NoReturnCount == 0) { Type = FunctionTypeValue::Fake; } else { Type = FunctionTypeValue::NoReturn; } // Retrieve the clobbered registers std::erase_if(ClobberedRegs, [&](const auto &E) { return CalleeSavedRegs.count(E) != 0; }); // Finalize CFG for the model for (const auto &[CI, EdgeType] : IBIResult) { auto PC = MetaAddress::fromConstant(CI->getArgOperand(2)); efa::BasicBlock &Block = CFG.at(PC); Block.Successors.insert(makeEdge(MetaAddress::invalid(), EdgeType)); } // Empty CFG if function is fake if (Type == FunctionTypeValue::Fake) CFG.clear(); if (ShouldAnalyzeABI) { // We say that a register is callee-saved when, besides being preserved by // the callee, there is at least a write onto this register. auto ActualCalleeSavedRegs = intersect(CalleeSavedRegs, WrittenRegisters); // Union between effective callee-saved registers and SP ActualCalleeSavedRegs.insert(GCBI->spReg()); // Refine ABI analyses results by suppressing callee-saved and stack pointer // registers. suppressCSAndSPRegisters(ABIResults, ActualCalleeSavedRegs); // ABI analyses run from all the `indirect_branch_info` (i.e., all candidate // returns). We are going to merge the results only from those return points // that have been classified as proper return (i.e., no broken return). discardBrokenReturns(ABIResults, IBIResult); // Merge return values registers ABIAnalyses::finalizeReturnValues(ABIResults); ABIResults.dump(EarlyFunctionAnalysisLog); } return FunctionSummary(Type, std::move(ClobberedRegs), std::move(ABIResults), std::move(CFG), WinFSO, nullptr); } void FunctionEntrypointAnalyzer::integrateFunctionCallee(llvm::BasicBlock *BB, MetaAddress Next) { using namespace llvm; // If the basic block originally had a call-site, the function call is // replaced with 1) hooks that delimit the space of the ABI analyses' // traversals and 2) a summary of the registers clobbered by that function. auto *Term = BB->getTerminator(); auto *Call = getFunctionCall(Term); // What is the function type of the callee? FunctionTypeValue Type = Oracle.getFunctionType(Next); switch (Type) { case FunctionTypeValue::Regular: case FunctionTypeValue::NoReturn: { // Extract MetaAddress of JT of the call-site auto CallSiteJT = GCBI->getJumpTarget(BB); revng_assert(CallSiteJT.isValid()); // What are the registers clobbered by the callee? const auto &ClobberedRegisters = Oracle.getRegistersClobbered(Next); // Different insert point depending on the callee type IRBuilder<> Builder(M.getContext()); if (Type == FunctionTypeValue::Regular) { Builder.SetInsertPoint(Term); } else { auto *AbortCall = dyn_cast(Term->getPrevNode()); revng_assert(AbortCall != nullptr && AbortCall->getCalledFunction() == M.getFunction("abort")); Builder.SetInsertPoint(AbortCall); } // Mark end of basic block with a pre-hook call StructType *MetaAddressTy = MetaAddress::getStruct(&M); SmallVector Args = { CallSiteJT.toConstant(MetaAddressTy), Next.toConstant(MetaAddressTy) }; auto *Last = Builder.CreateCall(PreHookMarker.F, Args); // Prevent the store instructions from being optimized out by storing // the rval of a call to an opaque function into the clobbered registers. RegistersClobberedPool.addFnAttribute(Attribute::ReadOnly); RegistersClobberedPool.addFnAttribute(Attribute::NoUnwind); RegistersClobberedPool.addFnAttribute(Attribute::WillReturn); for (GlobalVariable *Register : ClobberedRegisters) { auto *CSVTy = Register->getType()->getPointerElementType(); auto Name = ("registers_clobbered_" + Twine(Register->getName())).str(); auto *OpaqueRegistersClobberedCallee = RegistersClobberedPool .get(Register->getName(), CSVTy, {}, Name); Builder.CreateStore(Builder.CreateCall(OpaqueRegistersClobberedCallee), Register); } // Adjust back the stack pointer if (auto MaybeFSO = Oracle.getElectedFSO(Next); MaybeFSO) { GlobalVariable *SPCSV = GCBI->spReg(); auto *StackPointer = Builder.CreateLoad(SPCSV); Value *Offset = ConstantInt::get(StackPointer->getPointerOperandType() ->getPointerElementType(), *MaybeFSO); auto *AdjustedStackPointer = Builder.CreateAdd(StackPointer, Offset); Builder.CreateStore(AdjustedStackPointer, SPCSV); } // Mark end of basic block with a post-hook call Builder.CreateCall(PostHookMarker.F, Args); BB->splitBasicBlock(Last->getPrevNode(), BB->getName() + Twine("__summary")); // Erase the `function_call` marker unless fake Call->eraseFromParent(); break; } case FunctionTypeValue::Fake: { // Get the fake function by its entry basic block Function *FakeFunction = Oracle.getFakeFunction(Next); // If fake, it must have been already analyzed revng_assert(FakeFunction != nullptr); // If possible, inline the fake function auto *CI = CallInst::Create(FakeFunction, "", Term); InlineFunctionInfo IFI; bool Status = InlineFunction(*CI, IFI, nullptr, true).isSuccess(); revng_log(EarlyFunctionAnalysisLog, "Has callee " << FakeFunction->getName() << "been inlined? " << Status); break; } default: revng_abort(); } } OutlinedFunction FunctionEntrypointAnalyzer::outlineFunction(llvm::BasicBlock *Entry) { using namespace llvm; Function *Root = Entry->getParent(); OutlinedFunction OutlinedFunction; OnceQueue Queue; std::vector BlocksToClone; Queue.insert(Entry); // Collect list of blocks to clone while (!Queue.empty()) { BasicBlock *Current = Queue.pop(); BlocksToClone.emplace_back(Current); if (isFunctionCall(Current)) { auto *Successor = getFallthrough(Current); MetaAddress PCCallee = MetaAddress::invalid(); if (auto *Next = getFunctionCallCallee(Current)) PCCallee = getBasicBlockPC(Next); if (Oracle.getFunctionType(PCCallee) != FunctionTypeValue::NoReturn) Queue.insert(Successor); } else { for (auto *Successor : successors(Current)) { if (not isPartOfRootDispatcher(Successor)) Queue.insert(Successor); } } } // Create a copy of all the basic blocks to outline in `root` ValueToValueMapTy VMap; SmallVector BlocksToExtract; auto *AnyPCBB = GCBI->anyPC(); auto *UnexpectedPCBB = GCBI->unexpectedPC(); for (const auto &BB : BlocksToClone) { BasicBlock *Cloned = CloneBasicBlock(BB, VMap, Twine("_cloned"), Root); VMap[BB] = Cloned; BlocksToExtract.emplace_back(Cloned); } auto AnyPCIt = VMap.find(AnyPCBB); if (AnyPCIt != VMap.end()) OutlinedFunction.AnyPCCloned = cast(AnyPCIt->second); auto UnexpPCIt = VMap.find(UnexpectedPCBB); if (UnexpPCIt != VMap.end()) OutlinedFunction.UnexpectedPCCloned = cast(UnexpPCIt->second); remapInstructionsInBlocks(BlocksToExtract, VMap); // Fix successor when encountering a call-site and fix fall-through in // presence of a noreturn function. std::map CallMap; for (const auto &BB : BlocksToExtract) { if (isFunctionCall(BB)) { auto *Term = BB->getTerminator(); CallInst *CI = getFunctionCall(Term); // If the function callee is null, we are dealing with an indirect call MetaAddress PCCallee = MetaAddress::invalid(); if (BasicBlock *Next = getFunctionCallCallee(Term)) PCCallee = getBasicBlockPC(Next); auto CalleeType = Oracle.getFunctionType(PCCallee); if (CalleeType != FunctionTypeValue::NoReturn) { auto *Br = BranchInst::Create(getFallthrough(Term)); ReplaceInstWithInst(Term, Br); } else if (CalleeType == FunctionTypeValue::NoReturn) { auto *Abort = CallInst::Create(M.getFunction("abort")); new UnreachableInst(Term->getContext(), BB); ReplaceInstWithInst(Term, Abort); } // To allow correct function extraction, there must not exist users of BBs // to be extracted, so we destroy the blockaddress of the fall-through BB // in the `function_call` marker. unsigned ArgNo = 0; PointerType *I8PtrTy = Type::getInt8PtrTy(M.getContext()); Constant *I8NullPtr = ConstantPointerNull::get(I8PtrTy); for (Value *Arg : CI->args()) { if (isa(Arg)) { CI->setArgOperand(ArgNo, I8NullPtr); if (Arg->use_empty()) cast(Arg)->destroyConstant(); } ++ArgNo; } CallMap.insert({ CI, PCCallee }); } } // Extract outlined function OutlinedFunction.F = CodeExtractor(BlocksToExtract).extractCodeRegion(CEAC); revng_assert(OutlinedFunction.F != nullptr); revng_assert(OutlinedFunction.F->arg_size() == 0); revng_assert(OutlinedFunction.F->getReturnType()->isVoidTy()); revng_assert(OutlinedFunction.F->hasOneUser()); // Remove the only user (call to the outlined function) in `root` auto It = OutlinedFunction.F->user_begin(); cast(*It)->getParent()->eraseFromParent(); // Integrate function callee for (auto &BB : *OutlinedFunction.F) { if (isFunctionCall(&BB)) { auto *Term = BB.getTerminator(); MetaAddress CalleePC = CallMap.at(getFunctionCall(Term)); integrateFunctionCallee(&BB, CalleePC); } } // TODO: fix `unexpectedpc` of the fake callee for (auto &I : instructions(OutlinedFunction.F)) { if (CallInst *Call = getCallTo(&I, UnexpectedPCMarker.F)) { // TODO: can `unexpectedpc` not exist in the caller? revng_assert(OutlinedFunction.UnexpectedPCCloned != nullptr); auto *Br = BranchInst::Create(OutlinedFunction.UnexpectedPCCloned); ReplaceInstWithInst(I.getParent()->getTerminator(), Br); Call->eraseFromParent(); break; } } // Make sure `newpc` is still the first instruction when we have a jump target // (if not, create a new dedicated basic block); that otherwise would break // further assumptions when using `getBasicBlockPC` for model population. BasicBlock *Split = nullptr; Instruction *SplitPoint = nullptr; for (auto &BB : *OutlinedFunction.F) { if (Split == &BB) continue; Split = nullptr; for (auto &I : BB) { if (CallInst *Call = getCallTo(&I, "newpc")) { Value *IsJT = Call->getArgOperand(2); if (BB.getFirstNonPHI() != Call && getLimitedValue(IsJT) == 1) { if (isCallTo(Call->getPrevNode(), "function_call")) SplitPoint = Call->getPrevNode(); else SplitPoint = Call; Split = BB.splitBasicBlock(SplitPoint, BB.getName() + Twine("_jt")); break; } } } } return OutlinedFunction; } void FunctionEntrypointAnalyzer::runInterproceduralAnalysis() { while (!EntrypointsQueue->empty()) { BasicBlockNode *EntryNode = EntrypointsQueue->pop(); revng_log(EarlyFunctionAnalysisLog, "Analyzing Entry: " << EntryNode->BB->getName()); // Intraprocedural analysis FunctionSummary AnalysisResult = analyze(EntryNode->BB, true); bool Changed = Oracle.registerFunction(getBasicBlockPC(EntryNode->BB), std::move(AnalysisResult)); // If we got improved results for a function, we need to recompute its // callers, and if a caller turns out to be fake, the callers of the fake // function too. if (Changed) { BasicBlockQueue FakeFunctionWorklist; FakeFunctionWorklist.insert(EntryNode); while (!FakeFunctionWorklist.empty()) { BasicBlockNode *Node = FakeFunctionWorklist.pop(); for (auto *Caller : Node->predecessors()) { // Root node? if (Caller->BB == nullptr) break; if (!Oracle.isFakeFunction(getBasicBlockPC(Caller->BB))) EntrypointsQueue->insert(Caller); else FakeFunctionWorklist.insert(Caller); } } } } } template bool EarlyFunctionAnalysis::runOnModule(Module &M) { revng_log(PassesLog, "Starting EarlyFunctionAnalysis"); auto &GCBI = getAnalysis().getGCBI(); auto &LMP = getAnalysis().get(); TupleTree &Binary = LMP.getWriteableModel(); using BasicBlockToNodeMap = llvm::DenseMap; BasicBlockToNodeMap BasicBlockNodeMap; // Temporary worklist to collect the function entrypoints llvm::SmallVector Worklist; SmallCallGraph CG; // Create an over-approximated call graph for (const auto &Function : Binary->Functions) { auto *Entry = GCBI.getBlockAt(Function.Entry); BasicBlockNode Node{ Entry }; BasicBlockNode *GraphNode = CG.addNode(Node); BasicBlockNodeMap[Entry] = GraphNode; } for (const auto &Function : Binary->Functions) { llvm::SmallSet Visited; auto *Entry = GCBI.getBlockAt(Function.Entry); 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 (isFunctionCall(Current)) { // 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()) StartNode->addSuccessor(It->second); } BasicBlock *Next = getFallthrough(Current); if (!Visited.count(Next)) Worklist.push_back(Next); } for (BasicBlock *Successor : successors(Current)) { if (not isPartOfRootDispatcher(Successor) && !Visited.count(Successor)) 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 FakeNode{ nullptr }; BasicBlockNode *RootNode = CG.addNode(FakeNode); CG.setEntryNode(RootNode); for (const auto &[_, Node] : BasicBlockNodeMap) RootNode->addSuccessor(Node); // Create an over-approximated call graph of the program. A queue of all the // function entrypoints is maintained. BasicBlockQueue EntrypointsQueue; for (auto *Node : llvm::post_order(&CG)) { if (Node == RootNode) continue; // The intraprocedural analysis will be scheduled only for those functions // which have `Invalid` as type. auto &Function = Binary->Functions.at(getBasicBlockPC(Node->BB)); if (Function.Type == model::FunctionType::Invalid) EntrypointsQueue.insert(Node); } // Dump the call-graph, if requested std::unique_ptr OutputCG; if (CallGraphOutputPath.getNumOccurrences() == 1) { std::ifstream File(CallGraphOutputPath.c_str()); if (File.is_open()) { int Status = std::remove(CallGraphOutputPath.c_str()); revng_assert(Status == 0); } std::error_code EC; OutputCG = std::make_unique(CallGraphOutputPath, EC, llvm::sys::fs::OF_Append); revng_assert(!EC); llvm::WriteGraph(*OutputCG, &CG); } // Collect all the ABI registers, leave out the stack pointer for the moment. // We will include it back later when refining ABI results. std::vector ABICSVs; for (GlobalVariable *CSV : GCBI.abiRegisters()) if (CSV != nullptr && !(GCBI.isSPReg(CSV))) ABICSVs.emplace_back(CSV); // Default-constructed cache summary for indirect calls unsigned MinimalFSO; { using namespace model::Architecture; MinimalFSO = getMinimalFinalStackOffset(Binary->Architecture); } FunctionSummary DefaultSummary(model::FunctionType::Values::Regular, { ABICSVs.begin(), ABICSVs.end() }, ABIAnalyses::ABIAnalysesResults(), {}, MinimalFSO, nullptr); using FAR = FunctionAnalysisResults; FAR Properties(std::move(DefaultSummary)); // Instantiate a FunctionEntrypointAnalyzer object FEA Analyzer(M, &GCBI, ABICSVs, &EntrypointsQueue, Properties, Binary); // Prepopulate the cache with existing functions and dynamic functions from // model, and recreate fake functions Analyzer.importModel(); // EarlyFunctionAnalysis can be invoked with two option: via `--detect-abi` in // order to schedule a full ABI analysis for each function entry-point // detected, or via `--collect-cfg`, for control-flow graph recovery. In doing // so, the following property is obtained: `--detect-abi` writes the model but // not the IR, whereas `--collect-cfg` writes the IR but not the model. if (!ShouldAnalyzeABI) { // Recover the control-flow graph Analyzer.recoverCFG(); // Serialize function metadata, CFG included, to IR Analyzer.serializeFunctionMetadata(); } else { // Interprocedural analysis over the collected functions in post-order // traversal (leafs first). Analyzer.runInterproceduralAnalysis(); // Propagate results between call-sites and functions Analyzer.interproceduralPropagation(); // Commit the results onto the model. A non-const model is taken as argument // to be written. Analyzer.finalizeModel(Binary); } // Still OK? if (VerifyLog.isEnabled()) revng_assert(llvm::verifyModule(M, &llvm::dbgs()) == false); return false; } } // namespace EarlyFunctionAnalysis