//===-- sancov.cc --------------------------------------------===// // // The LLVM Compiler Infrastructure // // This file is distributed under the University of Illinois Open Source // License. See LICENSE.TXT for details. // //===----------------------------------------------------------------------===// // // This file is a command-line tool for reading and analyzing sanitizer // coverage. //===----------------------------------------------------------------------===// #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/Twine.h" #include "llvm/DebugInfo/Symbolize/Symbolize.h" #include "llvm/MC/MCAsmInfo.h" #include "llvm/MC/MCContext.h" #include "llvm/MC/MCDisassembler/MCDisassembler.h" #include "llvm/MC/MCInst.h" #include "llvm/MC/MCInstPrinter.h" #include "llvm/MC/MCInstrAnalysis.h" #include "llvm/MC/MCInstrInfo.h" #include "llvm/MC/MCObjectFileInfo.h" #include "llvm/MC/MCRegisterInfo.h" #include "llvm/MC/MCSubtargetInfo.h" #include "llvm/Object/Archive.h" #include "llvm/Object/Binary.h" #include "llvm/Object/ELFObjectFile.h" #include "llvm/Object/ObjectFile.h" #include "llvm/Support/Casting.h" #include "llvm/Support/CommandLine.h" #include "llvm/Support/Errc.h" #include "llvm/Support/ErrorOr.h" #include "llvm/Support/FileSystem.h" #include "llvm/Support/LineIterator.h" #include "llvm/Support/MD5.h" #include "llvm/Support/ManagedStatic.h" #include "llvm/Support/MemoryBuffer.h" #include "llvm/Support/Path.h" #include "llvm/Support/PrettyStackTrace.h" #include "llvm/Support/Regex.h" #include "llvm/Support/Signals.h" #include "llvm/Support/SpecialCaseList.h" #include "llvm/Support/TargetRegistry.h" #include "llvm/Support/TargetSelect.h" #include "llvm/Support/ToolOutputFile.h" #include "llvm/Support/raw_ostream.h" #include <algorithm> #include <set> #include <stdio.h> #include <string> #include <utility> #include <vector> using namespace llvm; namespace { // --------- COMMAND LINE FLAGS --------- enum ActionType { PrintAction, PrintCovPointsAction, CoveredFunctionsAction, NotCoveredFunctionsAction, HtmlReportAction, StatsAction }; cl::opt<ActionType> Action( cl::desc("Action (required)"), cl::Required, cl::values(clEnumValN(PrintAction, "print", "Print coverage addresses"), clEnumValN(PrintCovPointsAction, "print-coverage-pcs", "Print coverage instrumentation points addresses."), clEnumValN(CoveredFunctionsAction, "covered-functions", "Print all covered funcions."), clEnumValN(NotCoveredFunctionsAction, "not-covered-functions", "Print all not covered funcions."), clEnumValN(HtmlReportAction, "html-report", "Print HTML coverage report."), clEnumValN(StatsAction, "print-coverage-stats", "Print coverage statistics."), clEnumValEnd)); static cl::list<std::string> ClInputFiles(cl::Positional, cl::OneOrMore, cl::desc("(<binary file>|<.sancov file>)...")); static cl::opt<bool> ClDemangle("demangle", cl::init(true), cl::desc("Print demangled function name.")); static cl::opt<std::string> ClStripPathPrefix( "strip_path_prefix", cl::init(""), cl::desc("Strip this prefix from file paths in reports.")); static cl::opt<std::string> ClBlacklist("blacklist", cl::init(""), cl::desc("Blacklist file (sanitizer blacklist format).")); static cl::opt<bool> ClUseDefaultBlacklist( "use_default_blacklist", cl::init(true), cl::Hidden, cl::desc("Controls if default blacklist should be used.")); static const char *const DefaultBlacklistStr = "fun:__sanitizer_.*\n" "src:/usr/include/.*\n" "src:.*/libc\\+\\+/.*\n"; // --------- FORMAT SPECIFICATION --------- struct FileHeader { uint32_t Bitness; uint32_t Magic; }; static const uint32_t BinCoverageMagic = 0xC0BFFFFF; static const uint32_t Bitness32 = 0xFFFFFF32; static const uint32_t Bitness64 = 0xFFFFFF64; // --------- ERROR HANDLING --------- static void Fail(const llvm::Twine &E) { errs() << "Error: " << E << "\n"; exit(1); } static void FailIfError(std::error_code Error) { if (!Error) return; errs() << "Error: " << Error.message() << "(" << Error.value() << ")\n"; exit(1); } template <typename T> static void FailIfError(const ErrorOr<T> &E) { FailIfError(E.getError()); } static void FailIfError(Error Err) { if (Err) { logAllUnhandledErrors(std::move(Err), errs(), "Error: "); exit(1); } } template <typename T> static void FailIfError(Expected<T> &E) { FailIfError(E.takeError()); } static void FailIfNotEmpty(const llvm::Twine &E) { if (E.str().empty()) return; Fail(E); } template <typename T> static void FailIfEmpty(const std::unique_ptr<T> &Ptr, const std::string &Message) { if (Ptr.get()) return; Fail(Message); } // --------- // Produces std::map<K, std::vector<E>> grouping input // elements by FuncTy result. template <class RangeTy, class FuncTy> static inline auto group_by(const RangeTy &R, FuncTy F) -> std::map<typename std::decay<decltype(F(*R.begin()))>::type, std::vector<typename std::decay<decltype(*R.begin())>::type>> { std::map<typename std::decay<decltype(F(*R.begin()))>::type, std::vector<typename std::decay<decltype(*R.begin())>::type>> Result; for (const auto &E : R) { Result[F(E)].push_back(E); } return Result; } template <typename T> static void readInts(const char *Start, const char *End, std::set<uint64_t> *Ints) { const T *S = reinterpret_cast<const T *>(Start); const T *E = reinterpret_cast<const T *>(End); std::copy(S, E, std::inserter(*Ints, Ints->end())); } struct FileLoc { bool operator<(const FileLoc &RHS) const { return std::tie(FileName, Line) < std::tie(RHS.FileName, RHS.Line); } std::string FileName; uint32_t Line; }; struct FileFn { bool operator<(const FileFn &RHS) const { return std::tie(FileName, FunctionName) < std::tie(RHS.FileName, RHS.FunctionName); } std::string FileName; std::string FunctionName; }; struct FnLoc { bool operator<(const FnLoc &RHS) const { return std::tie(Loc, FunctionName) < std::tie(RHS.Loc, RHS.FunctionName); } FileLoc Loc; std::string FunctionName; }; std::string stripPathPrefix(std::string Path) { if (ClStripPathPrefix.empty()) return Path; size_t Pos = Path.find(ClStripPathPrefix); if (Pos == std::string::npos) return Path; return Path.substr(Pos + ClStripPathPrefix.size()); } static std::unique_ptr<symbolize::LLVMSymbolizer> createSymbolizer() { symbolize::LLVMSymbolizer::Options SymbolizerOptions; SymbolizerOptions.Demangle = ClDemangle; SymbolizerOptions.UseSymbolTable = true; return std::unique_ptr<symbolize::LLVMSymbolizer>( new symbolize::LLVMSymbolizer(SymbolizerOptions)); } // A DILineInfo with address. struct AddrInfo : public DILineInfo { uint64_t Addr; AddrInfo(const DILineInfo &DI, uint64_t Addr) : DILineInfo(DI), Addr(Addr) { FileName = normalizeFilename(FileName); } private: static std::string normalizeFilename(const std::string &FileName) { SmallString<256> S(FileName); sys::path::remove_dots(S, /* remove_dot_dot */ true); return S.str().str(); } }; class Blacklists { public: Blacklists() : DefaultBlacklist(createDefaultBlacklist()), UserBlacklist(createUserBlacklist()) {} // AddrInfo contains normalized filename. It is important to check it rather // than DILineInfo. bool isBlacklisted(const AddrInfo &AI) { if (DefaultBlacklist && DefaultBlacklist->inSection("fun", AI.FunctionName)) return true; if (DefaultBlacklist && DefaultBlacklist->inSection("src", AI.FileName)) return true; if (UserBlacklist && UserBlacklist->inSection("fun", AI.FunctionName)) return true; if (UserBlacklist && UserBlacklist->inSection("src", AI.FileName)) return true; return false; } private: static std::unique_ptr<SpecialCaseList> createDefaultBlacklist() { if (!ClUseDefaultBlacklist) return std::unique_ptr<SpecialCaseList>(); std::unique_ptr<MemoryBuffer> MB = MemoryBuffer::getMemBuffer(DefaultBlacklistStr); std::string Error; auto Blacklist = SpecialCaseList::create(MB.get(), Error); FailIfNotEmpty(Error); return Blacklist; } static std::unique_ptr<SpecialCaseList> createUserBlacklist() { if (ClBlacklist.empty()) return std::unique_ptr<SpecialCaseList>(); return SpecialCaseList::createOrDie({{ClBlacklist}}); } std::unique_ptr<SpecialCaseList> DefaultBlacklist; std::unique_ptr<SpecialCaseList> UserBlacklist; }; // Collect all debug info for given addresses. static std::vector<AddrInfo> getAddrInfo(const std::string &ObjectFile, const std::set<uint64_t> &Addrs, bool InlinedCode) { std::vector<AddrInfo> Result; auto Symbolizer(createSymbolizer()); Blacklists B; for (auto Addr : Addrs) { auto LineInfo = Symbolizer->symbolizeCode(ObjectFile, Addr); FailIfError(LineInfo); auto LineAddrInfo = AddrInfo(*LineInfo, Addr); if (B.isBlacklisted(LineAddrInfo)) continue; Result.push_back(LineAddrInfo); if (InlinedCode) { auto InliningInfo = Symbolizer->symbolizeInlinedCode(ObjectFile, Addr); FailIfError(InliningInfo); for (uint32_t I = 0; I < InliningInfo->getNumberOfFrames(); ++I) { auto FrameInfo = InliningInfo->getFrame(I); auto FrameAddrInfo = AddrInfo(FrameInfo, Addr); if (B.isBlacklisted(FrameAddrInfo)) continue; Result.push_back(FrameAddrInfo); } } } return Result; } // Locate __sanitizer_cov* function addresses that are used for coverage // reporting. static std::set<uint64_t> findSanitizerCovFunctions(const object::ObjectFile &O) { std::set<uint64_t> Result; for (const object::SymbolRef &Symbol : O.symbols()) { Expected<uint64_t> AddressOrErr = Symbol.getAddress(); FailIfError(errorToErrorCode(AddressOrErr.takeError())); Expected<StringRef> NameOrErr = Symbol.getName(); FailIfError(errorToErrorCode(NameOrErr.takeError())); StringRef Name = NameOrErr.get(); if (Name == "__sanitizer_cov" || Name == "__sanitizer_cov_with_check" || Name == "__sanitizer_cov_trace_func_enter") { if (!(Symbol.getFlags() & object::BasicSymbolRef::SF_Undefined)) Result.insert(AddressOrErr.get()); } } return Result; } // Locate addresses of all coverage points in a file. Coverage point // is defined as the 'address of instruction following __sanitizer_cov // call - 1'. static void getObjectCoveragePoints(const object::ObjectFile &O, std::set<uint64_t> *Addrs) { Triple TheTriple("unknown-unknown-unknown"); TheTriple.setArch(Triple::ArchType(O.getArch())); auto TripleName = TheTriple.getTriple(); std::string Error; const Target *TheTarget = TargetRegistry::lookupTarget(TripleName, Error); FailIfNotEmpty(Error); std::unique_ptr<const MCSubtargetInfo> STI( TheTarget->createMCSubtargetInfo(TripleName, "", "")); FailIfEmpty(STI, "no subtarget info for target " + TripleName); std::unique_ptr<const MCRegisterInfo> MRI( TheTarget->createMCRegInfo(TripleName)); FailIfEmpty(MRI, "no register info for target " + TripleName); std::unique_ptr<const MCAsmInfo> AsmInfo( TheTarget->createMCAsmInfo(*MRI, TripleName)); FailIfEmpty(AsmInfo, "no asm info for target " + TripleName); std::unique_ptr<const MCObjectFileInfo> MOFI(new MCObjectFileInfo); MCContext Ctx(AsmInfo.get(), MRI.get(), MOFI.get()); std::unique_ptr<MCDisassembler> DisAsm( TheTarget->createMCDisassembler(*STI, Ctx)); FailIfEmpty(DisAsm, "no disassembler info for target " + TripleName); std::unique_ptr<const MCInstrInfo> MII(TheTarget->createMCInstrInfo()); FailIfEmpty(MII, "no instruction info for target " + TripleName); std::unique_ptr<const MCInstrAnalysis> MIA( TheTarget->createMCInstrAnalysis(MII.get())); FailIfEmpty(MIA, "no instruction analysis info for target " + TripleName); auto SanCovAddrs = findSanitizerCovFunctions(O); if (SanCovAddrs.empty()) Fail("__sanitizer_cov* functions not found"); for (object::SectionRef Section : O.sections()) { if (Section.isVirtual() || !Section.isText()) // llvm-objdump does the same. continue; uint64_t SectionAddr = Section.getAddress(); uint64_t SectSize = Section.getSize(); if (!SectSize) continue; StringRef BytesStr; FailIfError(Section.getContents(BytesStr)); ArrayRef<uint8_t> Bytes(reinterpret_cast<const uint8_t *>(BytesStr.data()), BytesStr.size()); for (uint64_t Index = 0, Size = 0; Index < Section.getSize(); Index += Size) { MCInst Inst; if (!DisAsm->getInstruction(Inst, Size, Bytes.slice(Index), SectionAddr + Index, nulls(), nulls())) { if (Size == 0) Size = 1; continue; } uint64_t Addr = Index + SectionAddr; // Sanitizer coverage uses the address of the next instruction - 1. uint64_t CovPoint = Addr + Size - 1; uint64_t Target; if (MIA->isCall(Inst) && MIA->evaluateBranch(Inst, SectionAddr + Index, Size, Target) && SanCovAddrs.find(Target) != SanCovAddrs.end()) Addrs->insert(CovPoint); } } } static void visitObjectFiles(const object::Archive &A, function_ref<void(const object::ObjectFile &)> Fn) { Error Err; for (auto &C : A.children(Err)) { Expected<std::unique_ptr<object::Binary>> ChildOrErr = C.getAsBinary(); FailIfError(errorToErrorCode(ChildOrErr.takeError())); if (auto *O = dyn_cast<object::ObjectFile>(&*ChildOrErr.get())) Fn(*O); else FailIfError(object::object_error::invalid_file_type); } FailIfError(std::move(Err)); } static void visitObjectFiles(const std::string &FileName, function_ref<void(const object::ObjectFile &)> Fn) { Expected<object::OwningBinary<object::Binary>> BinaryOrErr = object::createBinary(FileName); if (!BinaryOrErr) FailIfError(errorToErrorCode(BinaryOrErr.takeError())); object::Binary &Binary = *BinaryOrErr.get().getBinary(); if (object::Archive *A = dyn_cast<object::Archive>(&Binary)) visitObjectFiles(*A, Fn); else if (object::ObjectFile *O = dyn_cast<object::ObjectFile>(&Binary)) Fn(*O); else FailIfError(object::object_error::invalid_file_type); } std::set<uint64_t> findSanitizerCovFunctions(const std::string &FileName) { std::set<uint64_t> Result; visitObjectFiles(FileName, [&](const object::ObjectFile &O) { auto Addrs = findSanitizerCovFunctions(O); Result.insert(Addrs.begin(), Addrs.end()); }); return Result; } // Locate addresses of all coverage points in a file. Coverage point // is defined as the 'address of instruction following __sanitizer_cov // call - 1'. std::set<uint64_t> getCoveragePoints(const std::string &FileName) { std::set<uint64_t> Result; visitObjectFiles(FileName, [&](const object::ObjectFile &O) { getObjectCoveragePoints(O, &Result); }); return Result; } static void printCovPoints(const std::string &ObjFile, raw_ostream &OS) { for (uint64_t Addr : getCoveragePoints(ObjFile)) { OS << "0x"; OS.write_hex(Addr); OS << "\n"; } } static std::string escapeHtml(const std::string &S) { std::string Result; Result.reserve(S.size()); for (char Ch : S) { switch (Ch) { case '&': Result.append("&"); break; case '\'': Result.append("'"); break; case '"': Result.append("""); break; case '<': Result.append("<"); break; case '>': Result.append(">"); break; default: Result.push_back(Ch); break; } } return Result; } // Adds leading zeroes wrapped in 'lz' style. // Leading zeroes help locate 000% coverage. static std::string formatHtmlPct(size_t Pct) { Pct = std::max(std::size_t{0}, std::min(std::size_t{100}, Pct)); std::string Num = std::to_string(Pct); std::string Zeroes(3 - Num.size(), '0'); if (!Zeroes.empty()) Zeroes = "<span class='lz'>" + Zeroes + "</span>"; return Zeroes + Num; } static std::string anchorName(const std::string &Anchor) { llvm::MD5 Hasher; llvm::MD5::MD5Result Hash; Hasher.update(Anchor); Hasher.final(Hash); SmallString<32> HexString; llvm::MD5::stringifyResult(Hash, HexString); return HexString.str().str(); } static ErrorOr<bool> isCoverageFile(const std::string &FileName) { ErrorOr<std::unique_ptr<MemoryBuffer>> BufOrErr = MemoryBuffer::getFile(FileName); if (!BufOrErr) { errs() << "Warning: " << BufOrErr.getError().message() << "(" << BufOrErr.getError().value() << "), filename: " << llvm::sys::path::filename(FileName) << "\n"; return BufOrErr.getError(); } std::unique_ptr<MemoryBuffer> Buf = std::move(BufOrErr.get()); if (Buf->getBufferSize() < 8) { return false; } const FileHeader *Header = reinterpret_cast<const FileHeader *>(Buf->getBufferStart()); return Header->Magic == BinCoverageMagic; } struct CoverageStats { CoverageStats() : AllPoints(0), CovPoints(0), AllFns(0), CovFns(0) {} size_t AllPoints; size_t CovPoints; size_t AllFns; size_t CovFns; }; static raw_ostream &operator<<(raw_ostream &OS, const CoverageStats &Stats) { OS << "all-edges: " << Stats.AllPoints << "\n"; OS << "cov-edges: " << Stats.CovPoints << "\n"; OS << "all-functions: " << Stats.AllFns << "\n"; OS << "cov-functions: " << Stats.CovFns << "\n"; return OS; } class CoverageData { public: // Read single file coverage data. static ErrorOr<std::unique_ptr<CoverageData>> read(const std::string &FileName) { ErrorOr<std::unique_ptr<MemoryBuffer>> BufOrErr = MemoryBuffer::getFile(FileName); if (!BufOrErr) return BufOrErr.getError(); std::unique_ptr<MemoryBuffer> Buf = std::move(BufOrErr.get()); if (Buf->getBufferSize() < 8) { errs() << "File too small (<8): " << Buf->getBufferSize(); return make_error_code(errc::illegal_byte_sequence); } const FileHeader *Header = reinterpret_cast<const FileHeader *>(Buf->getBufferStart()); if (Header->Magic != BinCoverageMagic) { errs() << "Wrong magic: " << Header->Magic; return make_error_code(errc::illegal_byte_sequence); } auto Addrs = llvm::make_unique<std::set<uint64_t>>(); switch (Header->Bitness) { case Bitness64: readInts<uint64_t>(Buf->getBufferStart() + 8, Buf->getBufferEnd(), Addrs.get()); break; case Bitness32: readInts<uint32_t>(Buf->getBufferStart() + 8, Buf->getBufferEnd(), Addrs.get()); break; default: errs() << "Unsupported bitness: " << Header->Bitness; return make_error_code(errc::illegal_byte_sequence); } return std::unique_ptr<CoverageData>(new CoverageData(std::move(Addrs))); } // Merge multiple coverage data together. static std::unique_ptr<CoverageData> merge(const std::vector<std::unique_ptr<CoverageData>> &Covs) { auto Addrs = llvm::make_unique<std::set<uint64_t>>(); for (const auto &Cov : Covs) Addrs->insert(Cov->Addrs->begin(), Cov->Addrs->end()); return std::unique_ptr<CoverageData>(new CoverageData(std::move(Addrs))); } // Read list of files and merges their coverage info. static ErrorOr<std::unique_ptr<CoverageData>> readAndMerge(const std::vector<std::string> &FileNames) { std::vector<std::unique_ptr<CoverageData>> Covs; for (const auto &FileName : FileNames) { auto Cov = read(FileName); if (!Cov) return Cov.getError(); Covs.push_back(std::move(Cov.get())); } return merge(Covs); } // Print coverage addresses. void printAddrs(raw_ostream &OS) { for (auto Addr : *Addrs) { OS << "0x"; OS.write_hex(Addr); OS << "\n"; } } protected: explicit CoverageData(std::unique_ptr<std::set<uint64_t>> Addrs) : Addrs(std::move(Addrs)) {} friend class CoverageDataWithObjectFile; std::unique_ptr<std::set<uint64_t>> Addrs; }; // Coverage data translated into source code line-level information. // Fetches debug info in constructor and calculates various information per // request. class SourceCoverageData { public: enum LineStatus { // coverage information for the line is not available. // default value in maps. UNKNOWN = 0, // the line is fully covered. COVERED = 1, // the line is fully uncovered. NOT_COVERED = 2, // some points in the line a covered, some are not. MIXED = 3 }; SourceCoverageData(std::string ObjectFile, const std::set<uint64_t> &Addrs) : AllCovPoints(getCoveragePoints(ObjectFile)) { if (!std::includes(AllCovPoints.begin(), AllCovPoints.end(), Addrs.begin(), Addrs.end())) { Fail("Coverage points in binary and .sancov file do not match."); } AllAddrInfo = getAddrInfo(ObjectFile, AllCovPoints, true); CovAddrInfo = getAddrInfo(ObjectFile, Addrs, true); } // Compute number of coverage points hit/total in a file. // file_name -> <coverage, all_coverage> std::map<std::string, std::pair<size_t, size_t>> computeFileCoverage() { std::map<std::string, std::pair<size_t, size_t>> FileCoverage; auto AllCovPointsByFile = group_by(AllAddrInfo, [](const AddrInfo &AI) { return AI.FileName; }); auto CovPointsByFile = group_by(CovAddrInfo, [](const AddrInfo &AI) { return AI.FileName; }); for (const auto &P : AllCovPointsByFile) { const std::string &FileName = P.first; FileCoverage[FileName] = std::make_pair(CovPointsByFile[FileName].size(), AllCovPointsByFile[FileName].size()); } return FileCoverage; } // line_number -> line_status. typedef std::map<int, LineStatus> LineStatusMap; // file_name -> LineStatusMap typedef std::map<std::string, LineStatusMap> FileLineStatusMap; // fills in the {file_name -> {line_no -> status}} map. FileLineStatusMap computeLineStatusMap() { FileLineStatusMap StatusMap; auto AllLocs = group_by(AllAddrInfo, [](const AddrInfo &AI) { return FileLoc{AI.FileName, AI.Line}; }); auto CovLocs = group_by(CovAddrInfo, [](const AddrInfo &AI) { return FileLoc{AI.FileName, AI.Line}; }); for (const auto &P : AllLocs) { const FileLoc &Loc = P.first; auto I = CovLocs.find(Loc); if (I == CovLocs.end()) { StatusMap[Loc.FileName][Loc.Line] = NOT_COVERED; } else { StatusMap[Loc.FileName][Loc.Line] = (I->second.size() == P.second.size()) ? COVERED : MIXED; } } return StatusMap; } std::set<FileFn> computeAllFunctions() const { std::set<FileFn> Fns; for (const auto &AI : AllAddrInfo) { Fns.insert(FileFn{AI.FileName, AI.FunctionName}); } return Fns; } std::set<FileFn> computeCoveredFunctions() const { std::set<FileFn> Fns; auto CovFns = group_by(CovAddrInfo, [](const AddrInfo &AI) { return FileFn{AI.FileName, AI.FunctionName}; }); for (const auto &P : CovFns) { Fns.insert(P.first); } return Fns; } std::set<FileFn> computeNotCoveredFunctions() const { std::set<FileFn> Fns; auto AllFns = group_by(AllAddrInfo, [](const AddrInfo &AI) { return FileFn{AI.FileName, AI.FunctionName}; }); auto CovFns = group_by(CovAddrInfo, [](const AddrInfo &AI) { return FileFn{AI.FileName, AI.FunctionName}; }); for (const auto &P : AllFns) { if (CovFns.find(P.first) == CovFns.end()) { Fns.insert(P.first); } } return Fns; } // Compute % coverage for each function. std::map<FileFn, int> computeFunctionsCoverage() const { std::map<FileFn, int> FnCoverage; auto AllFns = group_by(AllAddrInfo, [](const AddrInfo &AI) { return FileFn{AI.FileName, AI.FunctionName}; }); auto CovFns = group_by(CovAddrInfo, [](const AddrInfo &AI) { return FileFn{AI.FileName, AI.FunctionName}; }); for (const auto &P : AllFns) { FileFn F = P.first; FnCoverage[F] = CovFns[F].size() * 100 / P.second.size(); } return FnCoverage; } typedef std::map<FileLoc, std::set<std::string>> FunctionLocs; // finds first line number in a file for each function. FunctionLocs resolveFunctions(const std::set<FileFn> &Fns) const { std::vector<AddrInfo> FnAddrs; for (const auto &AI : AllAddrInfo) { if (Fns.find(FileFn{AI.FileName, AI.FunctionName}) != Fns.end()) FnAddrs.push_back(AI); } auto GroupedAddrs = group_by(FnAddrs, [](const AddrInfo &AI) { return FnLoc{FileLoc{AI.FileName, AI.Line}, AI.FunctionName}; }); FunctionLocs Result; std::string LastFileName; std::set<std::string> ProcessedFunctions; for (const auto &P : GroupedAddrs) { const FnLoc &Loc = P.first; std::string FileName = Loc.Loc.FileName; std::string FunctionName = Loc.FunctionName; if (LastFileName != FileName) ProcessedFunctions.clear(); LastFileName = FileName; if (!ProcessedFunctions.insert(FunctionName).second) continue; auto FLoc = FileLoc{FileName, Loc.Loc.Line}; Result[FLoc].insert(FunctionName); } return Result; } std::set<std::string> files() const { std::set<std::string> Files; for (const auto &AI : AllAddrInfo) { Files.insert(AI.FileName); } return Files; } void collectStats(CoverageStats *Stats) const { Stats->AllPoints += AllCovPoints.size(); Stats->AllFns += computeAllFunctions().size(); Stats->CovFns += computeCoveredFunctions().size(); } private: const std::set<uint64_t> AllCovPoints; std::vector<AddrInfo> AllAddrInfo; std::vector<AddrInfo> CovAddrInfo; }; static void printFunctionLocs(const SourceCoverageData::FunctionLocs &FnLocs, raw_ostream &OS) { for (const auto &Fns : FnLocs) { for (const auto &Fn : Fns.second) { OS << stripPathPrefix(Fns.first.FileName) << ":" << Fns.first.Line << " " << Fn << "\n"; } } } // Holder for coverage data + filename of corresponding object file. class CoverageDataWithObjectFile : public CoverageData { public: static ErrorOr<std::unique_ptr<CoverageDataWithObjectFile>> readAndMerge(const std::string &ObjectFile, const std::vector<std::string> &FileNames) { auto MergedDataOrError = CoverageData::readAndMerge(FileNames); if (!MergedDataOrError) return MergedDataOrError.getError(); return std::unique_ptr<CoverageDataWithObjectFile>( new CoverageDataWithObjectFile(ObjectFile, std::move(MergedDataOrError.get()))); } std::string object_file() const { return ObjectFile; } // Print list of covered functions. // Line format: <file_name>:<line> <function_name> void printCoveredFunctions(raw_ostream &OS) const { SourceCoverageData SCovData(ObjectFile, *Addrs); auto CoveredFns = SCovData.computeCoveredFunctions(); printFunctionLocs(SCovData.resolveFunctions(CoveredFns), OS); } // Print list of not covered functions. // Line format: <file_name>:<line> <function_name> void printNotCoveredFunctions(raw_ostream &OS) const { SourceCoverageData SCovData(ObjectFile, *Addrs); auto NotCoveredFns = SCovData.computeNotCoveredFunctions(); printFunctionLocs(SCovData.resolveFunctions(NotCoveredFns), OS); } void printReport(raw_ostream &OS) const { SourceCoverageData SCovData(ObjectFile, *Addrs); auto LineStatusMap = SCovData.computeLineStatusMap(); std::set<FileFn> AllFns = SCovData.computeAllFunctions(); // file_loc -> set[function_name] auto AllFnsByLoc = SCovData.resolveFunctions(AllFns); auto FileCoverage = SCovData.computeFileCoverage(); auto FnCoverage = SCovData.computeFunctionsCoverage(); auto FnCoverageByFile = group_by(FnCoverage, [](const std::pair<FileFn, int> &FileFn) { return FileFn.first.FileName; }); // TOC size_t NotCoveredFilesCount = 0; std::set<std::string> Files = SCovData.files(); // Covered Files. OS << "<details open><summary>Touched Files</summary>\n"; OS << "<table>\n"; OS << "<tr><th>File</th><th>Coverage %</th>"; OS << "<th>Hit (Total) Fns</th></tr>\n"; for (const auto &FileName : Files) { std::pair<size_t, size_t> FC = FileCoverage[FileName]; if (FC.first == 0) { NotCoveredFilesCount++; continue; } size_t CovPct = FC.second == 0 ? 100 : 100 * FC.first / FC.second; OS << "<tr><td><a href=\"#" << anchorName(FileName) << "\">" << stripPathPrefix(FileName) << "</a></td>" << "<td>" << formatHtmlPct(CovPct) << "%</td>" << "<td>" << FC.first << " (" << FC.second << ")" << "</tr>\n"; } OS << "</table>\n"; OS << "</details>\n"; // Not covered files. if (NotCoveredFilesCount) { OS << "<details><summary>Not Touched Files</summary>\n"; OS << "<table>\n"; for (const auto &FileName : Files) { std::pair<size_t, size_t> FC = FileCoverage[FileName]; if (FC.first == 0) OS << "<tr><td>" << stripPathPrefix(FileName) << "</td>\n"; } OS << "</table>\n"; OS << "</details>\n"; } else { OS << "<p>Congratulations! All source files are touched.</p>\n"; } // Source for (const auto &FileName : Files) { std::pair<size_t, size_t> FC = FileCoverage[FileName]; if (FC.first == 0) continue; OS << "<a name=\"" << anchorName(FileName) << "\"></a>\n"; OS << "<h2>" << stripPathPrefix(FileName) << "</h2>\n"; OS << "<details open><summary>Function Coverage</summary>"; OS << "<div class='fnlist'>\n"; auto &FileFnCoverage = FnCoverageByFile[FileName]; for (const auto &P : FileFnCoverage) { std::string FunctionName = P.first.FunctionName; OS << "<div class='fn' style='order: " << P.second << "'>"; OS << "<span class='pct'>" << formatHtmlPct(P.second) << "%</span> "; OS << "<span class='name'><a href=\"#" << anchorName(FileName + "::" + FunctionName) << "\">"; OS << escapeHtml(FunctionName) << "</a></span>"; OS << "</div>\n"; } OS << "</div></details>\n"; ErrorOr<std::unique_ptr<MemoryBuffer>> BufOrErr = MemoryBuffer::getFile(FileName); if (!BufOrErr) { OS << "Error reading file: " << FileName << " : " << BufOrErr.getError().message() << "(" << BufOrErr.getError().value() << ")\n"; continue; } OS << "<pre>\n"; const auto &LineStatuses = LineStatusMap[FileName]; for (line_iterator I = line_iterator(*BufOrErr.get(), false); !I.is_at_eof(); ++I) { uint32_t Line = I.line_number(); { // generate anchors (if any); FileLoc Loc = FileLoc{FileName, Line}; auto It = AllFnsByLoc.find(Loc); if (It != AllFnsByLoc.end()) { for (const std::string &Fn : It->second) { OS << "<a name=\"" << anchorName(FileName + "::" + Fn) << "\"></a>"; }; } } OS << "<span "; auto LIT = LineStatuses.find(I.line_number()); auto Status = (LIT != LineStatuses.end()) ? LIT->second : SourceCoverageData::UNKNOWN; switch (Status) { case SourceCoverageData::UNKNOWN: OS << "class=unknown"; break; case SourceCoverageData::COVERED: OS << "class=covered"; break; case SourceCoverageData::NOT_COVERED: OS << "class=notcovered"; break; case SourceCoverageData::MIXED: OS << "class=mixed"; break; } OS << ">"; OS << escapeHtml(*I) << "</span>\n"; } OS << "</pre>\n"; } } void collectStats(CoverageStats *Stats) const { Stats->CovPoints += Addrs->size(); SourceCoverageData SCovData(ObjectFile, *Addrs); SCovData.collectStats(Stats); } private: CoverageDataWithObjectFile(std::string ObjectFile, std::unique_ptr<CoverageData> Coverage) : CoverageData(std::move(Coverage->Addrs)), ObjectFile(std::move(ObjectFile)) {} const std::string ObjectFile; }; // Multiple coverage files data organized by object file. class CoverageDataSet { public: static ErrorOr<std::unique_ptr<CoverageDataSet>> readCmdArguments(std::vector<std::string> FileNames) { // Short name => file name. std::map<std::string, std::string> ObjFiles; std::string FirstObjFile; std::set<std::string> CovFiles; // Partition input values into coverage/object files. for (const auto &FileName : FileNames) { auto ErrorOrIsCoverage = isCoverageFile(FileName); if (!ErrorOrIsCoverage) continue; if (ErrorOrIsCoverage.get()) { CovFiles.insert(FileName); } else { auto ShortFileName = llvm::sys::path::filename(FileName); if (ObjFiles.find(ShortFileName) != ObjFiles.end()) { Fail("Duplicate binary file with a short name: " + ShortFileName); } ObjFiles[ShortFileName] = FileName; if (FirstObjFile.empty()) FirstObjFile = FileName; } } Regex SancovRegex("(.*)\\.[0-9]+\\.sancov"); SmallVector<StringRef, 2> Components; // Object file => list of corresponding coverage file names. auto CoverageByObjFile = group_by(CovFiles, [&](std::string FileName) { auto ShortFileName = llvm::sys::path::filename(FileName); auto Ok = SancovRegex.match(ShortFileName, &Components); if (!Ok) { Fail("Can't match coverage file name against " "<module_name>.<pid>.sancov pattern: " + FileName); } auto Iter = ObjFiles.find(Components[1]); if (Iter == ObjFiles.end()) { Fail("Object file for coverage not found: " + FileName); } return Iter->second; }); // Read coverage. std::vector<std::unique_ptr<CoverageDataWithObjectFile>> MergedCoverage; for (const auto &Pair : CoverageByObjFile) { if (findSanitizerCovFunctions(Pair.first).empty()) { for (const auto &FileName : Pair.second) { CovFiles.erase(FileName); } errs() << "Ignoring " << Pair.first << " and its coverage because __sanitizer_cov* functions were not " "found.\n"; continue; } auto DataOrError = CoverageDataWithObjectFile::readAndMerge(Pair.first, Pair.second); FailIfError(DataOrError); MergedCoverage.push_back(std::move(DataOrError.get())); } return std::unique_ptr<CoverageDataSet>( new CoverageDataSet(FirstObjFile, &MergedCoverage, CovFiles)); } void printCoveredFunctions(raw_ostream &OS) const { for (const auto &Cov : Coverage) { Cov->printCoveredFunctions(OS); } } void printNotCoveredFunctions(raw_ostream &OS) const { for (const auto &Cov : Coverage) { Cov->printNotCoveredFunctions(OS); } } void printStats(raw_ostream &OS) const { CoverageStats Stats; for (const auto &Cov : Coverage) { Cov->collectStats(&Stats); } OS << Stats; } void printReport(raw_ostream &OS) const { auto Title = (llvm::sys::path::filename(MainObjFile) + " Coverage Report").str(); OS << "<html>\n"; OS << "<head>\n"; // Stylesheet OS << "<style>\n"; OS << ".covered { background: #7F7; }\n"; OS << ".notcovered { background: #F77; }\n"; OS << ".mixed { background: #FF7; }\n"; OS << "summary { font-weight: bold; }\n"; OS << "details > summary + * { margin-left: 1em; }\n"; OS << ".fnlist { display: flex; flex-flow: column nowrap; }\n"; OS << ".fn { display: flex; flex-flow: row nowrap; }\n"; OS << ".pct { width: 3em; text-align: right; margin-right: 1em; }\n"; OS << ".name { flex: 2; }\n"; OS << ".lz { color: lightgray; }\n"; OS << "</style>\n"; OS << "<title>" << Title << "</title>\n"; OS << "</head>\n"; OS << "<body>\n"; // Title OS << "<h1>" << Title << "</h1>\n"; // Modules TOC. if (Coverage.size() > 1) { for (const auto &CovData : Coverage) { OS << "<li><a href=\"#module_" << anchorName(CovData->object_file()) << "\">" << llvm::sys::path::filename(CovData->object_file()) << "</a></li>\n"; } } for (const auto &CovData : Coverage) { if (Coverage.size() > 1) { OS << "<h2>" << llvm::sys::path::filename(CovData->object_file()) << "</h2>\n"; } OS << "<a name=\"module_" << anchorName(CovData->object_file()) << "\"></a>\n"; CovData->printReport(OS); } // About OS << "<details><summary>About</summary>\n"; OS << "Coverage files:<ul>"; for (const auto &InputFile : CoverageFiles) { llvm::sys::fs::file_status Status; llvm::sys::fs::status(InputFile, Status); OS << "<li>" << stripPathPrefix(InputFile) << " (" << Status.getLastModificationTime().str() << ")</li>\n"; } OS << "</ul></details>\n"; OS << "</body>\n"; OS << "</html>\n"; } bool empty() const { return Coverage.empty(); } private: explicit CoverageDataSet( const std::string &MainObjFile, std::vector<std::unique_ptr<CoverageDataWithObjectFile>> *Data, const std::set<std::string> &CoverageFiles) : MainObjFile(MainObjFile), CoverageFiles(CoverageFiles) { Data->swap(this->Coverage); } const std::string MainObjFile; std::vector<std::unique_ptr<CoverageDataWithObjectFile>> Coverage; const std::set<std::string> CoverageFiles; }; } // namespace int main(int argc, char **argv) { // Print stack trace if we signal out. sys::PrintStackTraceOnErrorSignal(argv[0]); PrettyStackTraceProgram X(argc, argv); llvm_shutdown_obj Y; // Call llvm_shutdown() on exit. llvm::InitializeAllTargetInfos(); llvm::InitializeAllTargetMCs(); llvm::InitializeAllDisassemblers(); cl::ParseCommandLineOptions(argc, argv, "Sanitizer Coverage Processing Tool"); // -print doesn't need object files. if (Action == PrintAction) { auto CovData = CoverageData::readAndMerge(ClInputFiles); FailIfError(CovData); CovData.get()->printAddrs(outs()); return 0; } else if (Action == PrintCovPointsAction) { // -print-coverage-points doesn't need coverage files. for (const std::string &ObjFile : ClInputFiles) { printCovPoints(ObjFile, outs()); } return 0; } auto CovDataSet = CoverageDataSet::readCmdArguments(ClInputFiles); FailIfError(CovDataSet); if (CovDataSet.get()->empty()) { Fail("No coverage files specified."); } switch (Action) { case CoveredFunctionsAction: { CovDataSet.get()->printCoveredFunctions(outs()); return 0; } case NotCoveredFunctionsAction: { CovDataSet.get()->printNotCoveredFunctions(outs()); return 0; } case HtmlReportAction: { CovDataSet.get()->printReport(outs()); return 0; } case StatsAction: { CovDataSet.get()->printStats(outs()); return 0; } case PrintAction: case PrintCovPointsAction: llvm_unreachable("unsupported action"); } }