From f6d41e7328b040722e844480055870f9587eead4 Mon Sep 17 00:00:00 2001 From: Sandy Carter Date: Wed, 22 Jul 2026 21:06:26 -0400 Subject: [PATCH 1/2] [lld][COFF] Read VC6 /Zi PDB 2.0 ("JG") type servers for --debug PDBs MSVC 6.0's /Zi writes each TU's types into an external PDB 2.0 type server (*.o.pdb), referenced from .debug$T by an old LF_TYPESERVER/ LF_TYPESERVER_ST record. PDB 2.0 is a different, older MSF container than PDB 7.0 ("DS"), so PDBFile/NativeSession can't open it, and its named type records use old "_ST" (Pascal-name) leaf kinds lld doesn't recognize -- so --debug PDBs for VC6-era objects previously got no struct/class/enum type info, only function names from COFF symbols. Adds a small from-scratch PDB 2.0 MSF + TPI-stream reader (Pdb2TypeServer.h/.cpp) that extracts the TPI stream and rewrites _ST leaf kinds to their modern equivalents in place (Pascal-length- prefixed names to null-terminated; same byte length, no reindexing). Intra-object TypeIndex references are already 0x1000-based per record position, matching what TypeIndex::toArrayIndex() expects, so no remapping is needed there. Wires this into InputFiles.cpp's LF_TYPESERVER/_ST handling as a plain generic TpiSource, bypassing TypeServerSource/NativeSession entirely for the old format. Every failure path (sidecar not found, unreadable, wrong format) falls back to an empty TpiSource rather than leaving debugTypesObj null, since PDB.cpp's writeSymbolRecord dereferences it unconditionally for any object with .debug$S symbol records. Chunks.cpp: accept CV_SIGNATURE_C11 for .debug$T specifically, so the old-format LF_TYPESERVER record isn't rejected before it can be read (.debug$S stays rejected -- VC6's C11 symbol subsection format isn't handled here). PDB.cpp: guard against empty-chunk output sections in addLinkerModuleSectionSymbol (unrelated crash hit along the way). llvm-pdbutil: fix a pre-existing build break (TpiStream::getHashValues was renamed to getHashValuesV80) hit while verifying the output PDB. Verified against a real BW1 game binary rebuild: --debug link completes cleanly and the output PDB contains real type names (enum and struct) round-tripped from VC6 object files. --- lld/COFF/CMakeLists.txt | 1 + lld/COFF/Chunks.cpp | 11 + lld/COFF/InputFiles.cpp | 90 ++- lld/COFF/PDB.cpp | 5 + lld/COFF/Pdb2TypeServer.cpp | 753 ++++++++++++++++++++ lld/COFF/Pdb2TypeServer.h | 53 ++ llvm/tools/llvm-pdbutil/DumpOutputStyle.cpp | 4 +- 7 files changed, 902 insertions(+), 15 deletions(-) create mode 100644 lld/COFF/Pdb2TypeServer.cpp create mode 100644 lld/COFF/Pdb2TypeServer.h diff --git a/lld/COFF/CMakeLists.txt b/lld/COFF/CMakeLists.txt index 2bbadf75bfa1e..7cca31ab2ed48 100644 --- a/lld/COFF/CMakeLists.txt +++ b/lld/COFF/CMakeLists.txt @@ -17,6 +17,7 @@ add_lld_library(lldCOFF MapFile.cpp MarkLive.cpp MinGW.cpp + Pdb2TypeServer.cpp PDB.cpp SymbolTable.cpp Symbols.cpp diff --git a/lld/COFF/Chunks.cpp b/lld/COFF/Chunks.cpp index ec0fdf0b67b38..0241850222e00 100644 --- a/lld/COFF/Chunks.cpp +++ b/lld/COFF/Chunks.cpp @@ -729,6 +729,17 @@ ArrayRef SectionChunk::consumeDebugMagic(ArrayRef data, ? DEBUG_HASHES_SECTION_MAGIC : DEBUG_SECTION_MAGIC; if (magic != expectedMagic) { + // MSVC 6.0 /Zi emits .debug$T with the old CodeView "C11" signature + // (CV_SIGNATURE_C11 = 2) instead of C13 (4). The record that follows is a + // normal CVType -- an old LF_TYPESERVER / LF_TYPESERVER_ST pointing at a + // PDB 2.0 type server -- which the type-server path in + // ObjFile::initializeDependencies() already handles. Let it through so + // those old type servers get merged. Everything else (notably C11 + // .debug$S, whose subsection layout the C13 reader can't parse) stays + // rejected. + constexpr uint32_t CV_SIGNATURE_C11 = 2; + if (sectionName == ".debug$T" && magic == CV_SIGNATURE_C11) + return data.slice(4); warn("ignoring section " + sectionName + " with unrecognized magic 0x" + utohexstr(magic)); return {}; diff --git a/lld/COFF/InputFiles.cpp b/lld/COFF/InputFiles.cpp index 025ca6396e052..e355b10f79fcc 100644 --- a/lld/COFF/InputFiles.cpp +++ b/lld/COFF/InputFiles.cpp @@ -7,6 +7,7 @@ //===----------------------------------------------------------------------===// #include "InputFiles.h" +#include "Pdb2TypeServer.h" #include "COFFLinkerContext.h" #include "Chunks.h" #include "Config.h" @@ -866,6 +867,11 @@ void ObjFile::initializeFlags() { } } +// Forward declaration; defined further down, needed by initializeDependencies +// to resolve a type-server path relative to this OBJ / the output file. +static std::optional +findPdbPath(StringRef pdbPath, ObjFile *dependentFile, StringRef outputPath); + // Depending on the compilation flags, OBJs can refer to external files, // necessary to merge this OBJ into the final PDB. We currently support two // types of external files: Precomp/PCH OBJs, when compiling with /Yc and /Yu. @@ -922,23 +928,81 @@ void ObjFile::initializeDependencies() { } // Handle older LF_TYPESERVER (0x1501) and LF_TYPESERVER_ST (0x0016) formats. - // These use a 4-byte CRC signature instead of a 16-byte GUID. Parse manually - // and create a TypeServer2Record with a zeroed GUID so that GUID matching is - // skipped in UseTypeServerSource::getTypeServerSource(). + // These are MSVC 6.0 /Zi objects pointing at a PDB 2.0 ("JG") type server, + // an older MSF container that PDBFile/NativeSession cannot open (hence no + // TypeServerSource/UseTypeServerSource route here, unlike the modern + // LF_TYPESERVER2 case above). Resolve the path ourselves, read the file + // directly, and hand the reindexed+fixed-up type records to this object as + // if they were a plain inline .debug$T -- see Pdb2TypeServer.h. if (firstType->kind() == LF_TYPESERVER || firstType->kind() == LF_TYPESERVER_ST) { + // Every failure path below falls back to a plain, empty TpiSource + // (mirroring the "no data" case above) rather than leaving + // debugTypesObj null: this object's .debug$S may still have real symbol + // records even though its type-server sidecar couldn't be found/opened/ + // parsed (a renamed build tree, a missing .o.pdb, an unrecognized + // format), and PDBLinker::writeSymbolRecord dereferences debugTypesObj + // unconditionally for every symbol record it processes -- leaving it + // null here would crash the whole link on what should just be "this + // object's symbols get remapped against no extra local types." ArrayRef content = firstType->content(); - // Record body: [4 bytes CRC sig][4 bytes age][null-terminated name] - if (content.size() >= 9) { - uint32_t age = support::endian::read32le(content.data() + 4); - StringRef name(reinterpret_cast(content.data() + 8)); - TypeServer2Record ts(TypeRecordKind::TypeServer2); - ts.Age = age; - ts.Name = name; - // Guid is zero-initialized; GUID matching skipped for old-style records. - debugTypesObj = makeUseTypeServerSource(ctx, this, ts); - enqueuePdbFile(ts.getName(), this); + // Record body: [4 bytes CRC sig][4 bytes age][name]. The old "_ST" form + // (MSVC 6.0 /Zi) stores the name length-prefixed (1 byte length + chars); + // the non-ST form stores it null-terminated. + if (content.size() < 9) { + debugTypesObj = makeTpiSource(ctx, this); + return; + } + StringRef name; + if (firstType->kind() == LF_TYPESERVER_ST) { + uint8_t len = content[8]; + if (9 + static_cast(len) <= content.size()) + name = StringRef(reinterpret_cast(content.data() + 9), + len); + } else { + name = StringRef(reinterpret_cast(content.data() + 8)); } + if (name.empty()) { + debugTypesObj = makeTpiSource(ctx, this); + return; + } + + std::optional path = + findPdbPath(name.str(), this, symtab.ctx.config.outputFile); + if (!path) { + Warn(ctx) << "VC6 type server not found: " << name; + debugTypesObj = makeTpiSource(ctx, this); + return; + } + ErrorOr> mbOrErr = + MemoryBuffer::getFile(*path, /*IsText=*/false, + /*RequiresNullTerminator=*/false); + if (!mbOrErr) { + Warn(ctx) << "failed to open VC6 type server " << *path << ": " + << mbOrErr.getError().message(); + debugTypesObj = makeTpiSource(ctx, this); + return; + } + MemoryBufferRef mbRef = (*mbOrErr)->getMemBufferRef(); + if (!isPdb2TypeServer(mbRef)) { + Warn(ctx) << *path << " is not a recognized PDB 2.0 type server"; + debugTypesObj = makeTpiSource(ctx, this); + return; + } + Expected> typesOrErr = + readPdb2TypeServerTypes(mbRef, bAlloc()); + if (!typesOrErr) { + Warn(ctx) << "failed to read VC6 type server " << *path << ": " + << toString(typesOrErr.takeError()); + debugTypesObj = makeTpiSource(ctx, this); + return; + } + // Keep the backing MemoryBuffer alive for the life of the link; the types + // above were copied out into bAlloc(), but nothing else needs `mbOrErr`. + ctx.driver.takeBuffer(std::move(*mbOrErr)); + + debugTypes = *typesOrErr; + debugTypesObj = makeTpiSource(ctx, this); return; } diff --git a/lld/COFF/PDB.cpp b/lld/COFF/PDB.cpp index 21475033b0ae8..ec2b3482fb340 100644 --- a/lld/COFF/PDB.cpp +++ b/lld/COFF/PDB.cpp @@ -1522,6 +1522,11 @@ static void addLinkerModuleSectionSymbol(pdb::DbiModuleDescriptorBuilder &mod, // Output COFF groups for individual chunks of this section. for (PartialSection *sec : os.contribSections) { + // A contributing section can end up with no chunks (e.g. after section + // merging such as .idata folded into .rdata); it has nothing to emit and + // would otherwise dereference an empty chunk list below. + if (sec->chunks.empty()) + continue; addLinkerModuleCoffGroup(sec, mod, os); } } diff --git a/lld/COFF/Pdb2TypeServer.cpp b/lld/COFF/Pdb2TypeServer.cpp new file mode 100644 index 0000000000000..ae32554c89848 --- /dev/null +++ b/lld/COFF/Pdb2TypeServer.cpp @@ -0,0 +1,753 @@ +//===- Pdb2TypeServer.cpp -------------------------------------*- C++ -*-===// +// +// 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 +// +//===----------------------------------------------------------------------===// +// +// See Pdb2TypeServer.h for the format background. Layout notes (all +// little-endian), reverse-engineered against real VC6 /Zi *.o.pdb files: +// +// SuperBlock (fixed offsets from file start): +// 0x00 44-byte text signature "Microsoft C/C++ program database 2.00\r\n" +// followed by 0x1a 'J' 'G' 0x00 0x00. +// 0x2C u32 pageSize (observed 0x1000) +// 0x30 u16 startPage (unused here) +// 0x32 u16 filePages (unused here) +// 0x34 u32 rootSize (byte size of the root/stream-directory +// stream) +// 0x38 u32 reserved +// 0x3C u16[ceil(rootSize/pageSize)] page numbers holding the root stream +// +// Root stream ("stream directory"), once reassembled from its pages: +// u32 numStreams +// numStreams * { u32 size; u32 reserved; } -- per-stream byte size +// then, for every stream with size != 0 (in stream-index order), that +// stream's page numbers as u16s, ceil(size/pageSize) of them. +// +// Stream #2 is the TPI (types) stream. Its own header is a fixed 0x38 (56) +// bytes; empirically: +// 0x00 u32 version (observed 19961031) +// 0x04 u32 typeIndexBegin (observed 56 -- NOT the modern 0x1000) +// 0x08 u32 typeIndexEnd +// 0x10 u32 typeRecordBytes (byte length of the record array that +// follows the header; verified to exactly +// match "TPI stream size - 56" on samples) +// Bytes [0x38, 0x38+typeRecordBytes) are then a plain, tightly-packed +// CVTypeArray -- [u16 length][u16 leaf][...body...] repeated, length not +// including itself. Leaf kinds without an embedded name (LF_MODIFIER, +// LF_POINTER, LF_ARGLIST, LF_PROCEDURE, LF_FIELDLIST itself, ...) use +// ordinary *modern* (0x1000+) kind values and field layouts. Leaf kinds that +// DO carry a name (LF_STRUCTURE/CLASS/UNION, LF_MEMBER, and siblings) use +// the old "_ST" kind values instead (e.g. LF_STRUCTURE_ST = 0x1005, not +// modern LF_STRUCTURE = 0x1505) with an otherwise-identical field layout, +// except the trailing name is Pascal-style length-prefixed rather than +// null-terminated. See stFixupFor() below for the kinds this reader +// recognizes and rewrites to modern form. +// Stream #3 (conventionally DBI/symbols) is empty in every sample, matching +// "type server PDBs do not contain symbols" (see DebugTypes.cpp). + +#include "Pdb2TypeServer.h" + +#include "llvm/DebugInfo/CodeView/CVRecord.h" +#include "llvm/DebugInfo/CodeView/CodeView.h" +#include "llvm/DebugInfo/CodeView/RecordSerialization.h" +#include "llvm/DebugInfo/CodeView/TypeIndex.h" +#include "llvm/Support/Endian.h" + +using namespace llvm; +using namespace llvm::codeview; +using namespace llvm::support; + +namespace lld::coff { + +static const char pdb2Magic[] = "Microsoft C/C++ program database 2.00\r\n"; +static constexpr size_t pdb2MagicLen = sizeof(pdb2Magic) - 1; // no NUL +static constexpr size_t tpiStreamIndex = 2; +static constexpr size_t pdb2TpiHeaderSize = 0x38; + +static Error err(const Twine &msg) { + return createStringError("VC6 PDB 2.0 type server: " + msg.str()); +} + +bool isPdb2TypeServer(MemoryBufferRef mb) { + StringRef buf = mb.getBuffer(); + return buf.size() > pdb2MagicLen && + buf.substr(0, pdb2MagicLen) == StringRef(pdb2Magic, pdb2MagicLen); +} + +namespace { +struct Pdb2File { + ArrayRef data; + uint32_t pageSize; + + ArrayRef page(uint32_t p) const { + uint64_t off = uint64_t(p) * pageSize; + if (off + pageSize > data.size()) + return {}; + return data.slice(off, pageSize); + } + + // Concatenate `count` pages starting at `pages[0..count)`, then trim to + // `byteSize`. Fails loudly (rather than silently dropping the page and + // shifting every later page's bytes left by one page width) if any page + // index is out of range -- this is the first parsing surface touching raw + // untrusted bytes off disk, not output from a trusted compiler run. + Expected> readStream(ArrayRef pages, + uint32_t byteSize) const { + std::vector out; + out.reserve(byteSize); + for (uint16_t p : pages) { + ArrayRef pg = page(p); + if (pg.empty()) + return err("page index " + Twine(p) + " is out of range"); + out.insert(out.end(), pg.begin(), pg.end()); + } + if (out.size() > byteSize) + out.resize(byteSize); + return out; + } +}; +} // namespace + +// Parses the SuperBlock + stream directory and returns the raw bytes of +// stream `tpiStreamIndex` (the TPI stream), still including its own 0x38-byte +// header. +static Expected> readTpiStreamRaw(MemoryBufferRef mb) { + ArrayRef data( + reinterpret_cast(mb.getBufferStart()), + mb.getBufferSize()); + if (data.size() < 0x40) + return err("file too short for a SuperBlock"); + + uint32_t pageSize = support::endian::read32le(data.data() + 0x2C); + uint32_t rootSize = support::endian::read32le(data.data() + 0x34); + if (pageSize == 0 || pageSize > (1u << 20)) + return err("implausible page size " + Twine(pageSize)); + + Pdb2File pdb{data, pageSize}; + + uint32_t rootPageCount = (rootSize + pageSize - 1) / pageSize; + if (0x3C + uint64_t(rootPageCount) * 2 > data.size()) + return err("stream directory page list runs off the end of the file"); + ArrayRef rootPages( + reinterpret_cast(data.data() + 0x3C), + rootPageCount); + + Expected> rootOrErr = pdb.readStream(rootPages, rootSize); + if (!rootOrErr) + return rootOrErr.takeError(); + std::vector &root = *rootOrErr; + if (root.size() < 4) + return err("stream directory too short"); + + uint32_t numStreams = support::endian::read32le(root.data()); + // Per-stream: u32 size, u32 reserved. + if (4 + uint64_t(numStreams) * 8 > root.size()) + return err("stream directory truncated (sizes table)"); + + std::vector sizes(numStreams); + for (uint32_t i = 0; i < numStreams; ++i) + sizes[i] = support::endian::read32le(root.data() + 4 + i * 8); + + if (tpiStreamIndex >= numStreams) + return err("file has no TPI stream (index " + Twine(tpiStreamIndex) + + " >= " + Twine(numStreams) + " streams)"); + + // Page numbers for all streams are concatenated after the sizes table, in + // stream-index order, skipping any stream whose size is 0. + size_t p = 4 + size_t(numStreams) * 8; + ArrayRef tpiPages; + for (uint32_t i = 0; i < numStreams; ++i) { + uint32_t pages = (sizes[i] + pageSize - 1) / pageSize; + if (p + pages * 2 > root.size()) + return err("stream directory truncated (page list)"); + if (i == tpiStreamIndex) + tpiPages = ArrayRef( + reinterpret_cast(root.data() + p), + pages); + p += pages * 2; + } + + return pdb.readStream(tpiPages, sizes[tpiStreamIndex]); +} + +// Converts a record's embedded name from Pascal-style (length-prefixed) to +// null-terminated, in place, given the fixed byte size of whatever +// non-name fields precede it (`prefixSize`, counted from the start of the +// record including its [len][kind] header). Both encodings occupy exactly +// N+1 bytes for an N-character name, so this never changes the record's +// length or shifts any other byte. Returns false (record left untouched) if +// the shape doesn't match what we know how to parse -- callers should treat +// that as "name stays Pascal-encoded", not fatal. +static bool fixupPascalNameDirectAt(MutableArrayRef rec, + size_t nameOffset) { + if (nameOffset >= rec.size()) + return false; + uint8_t nameLen = rec[nameOffset]; + if (nameOffset + 1 + nameLen > rec.size()) + return false; // malformed / not actually a Pascal name here + // Shift the N name chars left by one (over the length byte), then null- + // terminate in what used to be the last name byte's slot. + uint8_t *name = rec.data() + nameOffset; + for (uint8_t i = 0; i < nameLen; ++i) + name[i] = name[i + 1]; + name[nameLen] = 0; + return true; +} + +// CodeView numeric-leaf encoding: values < LF_NUMERIC (0x8000) are a plain +// u16; values >= 0x8000 are a 2-byte discriminator tag followed by the value +// itself. Mirrors llvm::codeview::consume(BinaryStreamReader&, APSInt&) in +// RecordSerialization.cpp -- which is itself the authority here, since it's +// the same fixed set that consumer accepts (member offsets / enum values / +// array sizes are always integral, so LF_REAL*/LF_COMPLEX* etc never appear +// in this position and neither llvm nor this reader need to handle them). +// Returns false (uncommon/unsupported encoding) if `firstWord` isn't one of +// these tags and isn't a plain u16. +static bool numericLeafSize(uint16_t firstWord, size_t &size) { + if (firstWord < LF_NUMERIC) { + size = 2; // plain u16 numeric leaf + return true; + } + switch (firstWord) { + case LF_CHAR: // u16 discriminator + i8 + size = 3; + return true; + case LF_SHORT: // u16 discriminator + i16 + case LF_USHORT: // u16 discriminator + u16 + size = 4; + return true; + case LF_LONG: // u16 discriminator + i32 + case LF_ULONG: // u16 discriminator + u32 + size = 6; + return true; + case LF_REAL32: // u16 discriminator + 4-byte float + size = 6; + return true; + case LF_REAL64: // u16 discriminator + 8-byte double + size = 10; + return true; + case LF_REAL80: // u16 discriminator + 10-byte extended + size = 12; + return true; + case LF_REAL128: // u16 discriminator + 16-byte float128 + size = 18; + return true; + case LF_QUADWORD: // u16 discriminator + i64 + case LF_UQUADWORD: // u16 discriminator + u64 + size = 10; + return true; + default: + return false; // uncommon numeric-leaf encoding; leave name as-is + } +} + +// As above, but `prefixSize` is followed by a CodeView numeric-leaf-encoded +// value (e.g. a member offset or enum value) before the Pascal name, whose +// own byte width must be determined first. +static bool fixupPascalNameAt(MutableArrayRef rec, + size_t prefixSize) { + if (rec.size() < prefixSize + 3) + return false; + size_t p = prefixSize; + uint16_t firstWord = support::endian::read16le(rec.data() + p); + size_t numericFieldSize; + if (!numericLeafSize(firstWord, numericFieldSize)) + return false; + p += numericFieldSize; + return fixupPascalNameDirectAt(rec, p); +} + +// LF_MEMBER: [len:2][kind:2][attr:2][type:4][offset-numeric-leaf][name]... +static bool fixupMemberName(MutableArrayRef rec) { + return fixupPascalNameAt(rec, /*len+kind+attr+type=*/4 + 2 + 4); +} + +// LF_FIELDLIST member sub-records have no [len:2] of their own (unlike +// top-level records): they're packed back-to-back as [kind:2][...], each +// individually padded up to a multiple of 4 with an LF_PAD1..3 filler byte, +// and the whole sequence's extent is implied by the *outer* FIELDLIST +// record's declared length. See fixupFieldListMembers, which computes each +// member's numeric-field offset (shape-dependent -- e.g. 8 for LF_MEMBER's +// [kind:2][attr:2][type:4], 4 for LF_ENUMERATE's [kind:2][attrs:2]) and +// calls fixupPascalNameAt directly rather than going through a per-kind +// wrapper like the top-level fixups below. + +// LF_STRUCTURE/LF_CLASS (identical shape up to the name; verified against +// llvm's TypeRecordMapping::visitKnownRecord(ClassRecord&) in +// TypeRecordMapping.cpp, which is the ground truth for what the ghash-based +// hashTypeRecord()/TypeDeserializer path expects on read): +// [len:2][kind:2][count:2][properties:2][fieldlist:4][derived:4][vshape:4] +// [size-numeric-leaf][name]... +static bool fixupStructureLikeName(MutableArrayRef rec) { + return fixupPascalNameAt(rec, /*len+kind+count+props+3 TypeIndices=*/ + 4 + 2 + 2 + 4 + 4 + 4); +} + +// LF_UNION: unlike LF_STRUCTURE/LF_CLASS, has no DerivedFrom or VShape +// TypeIndex field (TypeRecordMapping::visitKnownRecord(UnionRecord&): +// MemberCount, Options, FieldList, SizeOf, Name -- no DerivationList/ +// VTableShape). Do not merge this with fixupStructureLikeName; the prefix +// is 8 bytes shorter. +// [len:2][kind:2][count:2][properties:2][fieldlist:4][size-numeric-leaf][name]... +static bool fixupUnionName(MutableArrayRef rec) { + return fixupPascalNameAt(rec, /*len+kind+count+props+fieldlist=*/ + 4 + 2 + 2 + 4); +} + +// LF_ARRAY: [len:2][kind:2][elementType:4][indexType:4][size-numeric-leaf] +// [name]... Usually anonymous (empty Pascal name: a 0x00 length byte) since +// arrays are typically embedded as another record's type field rather than +// named in their own right, but the shape (and thus the fixup) is the same +// either way. +static bool fixupArrayLikeName(MutableArrayRef rec) { + return fixupPascalNameAt(rec, /*len+kind+elementType+indexType=*/ + 4 + 4 + 4); +} + +// LF_ENUM: [len:2][kind:2][count:2][properties:2][underlyingType:4] +// [fieldlist:4][name]... Per TypeRecordMapping::visitKnownRecord(EnumRecord&): +// MemberCount, Options, UnderlyingType, FieldList, then straight into the +// name -- unlike LF_STRUCTURE/CLASS/UNION/LF_ARRAY, there is no SizeOf/ +// encoded-integer field (an enum's size is implicit from UnderlyingType), so +// the name is NOT preceded by a numeric leaf. Use fixupPascalNameDirectAt +// here, not fixupPascalNameAt. +static bool fixupEnumName(MutableArrayRef rec) { + return fixupPascalNameDirectAt( + rec, /*len+kind+count+props+underlying+fieldlist=*/ + 4 + 2 + 2 + 4 + 4); +} + +// LF_ENUMERATE_ST only ever appears as a nested field-list member (see +// fixupFieldListMembers, which handles it directly via fixupPascalNameAt +// with the nested prefix size); this top-level-shaped wrapper exists only so +// stFixupFor's entry for it has a valid, safe fixName, in case it were ever +// (incorrectly) encountered as a top-level record's own kind. +static bool fixupEnumeratorNameTopLevel(MutableArrayRef rec) { + return fixupPascalNameAt(rec, /*len+kind+attrs=*/4 + 2); +} + +// The following three are, like LF_MEMBER/LF_ENUMERATE, nested-only (no +// top-level form): fixupFieldListMembers dispatches them directly via +// fixupPascalNameDirectAt (no numeric-leaf value field precedes their name, +// unlike LF_MEMBER/LF_ENUMERATE), so these top-level-shaped wrappers exist +// only for a valid, safe stFixupFor::fixName entry. +// +// LF_STMEMBER: [kind:2][attrs:2][type:4][name]... (static data member: a +// plain TypeIndex, no offset -- statics don't live at a fixed in-object +// offset). +static bool fixupStaticMemberNameTopLevel(MutableArrayRef rec) { + return fixupPascalNameDirectAt(rec, /*len+kind+attrs+type=*/4 + 2 + 4); +} + +// LF_METHOD (overload set): [kind:2][count:2][methodList:4][name]... +static bool fixupMethodNameTopLevel(MutableArrayRef rec) { + return fixupPascalNameDirectAt(rec, /*len+kind+count+methodList=*/4 + 2 + 4); +} + +// LF_NESTTYPE: [kind:2][padding:2][type:4][name]... +static bool fixupNestedTypeNameTopLevel(MutableArrayRef rec) { + return fixupPascalNameDirectAt(rec, /*len+kind+padding+type=*/4 + 2 + 4); +} + +// LF_ONEMETHOD: [kind:2][attrs:2][type:4][vftableOffset:4 -- ONLY present if +// attrs' MethodKind sub-field says this method introduces a virtual +// function][name]... The MethodKind occupies bits 2-4 of attrs +// (MethodOptions::MethodKindMask, shifted right by 2); MethodKind:: +// IntroducingVirtual or MethodKind::PureIntroducingVirtual means the 4-byte +// VFTableOffset field is present, any other kind means it's omitted -- see +// MemberAttributes::getMethodKind() / OneMethodRecord::isIntroducingVirtual() +// in TypeRecord.h. Unlike the other nested kinds, this one has a genuinely +// conditional prefix length, so it isn't expressible as a single fixed +// offset -- computed directly rather than via nestedShapeFor/NestedShape. +static bool isIntroducingVirtualMethodKind(uint16_t attrs) { + auto methodKind = MethodKind( + (attrs & uint16_t(MethodOptions::MethodKindMask)) >> 2); + return methodKind == MethodKind::IntroducingVirtual || + methodKind == MethodKind::PureIntroducingVirtual; +} +static size_t oneMethodPrefixSize(uint16_t attrs, size_t base) { + return base + (isIntroducingVirtualMethodKind(attrs) ? 4 : 0); +} +// Top-level-shaped wrapper for stFixupFor::fixName (see comment on the +// STMEMBER/METHOD/NESTTYPE wrappers above); LF_ONEMETHOD_ST normally only +// appears nested, handled directly in fixupFieldListMembers. +static bool fixupOneMethodNameTopLevel(MutableArrayRef rec) { + if (rec.size() < 6) + return false; + uint16_t attrs = support::endian::read16le(rec.data() + 4); // len+kind+attrs + return fixupPascalNameDirectAt(rec, oneMethodPrefixSize(attrs, 4 + 2 + 4)); +} + +// Three more nested-only field-list member kinds, assumed to share the same +// [kind:2][word:2][type:4][name]... shape as LF_STMEMBER/LF_METHOD/LF_NESTTYPE +// above (no numeric leaf): friend-function declarations, "extended" +// nested-type declarations (adds access attrs where LF_NESTTYPE has only +// padding), and member-modify records (multiple-inheritance override +// bookkeeping). None of these three have a TYPE_RECORD/MEMBER_RECORD entry +// in CodeViewTypes.def, so unlike LF_STMEMBER/LF_METHOD/LF_NESTTYPE/ +// LF_ONEMETHOD there's no TypeRecordMapping.cpp shape to verify against -- +// this is cvinfo.h-by-analogy, unverified (same caveat as fixupAliasName +// below). Same top-level-shaped-wrapper caveat as the others -- real +// occurrences are nested, handled via nestedShapeFor/fixupFieldListMembers. +static bool fixupFriendFcnNameTopLevel(MutableArrayRef rec) { + return fixupPascalNameDirectAt(rec, /*len+kind+pad+type=*/4 + 2 + 4); +} +static bool fixupNestTypeExNameTopLevel(MutableArrayRef rec) { + return fixupPascalNameDirectAt(rec, /*len+kind+attrs+type=*/4 + 2 + 4); +} +static bool fixupMemberModifyNameTopLevel(MutableArrayRef rec) { + return fixupPascalNameDirectAt(rec, /*len+kind+pad+type=*/4 + 2 + 4); +} + +// LF_ALIAS (typedef): [len:2][kind:2][utype:4][name]... -- genuinely +// top-level only (a typedef isn't a field-list member). No modern +// TypeRecord/deserializer class exists for this in llvm (only the raw +// CV_TYPE kind enum value), so downstream consumers see it as an opaque +// record; that's fine, all we need is a well-formed length/kind/name. +static bool fixupAliasName(MutableArrayRef rec) { + return fixupPascalNameDirectAt(rec, /*len+kind+utype=*/4 + 4); +} + +// LF_DEFARG (default argument expression, e.g. `void F(int x = 5)`): +// [len:2][kind:2][type:4][expr]... `expr` is Pascal/null-terminated source +// text (not a symbol name), but the fixup is byte-identical. Top-level only +// (referenced by an LF_ARGLIST/method's argument list, not a field member). +// Like LF_ALIAS below, LF_DEFARG has no TYPE_RECORD entry in +// CodeViewTypes.def and thus no TypeRecordMapping.cpp shape to check +// against -- this is old cvinfo.h-by-memory reasoning, unverified against +// llvm ground truth. +static bool fixupDefArgName(MutableArrayRef rec) { + return fixupPascalNameDirectAt(rec, /*len+kind+type=*/4 + 4); +} + +// LF_DIMARRAY (multi-dimensional array, rare -- Fortran-oriented, unlikely +// in VC6 C/C++ but cheap to support): [len:2][kind:2][elementType:4] +// [dimInfo:4][name]... Top-level only. Also no TypeRecordMapping.cpp entry +// to check against -- same unverified-against-llvm-ground-truth caveat as +// LF_ALIAS/LF_DEFARG. +static bool fixupDimArrayName(MutableArrayRef rec) { + return fixupPascalNameDirectAt(rec, /*len+kind+elementType+dimInfo=*/ + 4 + 4 + 4); +} + +// LF_PRECOMP (precompiled-header type-range reference): [len:2][kind:2] +// [start:4][count:4][signature:4][name]... Matches llvm's PrecompRecord +// (StartTypeIndex/TypesCount/Signature/PrecompFilePath). Top-level only. +// May be unused if bw1-decomp's objects don't use /Yc /Yu PCH, but handled +// for completeness rather than left as a silent gap. +static bool fixupPrecompName(MutableArrayRef rec) { + return fixupPascalNameDirectAt(rec, /*len+kind+start+count+signature=*/ + 4 + 4 + 4 + 4); +} + +// Old "_ST" (Pascal-name) leaf kinds whose non-name field layout is +// otherwise identical to a validated modern counterpart: the modern kind +// they should be rewritten to (so the rest of LLVM's CodeView tooling +// recognizes them), paired with the name-fixup shape to apply. +struct StStructureFixup { + TypeLeafKind modernKind; + bool (*fixName)(MutableArrayRef); +}; +static std::optional stFixupFor(uint16_t leaf) { + switch (leaf) { + case LF_STRUCTURE_ST: + return StStructureFixup{LF_STRUCTURE, fixupStructureLikeName}; + case LF_CLASS_ST: + return StStructureFixup{LF_CLASS, fixupStructureLikeName}; + case LF_UNION_ST: + return StStructureFixup{LF_UNION, fixupUnionName}; + case LF_MEMBER_ST: + return StStructureFixup{LF_MEMBER, fixupMemberName}; + case LF_ARRAY_ST: + return StStructureFixup{LF_ARRAY, fixupArrayLikeName}; + case LF_ENUM_ST: + return StStructureFixup{LF_ENUM, fixupEnumName}; + case LF_ENUMERATE_ST: // normally only nested; see fixName comment + return StStructureFixup{LF_ENUMERATE, fixupEnumeratorNameTopLevel}; + case LF_STMEMBER_ST: // normally only nested; see fixName comment + return StStructureFixup{LF_STMEMBER, fixupStaticMemberNameTopLevel}; + case LF_METHOD_ST: // normally only nested; see fixName comment + return StStructureFixup{LF_METHOD, fixupMethodNameTopLevel}; + case LF_NESTTYPE_ST: // normally only nested; see fixName comment + return StStructureFixup{LF_NESTTYPE, fixupNestedTypeNameTopLevel}; + case LF_ONEMETHOD_ST: // normally only nested; see fixName comment + return StStructureFixup{LF_ONEMETHOD, fixupOneMethodNameTopLevel}; + case LF_FRIENDFCN_ST: // normally only nested; see fixName comment + return StStructureFixup{LF_FRIENDFCN, fixupFriendFcnNameTopLevel}; + case LF_NESTTYPEEX_ST: // normally only nested; see fixName comment + return StStructureFixup{LF_NESTTYPEEX, fixupNestTypeExNameTopLevel}; + case LF_MEMBERMODIFY_ST: // normally only nested; see fixName comment + return StStructureFixup{LF_MEMBERMODIFY, fixupMemberModifyNameTopLevel}; + case LF_ALIAS_ST: // typedef; top-level only + return StStructureFixup{LF_ALIAS, fixupAliasName}; + case LF_DEFARG_ST: // default argument expression; top-level only + return StStructureFixup{LF_DEFARG, fixupDefArgName}; + case LF_DIMARRAY_ST: // top-level only + return StStructureFixup{LF_DIMARRAY, fixupDimArrayName}; + case LF_PRECOMP_ST: // top-level only + return StStructureFixup{LF_PRECOMP, fixupPrecompName}; + // LF_MANAGED_ST: .NET/managed-code metadata reference. Never emitted by + // VC6 compiling native C/C++ (no /clr in this era), so deliberately left + // unhandled rather than guessed at -- not a gap for this project's actual + // inputs. + default: + return std::nullopt; + } +} + +// Walks a decoded LF_FIELDLIST's content (immediately after its [len][kind] +// header) member-by-member -- mirroring llvm's own handleFieldList() +// size/padding logic -- and rewrites each recognized "_ST" member's kind to +// modern in place, fixing up its Pascal name in the same pass, so the rest +// of LLVM's CodeView tooling recognizes it. Stops (leaving the remainder +// untouched) at the first member kind we don't know the shape of. + +// Describes where a nested member's name-area begins (byte offset from the +// member's own [kind:2]) and whether a CodeView numeric-leaf-encoded value +// (whose own byte width must be resolved first) precedes the Pascal name +// there, for each nested member kind this function knows how to size. +struct NestedShape { + size_t prefixSize; + bool hasNumericLeaf; +}; +static std::optional nestedShapeFor(TypeLeafKind modernKind) { + switch (modernKind) { + case LF_MEMBER: + return NestedShape{8, true}; // [kind:2][attrs:2][type:4] + numeric-leaf + case LF_ENUMERATE: + return NestedShape{4, true}; // [kind:2][attrs:2] + numeric-leaf + case LF_STMEMBER: + return NestedShape{8, false}; // [kind:2][attrs:2][type:4], no leaf + case LF_METHOD: + return NestedShape{8, false}; // [kind:2][count:2][methodList:4], no leaf + case LF_NESTTYPE: + return NestedShape{8, false}; // [kind:2][padding:2][type:4], no leaf + case LF_FRIENDFCN: + return NestedShape{8, false}; // [kind:2][padding:2][type:4], no leaf + case LF_NESTTYPEEX: + return NestedShape{8, false}; // [kind:2][attrs:2][type:4], no leaf + case LF_MEMBERMODIFY: + return NestedShape{8, false}; // [kind:2][padding:2][type:4], no leaf + default: + return std::nullopt; + } +} + +// LF_BCLASS/LF_VBCLASS/LF_IVBCLASS/LF_VFUNCTAB/LF_INDEX have no embedded +// name in EITHER old or new CodeView, so there's no "_ST" sibling for them +// (they don't appear in stFixupFor) and no Pascal-name fixup is needed -- +// they're already byte-identical to their modern form. But the field-list +// walker still needs their size to skip over them and keep going, mirroring +// llvm's own handleBaseClass/handleVirtualBaseClass/handleVFPtr/ +// handleListContinuation in TypeIndexDiscovery.cpp. Without this, the walker +// would treat any class with a base class (i.e. almost any class using +// inheritance), a virtual base, or a vtable pointer as "unknown member kind" +// and bail immediately, leaving the rest of the field list un-fixed-up -- +// this was the actual cause of real corruption downstream (a Pascal-encoded +// name misread as null-terminated by later tooling), not any of the "_ST" +// leaf kinds. +static std::optional fixedFieldListMemberSize(uint16_t kind, + ArrayRef data) { + switch (TypeLeafKind(kind)) { + case LF_BCLASS: + // [kind:2][attrs:2][type:4][offset-numeric-leaf] + if (data.size() < 10) + return std::nullopt; + { + size_t n; + if (!numericLeafSize(support::endian::read16le(data.data() + 8), n)) + return std::nullopt; + return 8 + n; + } + case LF_VBCLASS: + case LF_IVBCLASS: { + // [kind:2][attrs:2][baseType:4][vbptrType:4][vbpOffset-leaf][vbOffset-leaf] + if (data.size() < 14) + return std::nullopt; + size_t n1; + if (!numericLeafSize(support::endian::read16le(data.data() + 12), n1)) + return std::nullopt; + size_t after1 = 12 + n1; + if (data.size() < after1 + 2) + return std::nullopt; + size_t n2; + if (!numericLeafSize(support::endian::read16le(data.data() + after1), n2)) + return std::nullopt; + return after1 + n2; + } + case LF_VFUNCTAB: + // [kind:2][padding:2][type:4] + return data.size() >= 8 ? std::optional(8) : std::nullopt; + case LF_INDEX: + // [kind:2][padding:2][type:4] (points at a continuation LF_FIELDLIST) + return data.size() >= 8 ? std::optional(8) : std::nullopt; + default: + return std::nullopt; + } +} + +// Note: hitting one unrecognized member kind stops conversion for every +// member *after* it too, not just that one -- a single unexpected kind +// partway through a class definition leaves the remaining, otherwise +// convertible members still Pascal-encoded. Not corruption (consistent with +// this project's "leave broken, don't fake it" philosophy), but the blast +// radius is bigger than "one member" if it ever happens. +static void fixupFieldListMembers(MutableArrayRef content) { + size_t p = 0; + while (p + 2 <= content.size()) { + uint16_t kind = support::endian::read16le(content.data() + p); + + if (std::optional size = + fixedFieldListMemberSize(kind, content.slice(p))) { + p += *size; + if (p < content.size() && content[p] >= LF_PAD0) + p += content[p] & 0x0F; + continue; + } + + std::optional fx = stFixupFor(kind); + if (!fx) + return; // unknown/unhandled nested member kind; stop here + + // LF_ONEMETHOD has a genuinely conditional prefix length (see + // oneMethodPrefixSize), not expressible via the fixed-offset NestedShape + // table below, so it's handled as its own case. + if (fx->modernKind == LF_ONEMETHOD) { + if (p + 8 > content.size()) + return; + uint16_t attrs = support::endian::read16le(content.data() + p + 2); + // [kind:2][attrs:2][type:4] = 8-byte fixed prefix, then optionally a + // 4-byte VFTableOffset, then the name -- see oneMethodPrefixSize. + size_t nameOff = oneMethodPrefixSize(attrs, p + 8); + if (nameOff >= content.size()) + return; + uint8_t nameLen = content[nameOff]; + if (nameOff + 1 + nameLen > content.size()) + return; + size_t memberLen = (nameOff + 1 + nameLen) - p; + + support::endian::write16le(content.data() + p, uint16_t(fx->modernKind)); + fixupPascalNameDirectAt(content.slice(p, memberLen), + nameOff - p); + + p += memberLen; + if (p < content.size() && content[p] >= LF_PAD0) + p += content[p] & 0x0F; + continue; + } + + std::optional shape = nestedShapeFor(fx->modernKind); + if (!shape) + return; // unknown/unhandled nested member shape; stop here + + size_t q = p + shape->prefixSize; + if (shape->hasNumericLeaf) { + // Recompute this member's length the same way handleDataMember() / + // handleEnumerator() do: prefix + encoded-integer + C-string name. + if (q + 2 > content.size()) + return; + uint16_t firstWord = support::endian::read16le(content.data() + q); + size_t numericFieldSize; + if (!numericLeafSize(firstWord, numericFieldSize)) + return; // uncommon numeric-leaf encoding; stop here + q += numericFieldSize; + } + if (q >= content.size()) + return; + uint8_t nameLen = content[q]; + if (q + 1 + nameLen > content.size()) + return; + size_t memberLen = (q + 1 + nameLen) - p; + + support::endian::write16le(content.data() + p, uint16_t(fx->modernKind)); + if (shape->hasNumericLeaf) + fixupPascalNameAt(content.slice(p, memberLen), shape->prefixSize); + else + fixupPascalNameDirectAt(content.slice(p, memberLen), shape->prefixSize); + + p += memberLen; + if (p < content.size() && content[p] >= LF_PAD0) + p += content[p] & 0x0F; // skip LF_PAD1..3 filler, same as handleFieldList + } +} + +Expected> readPdb2TypeServerTypes(MemoryBufferRef mb, + BumpPtrAllocator &alloc) { + Expected> tpiOrErr = readTpiStreamRaw(mb); + if (!tpiOrErr) + return tpiOrErr.takeError(); + std::vector &tpi = *tpiOrErr; + + if (tpi.empty()) { + // A genuinely empty TPI stream means this TU defines no local types of + // its own (observed for real /Zi objects, e.g. one that only uses types + // already known from elsewhere) -- valid and common, not an error. + return ArrayRef(); + } + if (tpi.size() < pdb2TpiHeaderSize) + return err("TPI stream shorter than its own header"); + + uint32_t typeIndexBegin = support::endian::read32le(tpi.data() + 4); + uint32_t typeIndexEnd = support::endian::read32le(tpi.data() + 8); + uint32_t typeRecordBytes = support::endian::read32le(tpi.data() + 16); + if (typeIndexBegin == 0 || typeIndexBegin > TypeIndex::FirstNonSimpleIndex) + return err("implausible TypeIndexBegin " + Twine(typeIndexBegin)); + if (typeIndexEnd < typeIndexBegin) + return err("TypeIndexEnd " + Twine(typeIndexEnd) + + " precedes TypeIndexBegin " + Twine(typeIndexBegin)); + if (pdb2TpiHeaderSize + uint64_t(typeRecordBytes) > tpi.size()) + return err("type record area runs past the end of the TPI stream"); + + // Copy just the record bytes out into an LLD-owned, mutable buffer -- this + // becomes the object's new `debugTypes`. + uint8_t *buf = alloc.Allocate(typeRecordBytes); + memcpy(buf, tpi.data() + pdb2TpiHeaderSize, typeRecordBytes); + MutableArrayRef out(buf, typeRecordBytes); + + // Embedded TypeIndex references inside this stream's records are already + // encoded exactly the way a modern object's local .debug$T would encode + // them: `0x1000 + N` where N is the 0-based sequential position of the + // referenced record within this same stream -- e.g. record position 1's + // LF_POINTER has `referent = 0x1000` (position 0); position 3's LF_POINTER + // has `referent = 0x1002` (position 2); matching what + // TypeIndex::toArrayIndex() (`value - 0x1000`) expects. No rewriting of + // TypeIndex values is needed. typeIndexBegin/typeIndexEnd (see + // readTpiStreamRaw) are bookkeeping internal to the PDB 2.0 container's + // own record layout, not a numbering embedded references use. The only + // transformation needed is rewriting old "_ST" (Pascal-name) leaf kinds to + // their modern equivalents. + size_t pos = 0; + while (pos < out.size()) { + if (pos + sizeof(RecordPrefix) > out.size()) + return err("type record truncated at offset " + Twine(pos)); + uint16_t length = support::endian::read16le(out.data() + pos); + if (pos + 2 + length > out.size()) + return err("type record overruns stream at offset " + Twine(pos)); + MutableArrayRef rec = out.slice(pos, 2 + length); + + // Old "_ST" (Pascal-name) records must be rewritten to their modern + // kind so the rest of LLVM's CodeView tooling recognizes them. Only + // commit the kind rewrite if the name fixup actually succeeds -- fixName + // can fail if this record's shape doesn't match what we assumed (e.g. an + // untested/speculative kind with a wrong byte-offset guess); if we wrote + // the modern kind regardless, the record would end up with a modern tag + // but a still-Pascal-encoded name, which is silently self-inconsistent + // rather than caught (the same corruption class the LF_UNION_ST and + // LF_ENUM_ST bugs produced, just with no code path left to notice it). + std::optional stFixup = + stFixupFor(support::endian::read16le(rec.data() + 2)); + if (stFixup && stFixup->fixName(rec)) + support::endian::write16le(rec.data() + 2, uint16_t(stFixup->modernKind)); + if (CVType(rec).kind() == LF_FIELDLIST) + fixupFieldListMembers(rec.drop_front(sizeof(RecordPrefix))); + + pos += 2 + length; + } + + return ArrayRef(out); +} + +} // namespace lld::coff diff --git a/lld/COFF/Pdb2TypeServer.h b/lld/COFF/Pdb2TypeServer.h new file mode 100644 index 0000000000000..42203e826d3fe --- /dev/null +++ b/lld/COFF/Pdb2TypeServer.h @@ -0,0 +1,53 @@ +//===- Pdb2TypeServer.h ---------------------------------------*- C++ -*-===// +// +// 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 +// +//===----------------------------------------------------------------------===// +// +// MSVC 6.0 `/Zi` writes each translation unit's types into an external +// PDB 2.0 ("JG") type server (a `*.o.pdb` referenced from the object's +// `.debug$T` by an old LF_TYPESERVER/LF_TYPESERVER_ST record). PDB 2.0 is a +// different, older MSF container than the PDB 7.0 ("DS") format the rest of +// LLVM's PDB/MSF libraries assume, so it cannot be opened via PDBFile / +// NativeSession; this file implements a small from-scratch reader for it. +// +// Intra-object TypeIndex references inside the TPI stream are already +// encoded the way a modern object's local .debug$T would encode them +// (`0x1000 + N`, N = the referenced record's 0-based position in the +// stream), so no reindexing is needed -- see the comment on +// readPdb2TypeServerTypes's implementation for the byte-level evidence. The +// one real fix-up needed: named leaf kinds (LF_STRUCTURE, LF_MEMBER, +// LF_ENUM, and siblings) use old "_ST" kind values with a Pascal-style +// (length-prefixed) name instead of the modern kind with a null-terminated +// name; see stFixupFor() in the .cpp for the full set handled. + +#ifndef LLD_COFF_PDB2TYPESERVER_H +#define LLD_COFF_PDB2TYPESERVER_H + +#include "llvm/ADT/ArrayRef.h" +#include "llvm/Support/Allocator.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/MemoryBufferRef.h" + +namespace lld::coff { + +// Returns true if `data` looks like a PDB 2.0 ("JG") MSF file, i.e. starts +// with the "Microsoft C/C++ program database 2.00" signature. +bool isPdb2TypeServer(llvm::MemoryBufferRef mb); + +// Reads the TPI stream out of a VC6 /Zi PDB 2.0 type server and returns the +// upgraded type-record bytes: named leaf kinds rewritten from old "_ST" +// (Pascal-name) kind values to their modern (null-terminated-name) +// equivalents. TypeIndex references are left untouched -- see the file +// comment above for why no reindexing is needed. The returned bytes are +// allocated out of `alloc` and are a drop-in replacement for what a normal +// object's `.debug$T` content (post consumeDebugMagic) would contain -- i.e. +// suitable for `ObjFile::debugTypes` + `makeTpiSource()`. +llvm::Expected> +readPdb2TypeServerTypes(llvm::MemoryBufferRef mb, llvm::BumpPtrAllocator &alloc); + +} // namespace lld::coff + +#endif diff --git a/llvm/tools/llvm-pdbutil/DumpOutputStyle.cpp b/llvm/tools/llvm-pdbutil/DumpOutputStyle.cpp index b2362ecb75703..34a47b8dd515a 100644 --- a/llvm/tools/llvm-pdbutil/DumpOutputStyle.cpp +++ b/llvm/tools/llvm-pdbutil/DumpOutputStyle.cpp @@ -1225,7 +1225,7 @@ static void dumpPartialTypeStream(LinePrinter &Printer, NumDigits(TypeIndex::FirstNonSimpleIndex + Stream.getNumTypeRecords()); MinimalTypeDumpVisitor V(Printer, Width + 2, Bytes, Extras, Types, RefTracker, - Stream.getNumHashBuckets(), Stream.getHashValues(), + Stream.getNumHashBuckets(), Stream.getHashValuesV80(), &Stream); if (opts::dump::DumpTypeDependents) { @@ -1375,7 +1375,7 @@ Error DumpOutputStyle::dumpTpiStream(uint32_t StreamIdx) { if (DumpTypes || !Indices.empty()) { if (Indices.empty()) dumpFullTypeStream(P, Types, MaybeTracker, Stream.getNumTypeRecords(), - Stream.getNumHashBuckets(), Stream.getHashValues(), + Stream.getNumHashBuckets(), Stream.getHashValuesV80(), &Stream, DumpBytes, DumpExtras); else { std::vector TiList(Indices.begin(), Indices.end()); From 6cc0842811ad6ff6b7ddd22ec1031a9df57aebd3 Mon Sep 17 00:00:00 2001 From: Sandy Carter Date: Thu, 23 Jul 2026 00:15:55 -0400 Subject: [PATCH 2/2] [lld][COFF] Convert VC6 old-format .debug$S symbols for --debug PDBs MSVC 6.0 /Zi writes each .debug$S section using the old CodeView C11 signature: a flat, unwrapped sequence of symbol records (no subsection framing, unlike modern C13), using old "_ST" (Pascal-name) kinds for the handful of record types that carry a name. lld previously rejected these sections outright (unrecognized magic), so --debug PDBs had no local/global variable names, enum constants, or UDT references -- only function names from COFF publics and (as of the previous commit) types from the per-object PDB 2.0 .debug$T type servers. Adds Pdb2Symbols.h/.cpp, converting the old symbol kinds actually emitted by real compiled objects in this project (surveyed across all of them): S_OBJNAME_ST, S_CONSTANT_ST, S_UDT_ST, S_LDATA32_ST, S_GDATA32_ST, S_REGISTER_ST, S_BPREL32_ST, S_LABEL32_ST, S_LPROC32_ST, S_GPROC32_ST -> their modern equivalents, and S_COMPILE (which predates the "_ST" family and has no modern equivalent in LLVM at all) -> S_SKIP. Objects with more than one function have additional .debug$S sections -- one per function -- that carry no magic at all, starting directly with S_GPROC32_ST/S_LPROC32_ST; those are handled too. Conversion happens once per chunk at object-load time (InputFiles.cpp) and is strictly in-place: same total size, same byte offsets, magic present or absent exactly as given. This section's own COFF relocations point at byte offsets within these records (e.g. S_GDATA32_ST's DataOffset/Segment fields), so nothing may shift them -- not even by stripping a magic prefix from the stored copy, which is why the override still carries whatever magic (or lack thereof) the original had; callers check isOldCodeViewSymbols/ isBareOldCodeViewSymbols to decide how to treat it instead. SectionChunk has a static_assert on its size (there can be very many instances), so the override lives in a small DenseMap on ObjFile instead of a new SectionChunk field -- ObjFile instances are much rarer. Verified against a real BW1 game binary rebuild: the output PDB now contains real global/local variable names, enum constants, and compiler-generated labels (e.g. "MPFEConnectionStatus::IsInternetLobby", "$L17277") round-tripped from VC6 object files, with zero errors or warnings. --- lld/COFF/CMakeLists.txt | 1 + lld/COFF/Chunks.cpp | 30 +++-- lld/COFF/InputFiles.cpp | 22 +++- lld/COFF/InputFiles.h | 19 ++++ lld/COFF/PDB.cpp | 97 +++++++++++++--- lld/COFF/Pdb2Symbols.cpp | 231 +++++++++++++++++++++++++++++++++++++++ lld/COFF/Pdb2Symbols.h | 64 +++++++++++ 7 files changed, 436 insertions(+), 28 deletions(-) create mode 100644 lld/COFF/Pdb2Symbols.cpp create mode 100644 lld/COFF/Pdb2Symbols.h diff --git a/lld/COFF/CMakeLists.txt b/lld/COFF/CMakeLists.txt index 7cca31ab2ed48..e72f6063eae45 100644 --- a/lld/COFF/CMakeLists.txt +++ b/lld/COFF/CMakeLists.txt @@ -17,6 +17,7 @@ add_lld_library(lldCOFF MapFile.cpp MarkLive.cpp MinGW.cpp + Pdb2Symbols.cpp Pdb2TypeServer.cpp PDB.cpp SymbolTable.cpp diff --git a/lld/COFF/Chunks.cpp b/lld/COFF/Chunks.cpp index 0241850222e00..31ab082eaec20 100644 --- a/lld/COFF/Chunks.cpp +++ b/lld/COFF/Chunks.cpp @@ -702,6 +702,9 @@ StringRef SectionChunk::getDebugName() const { } ArrayRef SectionChunk::getContents() const { + if (ArrayRef override = file->getDebugSOverride(this); + !override.empty()) + return override; ArrayRef a; cantFail(file->getCOFFObj()->getSectionContents(header, a)); return a; @@ -729,16 +732,25 @@ ArrayRef SectionChunk::consumeDebugMagic(ArrayRef data, ? DEBUG_HASHES_SECTION_MAGIC : DEBUG_SECTION_MAGIC; if (magic != expectedMagic) { - // MSVC 6.0 /Zi emits .debug$T with the old CodeView "C11" signature - // (CV_SIGNATURE_C11 = 2) instead of C13 (4). The record that follows is a - // normal CVType -- an old LF_TYPESERVER / LF_TYPESERVER_ST pointing at a - // PDB 2.0 type server -- which the type-server path in - // ObjFile::initializeDependencies() already handles. Let it through so - // those old type servers get merged. Everything else (notably C11 - // .debug$S, whose subsection layout the C13 reader can't parse) stays - // rejected. + // MSVC 6.0 /Zi emits .debug$T and .debug$S with the old CodeView "C11" + // signature (CV_SIGNATURE_C11 = 2) instead of C13 (4). + // + // For .debug$T, the record that follows is a normal CVType -- an old + // LF_TYPESERVER / LF_TYPESERVER_ST pointing at a PDB 2.0 type server -- + // which the type-server path in ObjFile::initializeDependencies() + // already handles. + // + // For .debug$S, ObjFile::readSection() already rewrote this chunk's old + // "_ST" symbol kinds to modern equivalents in place (see Pdb2Symbols.h) + // and installed the result via setDebugSOverride -- what getContents() + // returns here IS that converted buffer, just still C11-tagged since + // the rewrite doesn't touch the magic. Old .debug$S has no subsection + // framing (unlike modern C13), so callers must not treat the stripped + // bytes as a subsection array -- see the callers in PDB.cpp, which check + // isOldCodeViewSymbols() before parsing. constexpr uint32_t CV_SIGNATURE_C11 = 2; - if (sectionName == ".debug$T" && magic == CV_SIGNATURE_C11) + if ((sectionName == ".debug$T" || sectionName == ".debug$S") && + magic == CV_SIGNATURE_C11) return data.slice(4); warn("ignoring section " + sectionName + " with unrecognized magic 0x" + utohexstr(magic)); diff --git a/lld/COFF/InputFiles.cpp b/lld/COFF/InputFiles.cpp index e355b10f79fcc..7c5e7e688f08b 100644 --- a/lld/COFF/InputFiles.cpp +++ b/lld/COFF/InputFiles.cpp @@ -7,6 +7,7 @@ //===----------------------------------------------------------------------===// #include "InputFiles.h" +#include "Pdb2Symbols.h" #include "Pdb2TypeServer.h" #include "COFFLinkerContext.h" #include "Chunks.h" @@ -320,8 +321,27 @@ SectionChunk *ObjFile::readSection(uint32_t sectionNumber, // CodeView sections are stored to a different vector because they are not // linked in the regular manner. - if (c->isCodeView()) + if (c->isCodeView()) { debugChunks.push_back(c); + // MSVC 6.0 /Zi emits .debug$S with the old CV_SIGNATURE_C11 format (a + // flat symbol stream using old "_ST" kinds; see Pdb2Symbols.h) -- and an + // object with more than one function has additional .debug$S sections, + // one per function, that carry no magic at all (a "bare" continuation + // starting directly with S_GPROC32_ST/S_LPROC32_ST). Convert either kind + // in place now, once, rather than at each of the several places that + // later read this chunk's contents. + if (name == ".debug$S" && + (isOldCodeViewSymbols(c->getContents()) || + isBareOldCodeViewSymbols(c->getContents()))) { + if (Expected> converted = + convertOldCodeViewSymbols(c->getContents(), bAlloc())) + setDebugSOverride(c, *converted); + else + Warn(symtab.ctx) << "failed to convert VC6 .debug$S in " + << toString(this) << ": " + << toString(converted.takeError()); + } + } else if (name == ".gfids$y") guardFidChunks.push_back(c); else if (name == ".giats$y") diff --git a/lld/COFF/InputFiles.h b/lld/COFF/InputFiles.h index fd2e409ada30f..f536e75233273 100644 --- a/lld/COFF/InputFiles.h +++ b/lld/COFF/InputFiles.h @@ -144,6 +144,15 @@ class ObjFile : public InputFile { MachineTypes getMachineType() const override; ArrayRef getChunks() { return chunks; } ArrayRef getDebugChunks() { return debugChunks; } + + // See debugSOverrides below. + void setDebugSOverride(const SectionChunk *sc, ArrayRef data) { + debugSOverrides[sc] = data; + } + ArrayRef getDebugSOverride(const SectionChunk *sc) const { + auto it = debugSOverrides.find(sc); + return it == debugSOverrides.end() ? ArrayRef() : it->second; + } ArrayRef getSXDataChunks() { return sxDataChunks; } ArrayRef getGuardFidChunks() { return guardFidChunks; } ArrayRef getGuardIATChunks() { return guardIATChunks; } @@ -289,6 +298,16 @@ class ObjFile : public InputFile { // CodeView debug info sections. std::vector debugChunks; + // Overrides the contents SectionChunk::getContents() would otherwise + // derive from the raw COFF section, for VC6 .debug$S chunks whose old + // "_ST" symbol kinds have been rewritten to modern equivalents (see + // Pdb2Symbols.h). Same total size as the original, so this chunk's COFF + // relocations remain valid without adjustment. Keyed by ObjFile rather + // than added as a SectionChunk field: SectionChunk has a static_assert on + // its size (there can be very many of them), while ObjFile instances are + // comparatively rare. + llvm::DenseMap> debugSOverrides; + // Chunks containing symbol table indices of exception handlers. Only used for // 32-bit x86. std::vector sxDataChunks; diff --git a/lld/COFF/PDB.cpp b/lld/COFF/PDB.cpp index ec2b3482fb340..9f135c41ea01e 100644 --- a/lld/COFF/PDB.cpp +++ b/lld/COFF/PDB.cpp @@ -12,6 +12,7 @@ #include "Config.h" #include "DebugTypes.h" #include "Driver.h" +#include "Pdb2Symbols.h" #include "SymbolTable.h" #include "Symbols.h" #include "TypeMerger.h" @@ -657,23 +658,25 @@ Error PDBLinker::writeAllModuleSymbolRecords(ObjFile *file, continue; ArrayRef sectionContents = debugChunk->getContents(); - auto contents = - SectionChunk::consumeDebugMagic(sectionContents, ".debug$S"); - DebugSubsectionArray subsections; - BinaryStreamReader reader(contents, llvm::endianness::little); - exitOnErr(reader.readArray(subsections, contents.size())); + // Whether this chunk got an old-format override at load time (see + // Pdb2Symbols.h) is the signal to use here, not re-inspecting + // sectionContents: conversion rewrites the bare case's leading kind + // value (what isBareOldCodeViewSymbols keys on) from old to modern, so + // checking it again post-conversion would always come back false. The + // magic bytes are never touched by conversion, so isOldCodeViewSymbols + // remains valid to tell whether there's a magic to strip. + bool oldFormat = !file->getDebugSOverride(debugChunk).empty(); + bool magicPrefixed = isOldCodeViewSymbols(sectionContents); + ArrayRef contents = + magicPrefixed ? SectionChunk::consumeDebugMagic(sectionContents, + ".debug$S") + : sectionContents; uint32_t nextRelocIndex = 0; - for (const DebugSubsectionRecord &ss : subsections) { - if (ss.kind() != DebugSubsectionKind::Symbols) - continue; - + auto processSymbols = [&](ArrayRef symsBuffer) -> Error { uint32_t moduleSymStart = writer.getOffset(); scopes.clear(); storage.clear(); - ArrayRef symsBuffer; - BinaryStreamRef sr = ss.getRecordData(); - cantFail(sr.readBytes(0, sr.getLength(), symsBuffer)); auto ec = forEachCodeViewRecord( symsBuffer, [&](CVSymbol sym) -> llvm::Error { // Track the current scope. Only update records in the postmerge @@ -704,7 +707,31 @@ Error PDBLinker::writeAllModuleSymbolRecords(ObjFile *file, // at once. // TODO: Consider buffering symbols for the entire object file to reduce // overhead even further. - if (Error e = writer.writeBytes(storage)) + return writer.writeBytes(storage); + }; + + // Old (VC6 CV_SIGNATURE_C11, or bare per-function continuation) .debug$S + // has no subsection framing: the whole thing (already converted from + // "_ST" kinds; see Pdb2Symbols.h) IS one flat symbol stream. Modern + // .debug$S wraps symbol data in a Kind=Symbols subsection alongside + // others (StringTable, Lines, ...). + if (oldFormat) { + if (Error e = processSymbols(contents)) + return e; + continue; + } + + DebugSubsectionArray subsections; + BinaryStreamReader reader(contents, llvm::endianness::little); + exitOnErr(reader.readArray(subsections, contents.size())); + + for (const DebugSubsectionRecord &ss : subsections) { + if (ss.kind() != DebugSubsectionKind::Symbols) + continue; + ArrayRef symsBuffer; + BinaryStreamRef sr = ss.getRecordData(); + cantFail(sr.readBytes(0, sr.getLength(), symsBuffer)); + if (Error e = processSymbols(symsBuffer)) return e; } } @@ -759,17 +786,41 @@ translateStringTableIndex(COFFLinkerContext &ctx, uint32_t objIndex, void DebugSHandler::handleDebugS(SectionChunk *debugChunk) { // Note that we are processing the *unrelocated* section contents. They will // be relocated later during PDB writing. - ArrayRef contents = debugChunk->getContents(); - contents = SectionChunk::consumeDebugMagic(contents, ".debug$S"); - DebugSubsectionArray subsections; - BinaryStreamReader reader(contents, llvm::endianness::little); + ArrayRef rawContents = debugChunk->getContents(); + // Whether this chunk got an old-format override at load time (see + // Pdb2Symbols.h) is the signal to use here, not re-inspecting + // rawContents: conversion rewrites the bare case's leading kind value + // from old to modern, so checking it again post-conversion would always + // come back false. The magic bytes are never touched by conversion, so + // isOldCodeViewSymbols remains valid to tell whether there's a magic to + // strip. + bool oldFormat = !file.getDebugSOverride(debugChunk).empty(); + bool magicPrefixed = isOldCodeViewSymbols(rawContents); + ArrayRef contents = + magicPrefixed ? SectionChunk::consumeDebugMagic(rawContents, ".debug$S") + : rawContents; ExitOnError exitOnErr; - exitOnErr(reader.readArray(subsections, contents.size())); debugChunk->sortRelocations(); // Reset the relocation index, since this is a new section. nextRelocIndex = 0; + // Old (VC6 CV_SIGNATURE_C11, or bare per-function continuation) .debug$S + // has no subsection framing: the whole thing (already converted from + // "_ST" kinds; see Pdb2Symbols.h) IS one flat symbol stream, and this + // format has no StringTable/FileChecksums/Lines/etc subsections to look + // for. + if (oldFormat) { + linker.analyzeSymbolSubsection( + debugChunk, moduleStreamSize, nextRelocIndex, stringTableFixups, + BinaryStreamRef(contents, llvm::endianness::little)); + return; + } + + DebugSubsectionArray subsections; + BinaryStreamReader reader(contents, llvm::endianness::little); + exitOnErr(reader.readArray(subsections, contents.size())); + for (const DebugSubsectionRecord &ss : subsections) { // Ignore subsections with the 'ignore' bit. Some versions of the Visual C++ // runtime have subsections with this bit set. @@ -1776,6 +1827,16 @@ static bool findLineTable(const SectionChunk *c, uint32_t addr, for (SectionChunk *dbgC : c->file->getDebugChunks()) { if (dbgC->getSectionName() != ".debug$S") continue; + // Old (VC6 CV_SIGNATURE_C11, or bare per-function continuation) + // .debug$S is a flat symbol stream with no StringTable/FileChecksums/ + // Lines subsections to look for (see Pdb2Symbols.h) -- nothing here + // applies to it. Whether this chunk got an old-format override at load + // time is the signal (see the comment in handleDebugS): conversion + // rewrites the bare case's leading kind value, so re-checking + // isBareOldCodeViewSymbols against the (already-converted) contents + // here would always come back false. + if (!dbgC->file->getDebugSOverride(dbgC).empty()) + continue; // Build a mapping of SECREL relocations in dbgC that refer to `c`. DenseMap secrels; diff --git a/lld/COFF/Pdb2Symbols.cpp b/lld/COFF/Pdb2Symbols.cpp new file mode 100644 index 0000000000000..2a91a93cca0a2 --- /dev/null +++ b/lld/COFF/Pdb2Symbols.cpp @@ -0,0 +1,231 @@ +//===- Pdb2Symbols.cpp -----------------------------------------*- C++ -*-===// +// +// 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 +// +//===----------------------------------------------------------------------===// +// +// See Pdb2Symbols.h. A real MSVC 6.0 .debug$S section (surveyed across every +// matched object in this project) uses exactly these old symbol kinds: +// +// S_OBJNAME_ST (0x0009) [sig:4][name] -> S_OBJNAME (0x1101) +// S_CONSTANT_ST (0x1002) [type:4][value: numeric-leaf][name] -> S_CONSTANT (0x1107) +// S_UDT_ST (0x1003) [type:4][name] -> S_UDT (0x1108) +// S_LDATA32_ST (0x1007) [type:4][offset:4][segment:2][name] -> S_LDATA32 (0x110c) +// S_GDATA32_ST (0x1008) [type:4][offset:4][segment:2][name] -> S_GDATA32 (0x110d) +// S_REGISTER_ST (0x1001) [index:4][register:2][name] -> S_REGISTER (0x1106) +// S_BPREL32_ST (0x1006) [offset:4][type:4][name] -> S_BPREL32 (0x110b) +// S_LABEL32_ST (0x0209) [codeoffset:4][segment:2][flags:1][name] -> S_LABEL32 (0x1105) +// S_LPROC32_ST (0x100a) [8 x u32][segment:2][flags:1][name] -> S_LPROC32 (0x110f) +// S_GPROC32_ST (0x100b) same shape as S_LPROC32_ST -> S_GPROC32 (0x1110) +// S_END (0x0006) -- already the same value in old and new numbering +// (no name, no conversion needed) +// S_COMPILE (0x0001) -- no modern equivalent exists in LLVM at all (not +// even an "_ST" sibling; this predates that +// family). Carries only compiler-version text, +// nothing structural. Replaced with S_SKIP rather +// than guessed at. +// +// Shapes verified against llvm::codeview::SymbolRecordMapping:: +// visitKnownRecord for ObjNameSym/ConstantSym/UDTSym/DataSym/RegisterSym/ +// BPRelativeSym/LabelSym/ProcSym in SymbolRecordMapping.cpp, and cross- +// checked against real record bytes (struct.unpack, not by hand -- an +// earlier by-hand count of ProcSym's fields looked wrong at first and +// wasn't). + +#include "Pdb2Symbols.h" + +#include "llvm/DebugInfo/CodeView/CodeView.h" +#include "llvm/DebugInfo/CodeView/RecordSerialization.h" +#include "llvm/Support/Endian.h" + +using namespace llvm; +using namespace llvm::codeview; +using namespace llvm::support; + +namespace lld::coff { + +static constexpr uint32_t cvSignatureC11 = 2; + +bool isOldCodeViewSymbols(ArrayRef sectionContents) { + return sectionContents.size() >= 4 && + support::endian::read32le(sectionContents.data()) == cvSignatureC11; +} + +bool isBareOldCodeViewSymbols(ArrayRef sectionContents) { + if (sectionContents.size() < 4) + return false; + uint16_t length = support::endian::read16le(sectionContents.data()); + uint16_t kind = support::endian::read16le(sectionContents.data() + 2); + return (kind == S_GPROC32_ST || kind == S_LPROC32_ST) && + size_t(2 + length) <= sectionContents.size(); +} + +// Same numeric-leaf-encoding table as Pdb2TypeServer.cpp's numericLeafSize; +// duplicated rather than shared since it's the only overlap between the two +// files and it's small. +static bool numericLeafSize(uint16_t firstWord, size_t &size) { + if (firstWord < LF_NUMERIC) { + size = 2; + return true; + } + switch (firstWord) { + case LF_CHAR: + size = 3; + return true; + case LF_SHORT: + case LF_USHORT: + size = 4; + return true; + case LF_LONG: + case LF_ULONG: + size = 6; + return true; + case LF_REAL32: + size = 6; + return true; + case LF_REAL64: + size = 10; + return true; + case LF_REAL80: + size = 12; + return true; + case LF_REAL128: + size = 18; + return true; + case LF_QUADWORD: + case LF_UQUADWORD: + size = 10; + return true; + default: + return false; + } +} + +// Converts a Pascal-style (length-prefixed) name at `nameOffset` to +// null-terminated, in place. Both encodings occupy exactly N+1 bytes for an +// N-character name, so this never changes the record's length. Returns +// false (record left untouched) if the shape doesn't match. +static bool fixupPascalNameDirectAt(MutableArrayRef rec, + size_t nameOffset) { + if (nameOffset >= rec.size()) + return false; + uint8_t nameLen = rec[nameOffset]; + if (nameOffset + 1 + nameLen > rec.size()) + return false; + uint8_t *name = rec.data() + nameOffset; + for (uint8_t i = 0; i < nameLen; ++i) + name[i] = name[i + 1]; + name[nameLen] = 0; + return true; +} + +// As above, but `prefixSize` is followed by a numeric-leaf-encoded value +// (S_CONSTANT's Value) before the name. +static bool fixupPascalNameAfterLeafAt(MutableArrayRef rec, + size_t prefixSize) { + if (rec.size() < prefixSize + 3) + return false; + size_t numericFieldSize; + if (!numericLeafSize(support::endian::read16le(rec.data() + prefixSize), + numericFieldSize)) + return false; + return fixupPascalNameDirectAt(rec, prefixSize + numericFieldSize); +} + +// Rewrites one old-kind symbol record to its modern equivalent in place. +// Only commits the kind rewrite if the name fixup succeeds, for the same +// reason as Pdb2TypeServer.cpp's stFixupFor loop: a kind tag that doesn't +// match its actual (still Pascal-encoded) name is worse than leaving the +// record as an unrecognized old kind. +static void convertOneSymbol(MutableArrayRef rec) { + uint16_t kind = support::endian::read16le(rec.data() + 2); + switch (kind) { + case S_OBJNAME_ST: // [len:2][kind:2][sig:4][name] + if (fixupPascalNameDirectAt(rec, /*len+kind+sig=*/4 + 4)) + support::endian::write16le(rec.data() + 2, uint16_t(S_OBJNAME)); + break; + case S_CONSTANT_ST: // [len:2][kind:2][type:4][value-leaf][name] + if (fixupPascalNameAfterLeafAt(rec, /*len+kind+type=*/4 + 4)) + support::endian::write16le(rec.data() + 2, uint16_t(S_CONSTANT)); + break; + case S_UDT_ST: // [len:2][kind:2][type:4][name] + if (fixupPascalNameDirectAt(rec, /*len+kind+type=*/4 + 4)) + support::endian::write16le(rec.data() + 2, uint16_t(S_UDT)); + break; + case S_LDATA32_ST: // [len:2][kind:2][type:4][offset:4][segment:2][name] + if (fixupPascalNameDirectAt(rec, /*len+kind+type+offset+segment=*/ + 4 + 4 + 4 + 2)) + support::endian::write16le(rec.data() + 2, uint16_t(S_LDATA32)); + break; + case S_GDATA32_ST: // same shape as S_LDATA32_ST + if (fixupPascalNameDirectAt(rec, /*len+kind+type+offset+segment=*/ + 4 + 4 + 4 + 2)) + support::endian::write16le(rec.data() + 2, uint16_t(S_GDATA32)); + break; + case S_REGISTER_ST: // [len:2][kind:2][index:4][register:2][name] + if (fixupPascalNameDirectAt(rec, /*len+kind+index+register=*/4 + 4 + 2)) + support::endian::write16le(rec.data() + 2, uint16_t(S_REGISTER)); + break; + case S_BPREL32_ST: // [len:2][kind:2][offset:4][type:4][name] + if (fixupPascalNameDirectAt(rec, /*len+kind+offset+type=*/4 + 4 + 4)) + support::endian::write16le(rec.data() + 2, uint16_t(S_BPREL32)); + break; + case S_LABEL32_ST: // [len:2][kind:2][codeoffset:4][segment:2][flags:1][name] + if (fixupPascalNameDirectAt(rec, /*len+kind+codeoffset+segment+flags=*/ + 4 + 4 + 2 + 1)) + support::endian::write16le(rec.data() + 2, uint16_t(S_LABEL32)); + break; + case S_LPROC32_ST: // [len:2][kind:2][8 x u32][segment:2][flags:1][name] -- + // the 8 dwords are Parent,End,Next,CodeSize,DbgStart, + // DbgEnd,FunctionType,CodeOffset + if (fixupPascalNameDirectAt(rec, /*len+kind+8u32+segment+flags=*/ + 4 + 8 * 4 + 2 + 1)) + support::endian::write16le(rec.data() + 2, uint16_t(S_LPROC32)); + break; + case S_GPROC32_ST: // same shape as S_LPROC32_ST + if (fixupPascalNameDirectAt(rec, /*len+kind+8u32+segment+flags=*/ + 4 + 8 * 4 + 2 + 1)) + support::endian::write16le(rec.data() + 2, uint16_t(S_GPROC32)); + break; + case S_COMPILE: // no modern shape exists to convert to; just skip it + support::endian::write16le(rec.data() + 2, uint16_t(S_SKIP)); + break; + default: + break; // unrecognized old kind; leave as-is, downstream treats as opaque + } +} + +Expected> +convertOldCodeViewSymbols(ArrayRef sectionContents, + BumpPtrAllocator &alloc) { + bool hasMagic = isOldCodeViewSymbols(sectionContents); + assert(hasMagic || isBareOldCodeViewSymbols(sectionContents)); + + // Same total size and layout as the input (magic present or absent, + // exactly as given -- unchanged either way): this section's COFF + // relocations point at byte offsets counted from the section's start + // (e.g. S_GDATA32_ST's DataOffset/Segment fields), so nothing here may + // shift those offsets, including by stripping a magic that was there. + size_t bodyStart = hasMagic ? 4 : 0; + uint8_t *out = alloc.Allocate(sectionContents.size()); + memcpy(out, sectionContents.data(), sectionContents.size()); + MutableArrayRef body(out + bodyStart, + sectionContents.size() - bodyStart); + + size_t pos = 0; + while (pos + sizeof(RecordPrefix) <= body.size()) { + uint16_t length = support::endian::read16le(body.data() + pos); + if (pos + 2 + length > body.size()) + return createStringError("VC6 .debug$S: symbol record overruns " + "section at offset " + + Twine(pos)); + convertOneSymbol(body.slice(pos, 2 + length)); + pos += 2 + length; + } + + return ArrayRef(out, sectionContents.size()); +} + +} // namespace lld::coff diff --git a/lld/COFF/Pdb2Symbols.h b/lld/COFF/Pdb2Symbols.h new file mode 100644 index 0000000000000..2f1b8298552fa --- /dev/null +++ b/lld/COFF/Pdb2Symbols.h @@ -0,0 +1,64 @@ +//===- Pdb2Symbols.h --------------------------------------------*- C++ -*-===// +// +// 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 +// +//===----------------------------------------------------------------------===// +// +// MSVC 6.0 `/Zi` writes each .debug$S section using the old CodeView C11 +// signature (same CV_SIGNATURE_C11 = 2 as .debug$T, see Pdb2TypeServer.h): +// a flat, unwrapped sequence of symbol records (no subsection framing, unlike +// modern C13), using old "_ST" (Pascal-name) symbol kinds for the handful of +// record types that carry a name. +// +// An object with more than one function has additional .debug$S sections +// (one per function) that carry no magic at all -- they start directly with +// an S_GPROC32_ST/S_LPROC32_ST record. isBareOldCodeViewSymbols detects +// those. +// +// convertOldCodeViewSymbols rewrites the old-kind records to modern +// equivalents in place. The returned buffer is the same total size and +// layout as the input -- magic present or absent, exactly as given -- since +// this section's existing COFF relocations point at byte offsets counted +// from the section's start (e.g. S_GDATA32_ST's DataOffset/Segment fields); +// nothing here may shift those offsets, including by adding or stripping a +// magic prefix. Callers check isOldCodeViewSymbols/isBareOldCodeViewSymbols +// on whatever SectionChunk::getContents() returns (the override, once one +// is set) to decide whether to treat it as a flat symbol stream instead of +// a subsection array -- see the callers in PDB.cpp. + +#ifndef LLD_COFF_PDB2SYMBOLS_H +#define LLD_COFF_PDB2SYMBOLS_H + +#include "llvm/ADT/ArrayRef.h" +#include "llvm/Support/Allocator.h" +#include "llvm/Support/Error.h" + +namespace lld::coff { + +// Returns true if `sectionContents` (the raw, unstripped .debug$S section +// bytes) starts with the old CV_SIGNATURE_C11 magic. +bool isOldCodeViewSymbols(llvm::ArrayRef sectionContents); + +// Returns true if `sectionContents` has no magic at all but looks like a +// per-function continuation .debug$S section: starts directly with an +// S_GPROC32_ST/S_LPROC32_ST record whose declared length doesn't overrun +// the buffer. +bool isBareOldCodeViewSymbols(llvm::ArrayRef sectionContents); + +// Converts an old-format .debug$S section's known "_ST" symbol kinds to +// their modern (null-terminated-name) equivalents, and strips the leading +// magic if `sectionContents` has one (per isOldCodeViewSymbols) -- the +// returned buffer is always just the flat symbol stream, whether or not the +// input had a magic prefix. Suitable as a drop-in replacement for the +// section's contents, and every consumer's signal that this chunk is +// old-format and already converted, via ObjFile::getDebugSOverride +// returning non-empty for it. +llvm::Expected> +convertOldCodeViewSymbols(llvm::ArrayRef sectionContents, + llvm::BumpPtrAllocator &alloc); + +} // namespace lld::coff + +#endif