| //===- llvm/unittest/DebugInfo/GSYMV2Test.cpp -----------------------------===// |
| // |
| // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| // See https://llvm.org/LICENSE.txt for license information. |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| // |
| //===----------------------------------------------------------------------===// |
| |
| #include "llvm/ADT/SmallString.h" |
| #include "llvm/DebugInfo/GSYM/FileWriter.h" |
| #include "llvm/DebugInfo/GSYM/FunctionInfo.h" |
| #include "llvm/DebugInfo/GSYM/GlobalData.h" |
| #include "llvm/DebugInfo/GSYM/GsymCreator.h" |
| #include "llvm/DebugInfo/GSYM/GsymCreatorV1.h" |
| #include "llvm/DebugInfo/GSYM/GsymCreatorV2.h" |
| #include "llvm/DebugInfo/GSYM/GsymDataExtractor.h" |
| #include "llvm/DebugInfo/GSYM/GsymReader.h" |
| #include "llvm/DebugInfo/GSYM/HeaderV2.h" |
| #include "llvm/DebugInfo/GSYM/InlineInfo.h" |
| #include "llvm/DebugInfo/GSYM/OutputAggregator.h" |
| #include "llvm/Support/Endian.h" |
| #include "llvm/Support/MemoryBuffer.h" |
| #include "llvm/Support/Path.h" |
| #include "llvm/Testing/Support/Error.h" |
| |
| #include "gtest/gtest.h" |
| #include <string> |
| |
| using namespace llvm; |
| using namespace gsym; |
| |
| //===----------------------------------------------------------------------===// |
| // Creator V2 tests |
| //===----------------------------------------------------------------------===// |
| |
| /// Helper functions |
| |
| static void checkError(std::string ExpectedMsg, Error Err) { |
| ASSERT_TRUE(bool(Err)); |
| handleAllErrors(std::move(Err), [&](const ErrorInfoBase &Actual) { |
| EXPECT_EQ(Actual.message(), ExpectedMsg); |
| }); |
| } |
| |
| /// Helper to encode a GsymCreatorV2 and return the raw bytes. |
| static Expected<SmallString<512>> encodeV2(const GsymCreatorV2 &GC, |
| llvm::endianness ByteOrder) { |
| SmallString<512> Str; |
| raw_svector_ostream OutStrm(Str); |
| FileWriter FW(OutStrm, ByteOrder); |
| FW.setStringOffsetSize(GC.getStringOffsetSize()); |
| if (auto Err = GC.encode(FW)) |
| return std::move(Err); |
| return Str; |
| } |
| |
| /// Helper to decode the HeaderV2 from raw bytes. |
| static Expected<HeaderV2> decodeHeaderV2(StringRef Bytes, |
| llvm::endianness ByteOrder) { |
| GsymDataExtractor Data(Bytes, ByteOrder == llvm::endianness::little); |
| return HeaderV2::decode(Data); |
| } |
| |
| /// Helper to decode a GlobalData entry at a given offset. |
| static GlobalData decodeGlobalDataEntry(StringRef Bytes, uint64_t &Offset, |
| llvm::endianness ByteOrder) { |
| GsymDataExtractor Data(Bytes, ByteOrder == llvm::endianness::little); |
| GlobalData GD; |
| GD.Type = static_cast<GlobalInfoType>(Data.getU32(&Offset)); |
| GD.FileOffset = Data.getU64(&Offset); |
| GD.FileSize = Data.getU64(&Offset); |
| return GD; |
| } |
| |
| /// Encode error tests |
| |
| TEST(GSYMV2Test, TestCreatorV2DoubleFinalize) { |
| GsymCreatorV2 GC; |
| const uint32_t Name = GC.insertString("foo"); |
| GC.addFunctionInfo(FunctionInfo(0x1000, 0x100, Name)); |
| OutputAggregator Null(nullptr); |
| Error Err = GC.finalize(Null); |
| ASSERT_FALSE(bool(Err)); |
| Err = GC.finalize(Null); |
| ASSERT_TRUE(bool(Err)); |
| checkError("already finalized", std::move(Err)); |
| } |
| |
| /// Header and GlobalData structure tests |
| |
| /// Encode a V2 GSYM and verify the header fields and GlobalData layout. |
| static void TestV2HeaderAndGlobalData(llvm::endianness ByteOrder, |
| uint64_t BaseAddr, |
| uint8_t ExpectedAddrOffSize, |
| uint32_t ExpectedNumAddresses, |
| bool HasUUID) { |
| GsymCreatorV2 GC; |
| const uint32_t Func1Name = GC.insertString("foo"); |
| const uint32_t Func2Name = GC.insertString("bar"); |
| GC.addFunctionInfo(FunctionInfo(BaseAddr + 0x00, 0x10, Func1Name)); |
| GC.addFunctionInfo(FunctionInfo(BaseAddr + 0x20, 0x10, Func2Name)); |
| |
| uint8_t UUID[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}; |
| if (HasUUID) |
| GC.setUUID(UUID); |
| |
| OutputAggregator Null(nullptr); |
| Error Err = GC.finalize(Null); |
| ASSERT_FALSE(bool(Err)); |
| |
| auto Result = encodeV2(GC, ByteOrder); |
| ASSERT_THAT_EXPECTED(Result, Succeeded()); |
| StringRef Data = *Result; |
| |
| // Verify header. |
| auto HdrOrErr = decodeHeaderV2(Data, ByteOrder); |
| ASSERT_THAT_EXPECTED(HdrOrErr, Succeeded()); |
| const HeaderV2 &Hdr = *HdrOrErr; |
| EXPECT_EQ(Hdr.Magic, GSYM_MAGIC); |
| EXPECT_EQ(Hdr.Version, HeaderV2::getVersion()); |
| EXPECT_EQ(Hdr.BaseAddress, BaseAddr); |
| EXPECT_EQ(Hdr.NumAddresses, ExpectedNumAddresses); |
| EXPECT_EQ(Hdr.AddrOffSize, ExpectedAddrOffSize); |
| EXPECT_EQ(Hdr.StrTableEncoding, StringTableEncoding::Default); |
| |
| // Decode GlobalData entries starting at offset 24 (after fixed header). |
| uint64_t Offset = HeaderV2::getEncodedSize(); |
| bool FoundAddrOffsets = false; |
| bool FoundAddrInfoOffsets = false; |
| bool FoundStringTable = false; |
| bool FoundFileTable = false; |
| bool FoundFunctionInfo = false; |
| bool FoundUUID = false; |
| bool FoundEndOfList = false; |
| |
| while (Offset < Data.size()) { |
| GlobalData GD = decodeGlobalDataEntry(Data, Offset, ByteOrder); |
| |
| switch (GD.Type) { |
| case GlobalInfoType::EndOfList: |
| EXPECT_EQ(GD.FileOffset, 0u); |
| EXPECT_EQ(GD.FileSize, 0u); |
| FoundEndOfList = true; |
| break; |
| case GlobalInfoType::AddrOffsets: |
| EXPECT_EQ(GD.FileSize, |
| ExpectedNumAddresses * (uint64_t)ExpectedAddrOffSize); |
| EXPECT_GT(GD.FileOffset, 0u); |
| FoundAddrOffsets = true; |
| break; |
| case GlobalInfoType::AddrInfoOffsets: |
| EXPECT_EQ(GD.FileSize, |
| ExpectedNumAddresses * |
| (uint64_t)HeaderV2::getAddressInfoOffsetSize()); |
| EXPECT_GT(GD.FileOffset, 0u); |
| FoundAddrInfoOffsets = true; |
| break; |
| case GlobalInfoType::StringTable: |
| EXPECT_GT(GD.FileSize, 0u); |
| EXPECT_GT(GD.FileOffset, 0u); |
| FoundStringTable = true; |
| break; |
| case GlobalInfoType::FileTable: |
| EXPECT_GT(GD.FileSize, 0u); |
| EXPECT_GT(GD.FileOffset, 0u); |
| FoundFileTable = true; |
| break; |
| case GlobalInfoType::FunctionInfo: |
| EXPECT_GT(GD.FileSize, 0u); |
| EXPECT_GT(GD.FileOffset, 0u); |
| FoundFunctionInfo = true; |
| break; |
| case GlobalInfoType::UUID: |
| EXPECT_EQ(GD.FileSize, sizeof(UUID)); |
| EXPECT_GT(GD.FileOffset, 0u); |
| FoundUUID = true; |
| break; |
| } |
| if (FoundEndOfList) |
| break; |
| } |
| |
| EXPECT_TRUE(FoundAddrOffsets); |
| EXPECT_TRUE(FoundAddrInfoOffsets); |
| EXPECT_TRUE(FoundStringTable); |
| EXPECT_TRUE(FoundFileTable); |
| EXPECT_TRUE(FoundFunctionInfo); |
| EXPECT_TRUE(FoundEndOfList); |
| EXPECT_EQ(FoundUUID, HasUUID); |
| |
| // Verify that all section data fits within the encoded buffer. |
| Offset = HeaderV2::getEncodedSize(); |
| while (Offset < Data.size()) { |
| GlobalData GD = decodeGlobalDataEntry(Data, Offset, ByteOrder); |
| if (GD.Type == GlobalInfoType::EndOfList) |
| break; |
| EXPECT_LE(GD.FileOffset + GD.FileSize, Data.size()) |
| << "Section type " << static_cast<uint32_t>(GD.Type) |
| << " extends beyond buffer"; |
| } |
| } |
| |
| TEST(GSYMV2Test, TestCreatorV2HeaderAndGlobalDataLittle) { |
| TestV2HeaderAndGlobalData(llvm::endianness::little, 0x1000, |
| /*ExpectedAddrOffSize=*/1, |
| /*ExpectedNumAddresses=*/2, |
| /*HasUUID=*/true); |
| } |
| |
| TEST(GSYMV2Test, TestCreatorV2HeaderAndGlobalDataBig) { |
| TestV2HeaderAndGlobalData(llvm::endianness::big, 0x1000, |
| /*ExpectedAddrOffSize=*/1, |
| /*ExpectedNumAddresses=*/2, |
| /*HasUUID=*/true); |
| } |
| |
| TEST(GSYMV2Test, TestCreatorV2HeaderAndGlobalDataNoUUID) { |
| TestV2HeaderAndGlobalData(llvm::endianness::little, 0x1000, |
| /*ExpectedAddrOffSize=*/1, |
| /*ExpectedNumAddresses=*/2, |
| /*HasUUID=*/false); |
| } |
| |
| /// AddrInfoOffsets verification |
| |
| TEST(GSYMV2Test, TestCreatorV2AddrInfoOffsetsPointToFunctionInfo) { |
| // Verify that each AddrInfoOffset entry (relative to FunctionInfo section) |
| // points to a valid location within the FunctionInfo section. |
| GsymCreatorV2 GC; |
| const uint32_t Func1Name = GC.insertString("func_a"); |
| const uint32_t Func2Name = GC.insertString("func_b"); |
| const uint32_t Func3Name = GC.insertString("func_c"); |
| GC.addFunctionInfo(FunctionInfo(0x1000, 0x100, Func1Name)); |
| GC.addFunctionInfo(FunctionInfo(0x1100, 0x100, Func2Name)); |
| GC.addFunctionInfo(FunctionInfo(0x1200, 0x100, Func3Name)); |
| OutputAggregator Null(nullptr); |
| Error Err = GC.finalize(Null); |
| ASSERT_FALSE(bool(Err)); |
| |
| auto Result = encodeV2(GC, llvm::endianness::little); |
| ASSERT_THAT_EXPECTED(Result, Succeeded()); |
| StringRef Bytes = *Result; |
| |
| constexpr uint8_t AddrInfoOffSize = HeaderV2::getAddressInfoOffsetSize(); |
| |
| // Find the AddrInfoOffsets and FunctionInfo sections from GlobalData. |
| uint64_t Offset = HeaderV2::getEncodedSize(); |
| uint64_t AIOffsetsOffset = 0; |
| uint64_t FISize = 0; |
| while (Offset < Bytes.size()) { |
| GlobalData GD = |
| decodeGlobalDataEntry(Bytes, Offset, llvm::endianness::little); |
| if (GD.Type == GlobalInfoType::AddrInfoOffsets) { |
| AIOffsetsOffset = GD.FileOffset; |
| } else if (GD.Type == GlobalInfoType::FunctionInfo) { |
| FISize = GD.FileSize; |
| } else if (GD.Type == GlobalInfoType::EndOfList) { |
| break; |
| } |
| } |
| ASSERT_GT(AIOffsetsOffset, 0u); |
| ASSERT_GT(FISize, 0u); |
| |
| // Each AddrInfoOffset is relative to the FunctionInfo section and should |
| // be within [0, FISize). |
| GsymDataExtractor Data(Bytes, /*IsLittleEndian=*/true); |
| uint64_t AIOffset = AIOffsetsOffset; |
| for (uint32_t I = 0; I < 3; ++I) { |
| uint64_t RelOff = Data.getUnsigned(&AIOffset, AddrInfoOffSize); |
| EXPECT_LT(RelOff, FISize) |
| << "AddrInfoOffset[" << I << "] beyond FunctionInfo section"; |
| } |
| |
| // Relative offsets should be strictly increasing (sorted functions). |
| AIOffset = AIOffsetsOffset; |
| uint64_t PrevOff = Data.getUnsigned(&AIOffset, AddrInfoOffSize); |
| EXPECT_EQ(PrevOff, 0u) << "First AddrInfoOffset should be 0"; |
| for (uint32_t I = 1; I < 3; ++I) { |
| uint64_t CurOff = Data.getUnsigned(&AIOffset, AddrInfoOffSize); |
| EXPECT_GT(CurOff, PrevOff) |
| << "AddrInfoOffset[" << I << "] not strictly increasing"; |
| PrevOff = CurOff; |
| } |
| } |
| |
| /// UUID section verification |
| |
| TEST(GSYMV2Test, TestCreatorV2UUIDSection) { |
| GsymCreatorV2 GC; |
| const uint32_t Name = GC.insertString("main"); |
| GC.addFunctionInfo(FunctionInfo(0x1000, 0x100, Name)); |
| uint8_t UUID[] = {0xAA, 0xBB, 0xCC, 0xDD, 0x11, 0x22, 0x33, 0x44}; |
| GC.setUUID(UUID); |
| OutputAggregator Null(nullptr); |
| Error Err = GC.finalize(Null); |
| ASSERT_FALSE(bool(Err)); |
| |
| auto Result = encodeV2(GC, llvm::endianness::little); |
| ASSERT_THAT_EXPECTED(Result, Succeeded()); |
| StringRef Data = *Result; |
| |
| // Find UUID section. |
| uint64_t Offset = HeaderV2::getEncodedSize(); |
| uint64_t UUIDOffset = 0, UUIDSize = 0; |
| while (Offset < Data.size()) { |
| GlobalData GD = |
| decodeGlobalDataEntry(Data, Offset, llvm::endianness::little); |
| if (GD.Type == GlobalInfoType::UUID) { |
| UUIDOffset = GD.FileOffset; |
| UUIDSize = GD.FileSize; |
| } else if (GD.Type == GlobalInfoType::EndOfList) { |
| break; |
| } |
| } |
| ASSERT_EQ(UUIDSize, sizeof(UUID)); |
| ASSERT_GT(UUIDOffset, 0u); |
| |
| // Verify the UUID bytes match. |
| StringRef UUIDData = Data.substr(UUIDOffset, UUIDSize); |
| EXPECT_EQ(UUIDData, |
| StringRef(reinterpret_cast<const char *>(UUID), sizeof(UUID))); |
| } |
| |
| /// Verify that all sections in a V2 GSYM are correctly aligned. |
| /// Uses a 13-byte UUID and 8-byte AddrOffSize to create non-trivial |
| /// alignment scenarios where padding is required between sections. |
| TEST(GSYMV2Test, TestCreatorV2SectionAlignment) { |
| // 13-byte UUID: after header (24) + GlobalData (7 entries * 20 = 140), |
| // UUID ends at offset 177. AddrOffsets needs 8-byte alignment → 184. |
| uint8_t UUID[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}; |
| GsymCreatorV2 GC; |
| GC.setUUID(UUID); |
| // Addresses far apart to force 8-byte AddrOffSize. |
| GC.addFunctionInfo(FunctionInfo(0x1000, 0x100, GC.insertString("foo"))); |
| GC.addFunctionInfo(FunctionInfo(0x100001000, 0x100, GC.insertString("bar"))); |
| GC.addFunctionInfo(FunctionInfo(0x200002000, 0x100, GC.insertString("baz"))); |
| OutputAggregator Null(nullptr); |
| ASSERT_FALSE(GC.finalize(Null)); |
| |
| auto Result = encodeV2(GC, llvm::endianness::little); |
| ASSERT_THAT_EXPECTED(Result, Succeeded()); |
| StringRef Data = *Result; |
| |
| // Decode header to get alignment sizes. |
| auto HdrOrErr = decodeHeaderV2(Data, llvm::endianness::little); |
| ASSERT_THAT_EXPECTED(HdrOrErr, Succeeded()); |
| const HeaderV2 &Hdr = *HdrOrErr; |
| // Delta > UINT32_MAX rounds up to 8 (power-of-two only). |
| EXPECT_EQ(Hdr.AddrOffSize, 8u); |
| |
| // Decode GlobalData entries and verify alignment for each section. |
| uint64_t Offset = HeaderV2::getEncodedSize(); |
| while (Offset < Data.size()) { |
| GlobalData GD = |
| decodeGlobalDataEntry(Data, Offset, llvm::endianness::little); |
| if (GD.Type == GlobalInfoType::EndOfList) |
| break; |
| switch (GD.Type) { |
| case GlobalInfoType::UUID: |
| // UUID has no alignment requirement. |
| break; |
| case GlobalInfoType::AddrOffsets: |
| EXPECT_EQ(GD.FileOffset % Hdr.AddrOffSize, 0u) |
| << "AddrOffsets not aligned to " << (unsigned)Hdr.AddrOffSize; |
| break; |
| case GlobalInfoType::AddrInfoOffsets: |
| EXPECT_EQ(GD.FileOffset % HeaderV2::getAddressInfoOffsetSize(), 0u) |
| << "AddrInfoOffsets not aligned to " |
| << (unsigned)HeaderV2::getAddressInfoOffsetSize(); |
| break; |
| case GlobalInfoType::FileTable: |
| EXPECT_EQ(GD.FileOffset % 4, 0u) << "FileTable not 4-byte aligned"; |
| break; |
| case GlobalInfoType::StringTable: |
| // StringTable has no alignment requirement. |
| break; |
| case GlobalInfoType::FunctionInfo: |
| EXPECT_EQ(GD.FileOffset % 4, 0u) << "FunctionInfo not 4-byte aligned"; |
| break; |
| default: |
| break; |
| } |
| } |
| |
| // Also verify the round-trip works. |
| auto GROrErr = GsymReader::copyBuffer(Data); |
| ASSERT_THAT_EXPECTED(GROrErr, Succeeded()); |
| EXPECT_EQ((*GROrErr)->getNumAddresses(), 3u); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // Reader V2 tests (without creator — hand-crafted binary) |
| //===----------------------------------------------------------------------===// |
| |
| /// Helper to build a minimal valid V2 GSYM binary in native endianness. |
| /// Creates one function "main" at BaseAddr with size FuncSize. |
| static SmallString<512> buildMinimalV2Binary(uint64_t BaseAddr, |
| uint32_t FuncSize) { |
| SmallString<512> Str; |
| raw_svector_ostream OS(Str); |
| FileWriter FW(OS, llvm::endianness::native); |
| |
| // We'll build: header (20) + GlobalData entries (6 entries * 20 = 120) + |
| // sections. Total GlobalData entries: AddrOffsets, AddrInfoOffsets, |
| // StringTable, FileTable, FunctionInfo, EndOfList = 6. |
| constexpr uint64_t HeaderSize = HeaderV2::getEncodedSize(); |
| constexpr uint64_t NumGlobalEntries = 6; |
| constexpr uint64_t GlobalDataSize = NumGlobalEntries * 20; |
| constexpr uint8_t AddrOffSize = 1; |
| constexpr uint8_t AddrInfoOffSize = HeaderV2::getAddressInfoOffsetSize(); |
| constexpr uint8_t StrpSize = HeaderV2::getStringOffsetSize(); |
| constexpr uint32_t NumAddresses = 1; |
| |
| // Layout sections sequentially after header + GlobalData. |
| uint64_t CurOffset = HeaderSize + GlobalDataSize; |
| |
| // AddrOffsets: 1 address * 1 byte. |
| const uint64_t AddrOffsetsOff = CurOffset; |
| const uint64_t AddrOffsetsSize = NumAddresses * AddrOffSize; |
| CurOffset += AddrOffsetsSize; |
| |
| // Pad to alignment for AddrInfoOffsets. |
| CurOffset = llvm::alignTo(CurOffset, AddrInfoOffSize); |
| const uint64_t AddrInfoOffsetsOff = CurOffset; |
| const uint64_t AddrInfoOffsetsSize = NumAddresses * AddrInfoOffSize; |
| CurOffset += AddrInfoOffsetsSize; |
| |
| // FileTable: 4 bytes (NumFiles=1) + 1 FileEntry (2 * StrpSize bytes). |
| CurOffset = llvm::alignTo(CurOffset, 4); |
| const uint64_t FileTableOff = CurOffset; |
| const uint64_t FileTableSize = 4 + 2 * StrpSize; // 1 file entry. |
| CurOffset += FileTableSize; |
| |
| // StringTable: "\0main\0" = 6 bytes. |
| const uint64_t StringTableOff = CurOffset; |
| const char StrTabData[] = "\0main"; |
| const uint64_t StringTableSize = sizeof(StrTabData); // includes trailing \0 |
| CurOffset += StringTableSize; |
| |
| // FunctionInfo: encode a minimal FunctionInfo. |
| CurOffset = llvm::alignTo(CurOffset, 4); |
| const uint64_t FuncInfoOff = CurOffset; |
| // FunctionInfo encoding: uint32_t Size, uint32_t Name (strp offset). |
| // "main" is at offset 1 in the string table. |
| // Minimal FI: size (4 bytes) + name (4 bytes) = 8 bytes, no line table or |
| // inline info (InfoType::EndOfList = 0 follows). |
| // Actually FunctionInfo::encode writes: size, name, then info types. |
| // Let's pre-encode a FunctionInfo to get exact bytes. |
| SmallString<64> FIBuf; |
| { |
| raw_svector_ostream FIOS(FIBuf); |
| FileWriter FIFW(FIOS, llvm::endianness::native); |
| FIFW.setStringOffsetSize(HeaderV2::getStringOffsetSize()); |
| FunctionInfo FI(BaseAddr, FuncSize, |
| /*Name=*/1); // "main" at strtab offset 1 |
| auto OffOrErr = FI.encode(FIFW); |
| assert(OffOrErr && "FunctionInfo encode failed"); |
| (void)OffOrErr; |
| } |
| const uint64_t FuncInfoSize = FIBuf.size(); |
| |
| // Write header. |
| FW.writeU32(GSYM_MAGIC); // Magic |
| FW.writeU16(HeaderV2::getVersion()); // Version |
| FW.writeU8(AddrOffSize); // AddrOffSize |
| FW.writeU8(0); // StrTableEncoding |
| FW.writeU64(BaseAddr); // BaseAddress |
| FW.writeU32(NumAddresses); // NumAddresses |
| |
| // GlobalData entries. |
| auto writeGD = [&](GlobalInfoType Type, uint64_t Off, uint64_t Size) { |
| FW.writeU32(static_cast<uint32_t>(Type)); |
| FW.writeU64(Off); |
| FW.writeU64(Size); |
| }; |
| writeGD(GlobalInfoType::AddrOffsets, AddrOffsetsOff, AddrOffsetsSize); |
| writeGD(GlobalInfoType::AddrInfoOffsets, AddrInfoOffsetsOff, |
| AddrInfoOffsetsSize); |
| writeGD(GlobalInfoType::StringTable, StringTableOff, StringTableSize); |
| writeGD(GlobalInfoType::FileTable, FileTableOff, FileTableSize); |
| writeGD(GlobalInfoType::FunctionInfo, FuncInfoOff, FuncInfoSize); |
| writeGD(GlobalInfoType::EndOfList, 0, 0); |
| |
| // AddrOffsets section. |
| assert(FW.tell() == AddrOffsetsOff); |
| FW.writeU8(0); // Offset from BaseAddr = 0 for first function. |
| |
| // Pad to AddrInfoOffsets. Values are relative to FunctionInfo section. |
| FW.alignTo(AddrInfoOffSize); |
| assert(FW.tell() == AddrInfoOffsetsOff); |
| FW.writeU64(0); // RelOff = 0 (first and only FunctionInfo). |
| |
| // FileTable. |
| FW.alignTo(4); |
| assert(FW.tell() == FileTableOff); |
| FW.writeU32(1); // NumFiles = 1 |
| FW.writeU64(0); // File[0].Dir = 0 |
| FW.writeU64(0); // File[0].Base = 0 |
| |
| // StringTable. |
| assert(FW.tell() == StringTableOff); |
| FW.writeData(ArrayRef<uint8_t>(reinterpret_cast<const uint8_t *>(StrTabData), |
| StringTableSize)); |
| |
| // FunctionInfo. |
| FW.alignTo(4); |
| assert(FW.tell() == FuncInfoOff); |
| FW.writeData(ArrayRef<uint8_t>( |
| reinterpret_cast<const uint8_t *>(FIBuf.data()), FIBuf.size())); |
| |
| return Str; |
| } |
| |
| TEST(GSYMV2Test, TestReaderV2ParseHandCrafted) { |
| // Build a minimal V2 binary by hand and verify the reader can parse it. |
| auto Bytes = buildMinimalV2Binary(0x1000, 0x100); |
| auto GROrErr = GsymReader::copyBuffer(StringRef(Bytes.data(), Bytes.size())); |
| ASSERT_THAT_EXPECTED(GROrErr, Succeeded()); |
| auto &GR = *GROrErr; |
| |
| EXPECT_EQ(GR->getBaseAddress(), 0x1000u); |
| EXPECT_EQ(GR->getNumAddresses(), 1u); |
| EXPECT_EQ(GR->getAddressOffsetSize(), 1u); |
| |
| // Verify address lookup. |
| auto Addr = GR->getAddress(0); |
| ASSERT_TRUE(Addr.has_value()); |
| EXPECT_EQ(*Addr, 0x1000u); |
| |
| // Verify getString. |
| EXPECT_EQ(GR->getString(1), "main"); |
| |
| // Verify getFile (index 0 is the empty file entry). |
| auto FE = GR->getFile(0); |
| ASSERT_TRUE(FE.has_value()); |
| EXPECT_EQ(FE->Dir, 0u); |
| EXPECT_EQ(FE->Base, 0u); |
| } |
| |
| TEST(GSYMV2Test, TestReaderV2GetFunctionInfoHandCrafted) { |
| auto Bytes = buildMinimalV2Binary(0x1000, 0x100); |
| auto GROrErr = GsymReader::copyBuffer(StringRef(Bytes.data(), Bytes.size())); |
| ASSERT_THAT_EXPECTED(GROrErr, Succeeded()); |
| auto &GR = *GROrErr; |
| |
| // getFunctionInfo should decode the function at 0x1000. |
| auto FI = GR->getFunctionInfo(0x1000); |
| ASSERT_THAT_EXPECTED(FI, Succeeded()); |
| EXPECT_EQ(FI->Range, AddressRange(0x1000, 0x1100)); |
| EXPECT_EQ(FI->Name, 1u); // "main" at strtab offset 1 |
| EXPECT_EQ(GR->getString(FI->Name), "main"); |
| |
| // Address within the function range should also work. |
| auto FI2 = GR->getFunctionInfo(0x1050); |
| ASSERT_THAT_EXPECTED(FI2, Succeeded()); |
| EXPECT_EQ(FI2->Range, AddressRange(0x1000, 0x1100)); |
| |
| // Address outside range should fail. |
| auto FI3 = GR->getFunctionInfo(0x2000); |
| EXPECT_THAT_EXPECTED(FI3, Failed()); |
| } |
| |
| TEST(GSYMV2Test, TestReaderV2LookupHandCrafted) { |
| auto Bytes = buildMinimalV2Binary(0x1000, 0x100); |
| auto GROrErr = GsymReader::copyBuffer(StringRef(Bytes.data(), Bytes.size())); |
| ASSERT_THAT_EXPECTED(GROrErr, Succeeded()); |
| auto &GR = *GROrErr; |
| |
| // lookup should return a LookupResult. |
| auto LR = GR->lookup(0x1000); |
| ASSERT_THAT_EXPECTED(LR, Succeeded()); |
| EXPECT_EQ(LR->FuncName, "main"); |
| EXPECT_EQ(LR->FuncRange, AddressRange(0x1000, 0x1100)); |
| |
| // lookup within range. |
| auto LR2 = GR->lookup(0x1080); |
| ASSERT_THAT_EXPECTED(LR2, Succeeded()); |
| EXPECT_EQ(LR2->FuncName, "main"); |
| |
| // lookup outside range. |
| auto LR3 = GR->lookup(0x2000); |
| EXPECT_THAT_EXPECTED(LR3, Failed()); |
| } |
| |
| TEST(GSYMV2Test, TestReaderV2InvalidMagic) { |
| // Create a buffer with invalid magic. |
| char Buf[24] = {}; |
| Buf[0] = 'X'; // Bad magic. |
| auto GR = GsymReader::copyBuffer(StringRef(Buf, sizeof(Buf))); |
| EXPECT_THAT_EXPECTED(GR, Failed()); |
| } |
| |
| TEST(GSYMV2Test, TestReaderV2TooSmall) { |
| // Buffer smaller than header. |
| char Buf[10] = {}; |
| auto GR = GsymReader::copyBuffer(StringRef(Buf, sizeof(Buf))); |
| EXPECT_THAT_EXPECTED(GR, Failed()); |
| } |
| |
| TEST(GSYMV2Test, TestReaderV2TruncatedFileTable) { |
| // Build a valid V2 binary, then truncate the file table by reducing its |
| // GlobalData FileSize. The reader should report the file table is too small. |
| auto Bytes = buildMinimalV2Binary(0x1000, 0x100); |
| // The binary has 1 file entry. Find the FileTable GlobalData entry and |
| // shrink its FileSize to be too small. |
| // GlobalData entries start at offset 24 (after HeaderV2). |
| // Each entry is 20 bytes: Type(4) + FileOffset(8) + FileSize(8). |
| // We need to find the FileTable entry and modify its FileSize. |
| GsymDataExtractor Data(StringRef(Bytes.data(), Bytes.size()), |
| llvm::endianness::native == llvm::endianness::little); |
| uint64_t Offset = HeaderV2::getEncodedSize(); |
| while (Offset < Bytes.size()) { |
| uint64_t EntryOffset = Offset; |
| uint32_t Type = Data.getU32(&Offset); |
| uint64_t FileOffset = Data.getU64(&Offset); |
| uint64_t FileSize = Data.getU64(&Offset); |
| (void)FileOffset; |
| (void)FileSize; |
| if (Type == static_cast<uint32_t>(GlobalInfoType::FileTable)) { |
| // Set FileSize to 4 (just the NumFiles field, no room for entries). |
| // FileSize is at EntryOffset + 4 (Type) + 8 (FileOffset) = +12. |
| uint64_t FileSizeOffset = EntryOffset + 12; |
| support::endian::write64(Bytes.data() + FileSizeOffset, 4, |
| llvm::endianness::native); |
| break; |
| } |
| if (Type == static_cast<uint32_t>(GlobalInfoType::EndOfList)) |
| break; |
| } |
| auto GR = GsymReader::copyBuffer(StringRef(Bytes.data(), Bytes.size())); |
| ASSERT_FALSE(bool(GR)); |
| std::string ErrMsg; |
| handleAllErrors(GR.takeError(), |
| [&](const ErrorInfoBase &E) { ErrMsg = E.message(); }); |
| EXPECT_NE(ErrMsg.find("FileTable section too small"), std::string::npos) |
| << "Unexpected error: " << ErrMsg; |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // Creator/reader round-trip tests: Creator V2 -> Reader V2 |
| //===----------------------------------------------------------------------===// |
| |
| /// Helper to create, finalize, encode with GsymCreatorV2, then decode with |
| /// GsymReaderV2 and return the reader. |
| static Expected<std::unique_ptr<GsymReader>> |
| createAndReadV2(GsymCreatorV2 &GC, |
| llvm::endianness ByteOrder = llvm::endianness::native) { |
| OutputAggregator Null(nullptr); |
| if (auto Err = GC.finalize(Null)) |
| return std::move(Err); |
| |
| SmallString<512> Str; |
| raw_svector_ostream OutStrm(Str); |
| FileWriter FW(OutStrm, ByteOrder); |
| FW.setStringOffsetSize(GC.getStringOffsetSize()); |
| if (auto Err = GC.encode(FW)) |
| return std::move(Err); |
| |
| return GsymReader::copyBuffer(OutStrm.str()); |
| } |
| |
| TEST(GSYMV2Test, TestRoundTripGetFunctionInfoAtIndex) { |
| GsymCreatorV2 GC; |
| const uint32_t Name1 = GC.insertString("func_x"); |
| const uint32_t Name2 = GC.insertString("func_y"); |
| GC.addFunctionInfo(FunctionInfo(0x3000, 0x100, Name1)); |
| GC.addFunctionInfo(FunctionInfo(0x3100, 0x100, Name2)); |
| |
| auto GROrErr = createAndReadV2(GC); |
| ASSERT_THAT_EXPECTED(GROrErr, Succeeded()); |
| auto &GR = *GROrErr; |
| |
| // Access by index. |
| auto FI0 = GR->getFunctionInfoAtIndex(0); |
| ASSERT_THAT_EXPECTED(FI0, Succeeded()); |
| EXPECT_EQ(GR->getString(FI0->Name), "func_x"); |
| |
| auto FI1 = GR->getFunctionInfoAtIndex(1); |
| ASSERT_THAT_EXPECTED(FI1, Succeeded()); |
| EXPECT_EQ(GR->getString(FI1->Name), "func_y"); |
| |
| // Out of bounds index. |
| auto FI2 = GR->getFunctionInfoAtIndex(2); |
| EXPECT_THAT_EXPECTED(FI2, Failed()); |
| } |
| |
| TEST(GSYMV2Test, TestRoundTripAddressTable) { |
| GsymCreatorV2 GC; |
| const uint32_t N1 = GC.insertString("a"); |
| const uint32_t N2 = GC.insertString("b"); |
| const uint32_t N3 = GC.insertString("c"); |
| GC.addFunctionInfo(FunctionInfo(0x8000, 0x10, N1)); |
| GC.addFunctionInfo(FunctionInfo(0x8020, 0x10, N2)); |
| GC.addFunctionInfo(FunctionInfo(0x8040, 0x10, N3)); |
| |
| auto GROrErr = createAndReadV2(GC); |
| ASSERT_THAT_EXPECTED(GROrErr, Succeeded()); |
| auto &GR = *GROrErr; |
| |
| // Verify addresses via getAddress. |
| EXPECT_EQ(GR->getAddress(0), std::optional<uint64_t>(0x8000u)); |
| EXPECT_EQ(GR->getAddress(1), std::optional<uint64_t>(0x8020u)); |
| EXPECT_EQ(GR->getAddress(2), std::optional<uint64_t>(0x8040u)); |
| EXPECT_EQ(GR->getAddress(3), std::nullopt); // Out of bounds. |
| } |
| |
| TEST(GSYMV2Test, TestRoundTripLargeAddressOffsets) { |
| // Test with address offsets that require more than 2 bytes (rounds up to 4). |
| GsymCreatorV2 GC; |
| const uint32_t N1 = GC.insertString("near"); |
| const uint32_t N2 = GC.insertString("far"); |
| GC.addFunctionInfo(FunctionInfo(0x1000, 0x10, N1)); |
| GC.addFunctionInfo(FunctionInfo(0x1000 + 0x20000, 0x10, N2)); |
| |
| auto GROrErr = createAndReadV2(GC); |
| ASSERT_THAT_EXPECTED(GROrErr, Succeeded()); |
| auto &GR = *GROrErr; |
| |
| // V2 only supports power-of-two AddrOffSize (1/2/4/8), so 3 rounds up to 4. |
| EXPECT_EQ(GR->getAddressOffsetSize(), 4u); |
| EXPECT_EQ(GR->getNumAddresses(), 2u); |
| |
| auto FI1 = GR->getFunctionInfo(0x1000); |
| ASSERT_THAT_EXPECTED(FI1, Succeeded()); |
| EXPECT_EQ(GR->getString(FI1->Name), "near"); |
| |
| auto FI2 = GR->getFunctionInfo(0x1000 + 0x20000); |
| ASSERT_THAT_EXPECTED(FI2, Succeeded()); |
| EXPECT_EQ(GR->getString(FI2->Name), "far"); |
| } |
| |
| /// Swapped-endianness round-trip tests |
| |
| /// Get the non-native byte order. |
| static llvm::endianness swappedEndianness() { |
| if constexpr (llvm::endianness::native == llvm::endianness::little) |
| return llvm::endianness::big; |
| else |
| return llvm::endianness::little; |
| } |
| |
| TEST(GSYMV2Test, TestRoundTripSwappedSingleFunction) { |
| GsymCreatorV2 GC; |
| const uint32_t Name = GC.insertString("hello"); |
| GC.addFunctionInfo(FunctionInfo(0x2000, 0x200, Name)); |
| |
| auto GROrErr = createAndReadV2(GC, swappedEndianness()); |
| ASSERT_THAT_EXPECTED(GROrErr, Succeeded()); |
| auto &GR = *GROrErr; |
| |
| EXPECT_EQ(GR->getNumAddresses(), 1u); |
| EXPECT_EQ(GR->getBaseAddress(), 0x2000u); |
| |
| auto FI = GR->getFunctionInfo(0x2000); |
| ASSERT_THAT_EXPECTED(FI, Succeeded()); |
| EXPECT_EQ(FI->Range, AddressRange(0x2000, 0x2200)); |
| EXPECT_EQ(GR->getString(FI->Name), "hello"); |
| } |
| |
| TEST(GSYMV2Test, TestRoundTripSwappedMultipleFunctions) { |
| GsymCreatorV2 GC; |
| const uint32_t Name1 = GC.insertString("alpha"); |
| const uint32_t Name2 = GC.insertString("beta"); |
| const uint32_t Name3 = GC.insertString("gamma"); |
| GC.addFunctionInfo(FunctionInfo(0x1000, 0x100, Name1)); |
| GC.addFunctionInfo(FunctionInfo(0x1100, 0x100, Name2)); |
| GC.addFunctionInfo(FunctionInfo(0x1200, 0x100, Name3)); |
| |
| auto GROrErr = createAndReadV2(GC, swappedEndianness()); |
| ASSERT_THAT_EXPECTED(GROrErr, Succeeded()); |
| auto &GR = *GROrErr; |
| |
| EXPECT_EQ(GR->getNumAddresses(), 3u); |
| |
| auto FI1 = GR->getFunctionInfo(0x1000); |
| ASSERT_THAT_EXPECTED(FI1, Succeeded()); |
| EXPECT_EQ(GR->getString(FI1->Name), "alpha"); |
| |
| auto FI2 = GR->getFunctionInfo(0x1100); |
| ASSERT_THAT_EXPECTED(FI2, Succeeded()); |
| EXPECT_EQ(GR->getString(FI2->Name), "beta"); |
| |
| auto FI3 = GR->getFunctionInfo(0x1200); |
| ASSERT_THAT_EXPECTED(FI3, Succeeded()); |
| EXPECT_EQ(GR->getString(FI3->Name), "gamma"); |
| } |
| |
| TEST(GSYMV2Test, TestRoundTripSwappedLookup) { |
| GsymCreatorV2 GC; |
| const uint32_t Name1 = GC.insertString("start"); |
| const uint32_t Name2 = GC.insertString("end"); |
| GC.addFunctionInfo(FunctionInfo(0x5000, 0x500, Name1)); |
| GC.addFunctionInfo(FunctionInfo(0x5500, 0x500, Name2)); |
| |
| auto GROrErr = createAndReadV2(GC, swappedEndianness()); |
| ASSERT_THAT_EXPECTED(GROrErr, Succeeded()); |
| auto &GR = *GROrErr; |
| |
| auto LR1 = GR->lookup(0x5000); |
| ASSERT_THAT_EXPECTED(LR1, Succeeded()); |
| EXPECT_EQ(LR1->FuncName, "start"); |
| EXPECT_EQ(LR1->FuncRange, AddressRange(0x5000, 0x5500)); |
| |
| auto LR2 = GR->lookup(0x5500); |
| ASSERT_THAT_EXPECTED(LR2, Succeeded()); |
| EXPECT_EQ(LR2->FuncName, "end"); |
| |
| auto LR3 = GR->lookup(0x5100); |
| ASSERT_THAT_EXPECTED(LR3, Succeeded()); |
| EXPECT_EQ(LR3->FuncName, "start"); |
| |
| auto LR4 = GR->lookup(0x6000); |
| EXPECT_THAT_EXPECTED(LR4, Failed()); |
| } |
| |
| TEST(GSYMV2Test, TestRoundTripSwappedAddressTable) { |
| GsymCreatorV2 GC; |
| const uint32_t N1 = GC.insertString("a"); |
| const uint32_t N2 = GC.insertString("b"); |
| const uint32_t N3 = GC.insertString("c"); |
| GC.addFunctionInfo(FunctionInfo(0x8000, 0x10, N1)); |
| GC.addFunctionInfo(FunctionInfo(0x8020, 0x10, N2)); |
| GC.addFunctionInfo(FunctionInfo(0x8040, 0x10, N3)); |
| |
| auto GROrErr = createAndReadV2(GC, swappedEndianness()); |
| ASSERT_THAT_EXPECTED(GROrErr, Succeeded()); |
| auto &GR = *GROrErr; |
| |
| EXPECT_EQ(GR->getAddress(0), std::optional<uint64_t>(0x8000u)); |
| EXPECT_EQ(GR->getAddress(1), std::optional<uint64_t>(0x8020u)); |
| EXPECT_EQ(GR->getAddress(2), std::optional<uint64_t>(0x8040u)); |
| EXPECT_EQ(GR->getAddress(3), std::nullopt); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // Version round-trip tests: V1 -> V2 -> V1 and V2 -> V1 -> V2 |
| //===----------------------------------------------------------------------===// |
| |
| /// Recursively re-insert inline info strings and files from a reader into a |
| /// creator. |
| static void fixupInlineInfoForTransfer(const GsymReader &Reader, |
| GsymCreator &Creator, InlineInfo &II) { |
| II.Name = Creator.insertString(Reader.getString(II.Name)); |
| if (II.CallFile != 0) { |
| if (auto FE = Reader.getFile(II.CallFile)) { |
| StringRef Dir = Reader.getString(FE->Dir); |
| StringRef Base = Reader.getString(FE->Base); |
| SmallString<128> Path; |
| if (!Dir.empty()) { |
| Path = Dir; |
| llvm::sys::path::append(Path, Base); |
| } else { |
| Path = Base; |
| } |
| II.CallFile = Creator.insertFile(Path); |
| } |
| } |
| for (auto &Child : II.Children) |
| fixupInlineInfoForTransfer(Reader, Creator, Child); |
| } |
| |
| /// Transfer all function infos from a reader into a creator, re-inserting |
| /// all strings and files so that offsets are valid in the new creator. |
| static void transferFunctions(const GsymReader &Reader, GsymCreator &Creator) { |
| for (uint32_t I = 0; I < Reader.getNumAddresses(); ++I) { |
| auto FI = Reader.getFunctionInfoAtIndex(I); |
| ASSERT_THAT_EXPECTED(FI, Succeeded()); |
| |
| // Re-insert function name. |
| FI->Name = Creator.insertString(Reader.getString(FI->Name)); |
| |
| // Re-insert line table file entries. |
| if (FI->OptLineTable) { |
| for (size_t J = 0; J < FI->OptLineTable->size(); ++J) { |
| LineEntry &LE = FI->OptLineTable->get(J); |
| if (LE.File != 0) { |
| if (auto FE = Reader.getFile(LE.File)) { |
| StringRef Dir = Reader.getString(FE->Dir); |
| StringRef Base = Reader.getString(FE->Base); |
| SmallString<128> Path; |
| if (!Dir.empty()) { |
| Path = Dir; |
| llvm::sys::path::append(Path, Base); |
| } else { |
| Path = Base; |
| } |
| LE.File = Creator.insertFile(Path); |
| } |
| } |
| } |
| } |
| |
| // Re-insert inline info strings and files. |
| if (FI->Inline) |
| fixupInlineInfoForTransfer(Reader, Creator, *FI->Inline); |
| |
| Creator.addFunctionInfo(std::move(*FI)); |
| } |
| } |
| |
| /// Encode a GsymCreator to bytes. |
| static SmallString<1024> encodeCreator(const GsymCreator &GC) { |
| SmallString<1024> Str; |
| raw_svector_ostream OS(Str); |
| FileWriter FW(OS, llvm::endianness::native); |
| FW.setStringOffsetSize(GC.getStringOffsetSize()); |
| llvm::Error Err = GC.encode(FW); |
| EXPECT_FALSE(bool(Err)); |
| return Str; |
| } |
| |
| /// Collect lookup results for a set of addresses from a reader. |
| static std::vector<LookupResult> collectLookups(const GsymReader &Reader, |
| ArrayRef<uint64_t> Addrs) { |
| std::vector<LookupResult> Results; |
| for (auto Addr : Addrs) { |
| auto LR = Reader.lookup(Addr); |
| EXPECT_TRUE(bool(LR)); |
| if (LR) |
| Results.push_back(std::move(*LR)); |
| } |
| return Results; |
| } |
| |
| TEST(GSYMV2Test, TestVersionRoundTripV1ToV2ToV1) { |
| // Create a V1 GSYM with line tables and inline info. |
| GsymCreatorV1 GC1; |
| FunctionInfo FI(0x1000, 0x100, GC1.insertString("main")); |
| FI.OptLineTable = LineTable(); |
| const uint32_t MainFile = GC1.insertFile("/tmp/main.c"); |
| const uint32_t FooFile = GC1.insertFile("/tmp/foo.h"); |
| FI.OptLineTable->push(LineEntry(0x1000, MainFile, 5)); |
| FI.OptLineTable->push(LineEntry(0x1010, FooFile, 10)); |
| FI.OptLineTable->push(LineEntry(0x1020, MainFile, 8)); |
| FI.Inline = InlineInfo(); |
| FI.Inline->Name = GC1.insertString("inlined_func"); |
| FI.Inline->CallFile = MainFile; |
| FI.Inline->CallLine = 6; |
| FI.Inline->Ranges.insert(AddressRange(0x1010, 0x1020)); |
| InlineInfo NestedInline; |
| NestedInline.Name = GC1.insertString("deep_inline"); |
| NestedInline.CallFile = FooFile; |
| NestedInline.CallLine = 33; |
| NestedInline.Ranges.insert(AddressRange(0x1012, 0x1018)); |
| FI.Inline->Children.emplace_back(NestedInline); |
| GC1.addFunctionInfo(std::move(FI)); |
| |
| FunctionInfo FI2(0x1100, 0x50, GC1.insertString("helper")); |
| FI2.OptLineTable = LineTable(); |
| FI2.OptLineTable->push(LineEntry(0x1100, MainFile, 20)); |
| FI2.OptLineTable->push(LineEntry(0x1120, MainFile, 25)); |
| GC1.addFunctionInfo(std::move(FI2)); |
| |
| OutputAggregator Null(nullptr); |
| ASSERT_FALSE(bool(GC1.finalize(Null))); |
| SmallString<1024> OrigV1Bytes = encodeCreator(GC1); |
| ASSERT_GT(OrigV1Bytes.size(), 0u); |
| |
| // Read original V1. |
| auto OrigReaderOrErr = GsymReader::copyBuffer(OrigV1Bytes); |
| ASSERT_THAT_EXPECTED(OrigReaderOrErr, Succeeded()); |
| auto &OrigReader = *OrigReaderOrErr; |
| |
| // Collect lookup results from original V1. |
| std::vector<uint64_t> TestAddrs = {0x1000, 0x1008, 0x1010, 0x1012, |
| 0x1015, 0x1020, 0x1100, 0x1120}; |
| auto OrigResults = collectLookups(*OrigReader, TestAddrs); |
| ASSERT_EQ(OrigResults.size(), TestAddrs.size()); |
| |
| // Convert V1 → V2. |
| GsymCreatorV2 GC2; |
| transferFunctions(*OrigReader, GC2); |
| ASSERT_FALSE(bool(GC2.finalize(Null))); |
| SmallString<1024> V2Bytes = encodeCreator(GC2); |
| ASSERT_GT(V2Bytes.size(), 0u); |
| |
| auto V2ReaderOrErr = GsymReader::copyBuffer(V2Bytes); |
| ASSERT_THAT_EXPECTED(V2ReaderOrErr, Succeeded()); |
| auto &V2Reader = *V2ReaderOrErr; |
| |
| // Verify V2 lookups match original V1. |
| auto V2Results = collectLookups(*V2Reader, TestAddrs); |
| ASSERT_EQ(V2Results.size(), TestAddrs.size()); |
| for (size_t I = 0; I < TestAddrs.size(); ++I) |
| EXPECT_EQ(V2Results[I], OrigResults[I]) |
| << "Mismatch at address " << TestAddrs[I] << " after V1->V2"; |
| |
| // Convert V2 → V1. |
| GsymCreatorV1 GC3; |
| transferFunctions(*V2Reader, GC3); |
| ASSERT_FALSE(bool(GC3.finalize(Null))); |
| SmallString<1024> FinalV1Bytes = encodeCreator(GC3); |
| ASSERT_GT(FinalV1Bytes.size(), 0u); |
| |
| auto FinalReaderOrErr = GsymReader::copyBuffer(FinalV1Bytes); |
| ASSERT_THAT_EXPECTED(FinalReaderOrErr, Succeeded()); |
| auto &FinalReader = *FinalReaderOrErr; |
| |
| // Verify final V1 lookups match original V1. |
| auto FinalResults = collectLookups(*FinalReader, TestAddrs); |
| ASSERT_EQ(FinalResults.size(), TestAddrs.size()); |
| for (size_t I = 0; I < TestAddrs.size(); ++I) |
| EXPECT_EQ(FinalResults[I], OrigResults[I]) |
| << "Mismatch at address " << TestAddrs[I] << " after V1->V2->V1"; |
| } |
| |
| TEST(GSYMV2Test, TestVersionRoundTripV2ToV1ToV2) { |
| // Create a V2 GSYM with line tables and inline info. |
| GsymCreatorV2 GC1; |
| FunctionInfo FI(0x2000, 0x200, GC1.insertString("entry")); |
| FI.OptLineTable = LineTable(); |
| const uint32_t SrcFile = GC1.insertFile("/src/app.cc"); |
| const uint32_t HdrFile = GC1.insertFile("/src/util.h"); |
| FI.OptLineTable->push(LineEntry(0x2000, SrcFile, 10)); |
| FI.OptLineTable->push(LineEntry(0x2040, HdrFile, 50)); |
| FI.OptLineTable->push(LineEntry(0x2080, HdrFile, 55)); |
| FI.OptLineTable->push(LineEntry(0x20C0, SrcFile, 15)); |
| FI.Inline = InlineInfo(); |
| FI.Inline->Name = GC1.insertString("util_helper"); |
| FI.Inline->CallFile = SrcFile; |
| FI.Inline->CallLine = 11; |
| FI.Inline->Ranges.insert(AddressRange(0x2040, 0x20C0)); |
| InlineInfo Child; |
| Child.Name = GC1.insertString("util_detail"); |
| Child.CallFile = HdrFile; |
| Child.CallLine = 52; |
| Child.Ranges.insert(AddressRange(0x2080, 0x20A0)); |
| FI.Inline->Children.emplace_back(Child); |
| GC1.addFunctionInfo(std::move(FI)); |
| |
| FunctionInfo FI2(0x2200, 0x100, GC1.insertString("cleanup")); |
| FI2.OptLineTable = LineTable(); |
| FI2.OptLineTable->push(LineEntry(0x2200, SrcFile, 30)); |
| FI2.OptLineTable->push(LineEntry(0x2250, SrcFile, 35)); |
| GC1.addFunctionInfo(std::move(FI2)); |
| |
| OutputAggregator Null(nullptr); |
| ASSERT_FALSE(bool(GC1.finalize(Null))); |
| SmallString<1024> OrigV2Bytes = encodeCreator(GC1); |
| ASSERT_GT(OrigV2Bytes.size(), 0u); |
| |
| // Read original V2. |
| auto OrigReaderOrErr = GsymReader::copyBuffer(OrigV2Bytes); |
| ASSERT_THAT_EXPECTED(OrigReaderOrErr, Succeeded()); |
| auto &OrigReader = *OrigReaderOrErr; |
| |
| // Collect lookup results from original V2. |
| std::vector<uint64_t> TestAddrs = {0x2000, 0x2020, 0x2040, 0x2080, |
| 0x2090, 0x20C0, 0x2200, 0x2250}; |
| auto OrigResults = collectLookups(*OrigReader, TestAddrs); |
| ASSERT_EQ(OrigResults.size(), TestAddrs.size()); |
| |
| // Convert V2 → V1. |
| GsymCreatorV1 GC2; |
| transferFunctions(*OrigReader, GC2); |
| ASSERT_FALSE(bool(GC2.finalize(Null))); |
| SmallString<1024> V1Bytes = encodeCreator(GC2); |
| ASSERT_GT(V1Bytes.size(), 0u); |
| |
| auto V1ReaderOrErr = GsymReader::copyBuffer(V1Bytes); |
| ASSERT_THAT_EXPECTED(V1ReaderOrErr, Succeeded()); |
| auto &V1Reader = *V1ReaderOrErr; |
| |
| // Verify V1 lookups match original V2. |
| auto V1Results = collectLookups(*V1Reader, TestAddrs); |
| ASSERT_EQ(V1Results.size(), TestAddrs.size()); |
| for (size_t I = 0; I < TestAddrs.size(); ++I) |
| EXPECT_EQ(V1Results[I], OrigResults[I]) |
| << "Mismatch at address " << TestAddrs[I] << " after V2->V1"; |
| |
| // Convert V1 → V2. |
| GsymCreatorV2 GC3; |
| transferFunctions(*V1Reader, GC3); |
| ASSERT_FALSE(bool(GC3.finalize(Null))); |
| SmallString<1024> FinalV2Bytes = encodeCreator(GC3); |
| ASSERT_GT(FinalV2Bytes.size(), 0u); |
| |
| auto FinalReaderOrErr = GsymReader::copyBuffer(FinalV2Bytes); |
| ASSERT_THAT_EXPECTED(FinalReaderOrErr, Succeeded()); |
| auto &FinalReader = *FinalReaderOrErr; |
| |
| // Verify final V2 lookups match original V2. |
| auto FinalResults = collectLookups(*FinalReader, TestAddrs); |
| ASSERT_EQ(FinalResults.size(), TestAddrs.size()); |
| for (size_t I = 0; I < TestAddrs.size(); ++I) |
| EXPECT_EQ(FinalResults[I], OrigResults[I]) |
| << "Mismatch at address " << TestAddrs[I] << " after V2->V1->V2"; |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // Segmenting tests |
| //===----------------------------------------------------------------------===// |
| |
| TEST(GSYMV2Test, TestV2SegmentingSize) { |
| // Test that V2 segmenting produces segments whose actual encoded size |
| // does not exceed the requested segment size. This catches bugs where |
| // calculateHeaderAndTableSize() overestimates (e.g. using wrong |
| // GlobalData entry size), causing segments to contain fewer functions |
| // than they could. |
| GsymCreatorV2 GC; |
| const uint64_t BaseAddr = 0x1000; |
| // Add 10 simple functions (no line table, minimal size). |
| for (uint32_t I = 0; I < 10; ++I) { |
| std::string Name = "f" + std::to_string(I); |
| uint32_t NameOff = GC.insertString(Name); |
| GC.addFunctionInfo(FunctionInfo(BaseAddr + I * 0x100, 0x100, NameOff)); |
| } |
| OutputAggregator Null(nullptr); |
| ASSERT_FALSE(GC.finalize(Null)); |
| |
| // Create the first segment with a generous-but-bounded size. |
| // We want a size that fits all 10 functions if the estimate is correct, |
| // but might not fit all 10 if the estimate is too large. |
| // First, encode the full GSYM to know the actual total size. |
| SmallString<512> FullStr; |
| raw_svector_ostream FullOS(FullStr); |
| FileWriter FullFW(FullOS, llvm::endianness::native); |
| FullFW.setStringOffsetSize(GC.getStringOffsetSize()); |
| ASSERT_FALSE(GC.encode(FullFW)); |
| const uint64_t FullSize = FullStr.size(); |
| |
| // Use the full size as segment size — all functions should fit in one |
| // segment. If calculateHeaderAndTableSize() overestimates, some functions |
| // won't fit. |
| size_t FuncIdx = 0; |
| auto SegOrErr = GC.createSegment(FullSize, FuncIdx); |
| ASSERT_THAT_EXPECTED(SegOrErr, Succeeded()); |
| ASSERT_NE(SegOrErr->get(), nullptr); |
| // All or almost all functions should fit. The estimate may slightly |
| // overestimate due to string/file table differences between the full |
| // creator and the segment, but it should not be off by more than 1. |
| EXPECT_GE(FuncIdx, 9u) |
| << "Too few functions fit in segment — calculateHeaderAndTableSize() " |
| "may be overestimating"; |
| } |