blob: a0e561efafdb322b68fccfd8e8a797699d0ddd48 [file] [edit]
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// Reading of PDB files, which contain the debugging information for
// PE/COFF snapshots.
//
// References:
// - https://llvm.org/docs/PDB/index.html
// - https://github.com/microsoft/microsoft-pdb (cvinfo.h, dbi.h)
import 'dart:convert';
import 'dart:ffi'
show
Array,
Int32,
Packed,
Struct,
Uint16,
Uint32,
Uint8,
Uint8Array,
sizeOf;
import 'dart:typed_data';
import 'constants.dart' as constants;
import 'dwarf.dart';
import 'reader.dart';
/// A cursor for reading little-endian data.
///
/// Unlike [Reader], decodes strings as UTF-8, as CodeView strings are, and
/// aligns relative to a base offset, as the substreams of PDB streams are
/// aligned relative to their start.
class _Cursor {
final ByteData _data;
int offset;
_Cursor(Uint8List bytes, [this.offset = 0])
: _data = ByteData.sublistView(bytes);
int get length => _data.lengthInBytes;
bool get done => offset >= length;
int u8() => _data.getUint8(offset++);
int u16() {
final value = _data.getUint16(offset, Endian.little);
offset += 2;
return value;
}
int u32() {
final value = _data.getUint32(offset, Endian.little);
offset += 4;
return value;
}
/// Reads a struct with [reader].
T read<T extends Struct>(_StructReader<T> reader) {
final value = reader.create(_data, offset);
offset += reader.size;
return value;
}
/// Reads the prefix of a type or symbol record, and returns the kind of the
/// record and the offset of its end.
(int, int) recordPrefix() {
final start = offset;
final prefix = read(_recordPrefix);
return (prefix.recordKind, start + 2 + prefix.recordLen);
}
Uint8List bytes(int size) {
final value = Uint8List.sublistView(_data, offset, offset + size);
offset += size;
return value;
}
String string() {
var end = offset;
while (end < length && _data.getUint8(end) != 0) {
end++;
}
final value = utf8.decode(Uint8List.sublistView(_data, offset, end),
allowMalformed: true);
offset = end + 1;
return value;
}
void align(int alignment, {int base = 0}) {
offset = base + (offset - base + alignment - 1) ~/ alignment * alignment;
}
}
/// Creates views of structs of type [T] on bytes.
///
/// [Struct.create] needs its type argument to be known at compile time, so
/// each struct type has its own reader, like [_dbiStreamHeader]. Like PDB
/// files, the structs are little-endian, as all hosts that Dart runs on are.
final class _StructReader<T extends Struct> {
final int size;
final T Function(TypedData data, int offset) create;
const _StructReader(this.size, this.create);
}
// The structs of PDB files, with the names they have in LLVM and in
// microsoft-pdb.
/// The super block at the start of an MSF file (`SuperBlock` in LLVM's
/// MSFCommon.h, https://llvm.org/docs/PDB/MsfFile.html).
final class _SuperBlock extends Struct {
@Array(32)
external Array<Uint8> fileMagic;
@Uint32()
external int blockSize;
@Uint32()
external int freeBlockMapBlock;
@Uint32()
external int numBlocks;
@Uint32()
external int numDirectoryBytes;
@Uint32()
external int unknown;
@Uint32()
external int blockMapAddr;
}
final _superBlock = _StructReader(sizeOf<_SuperBlock>(),
(data, offset) => Struct.create<_SuperBlock>(data, offset));
/// A GUID (`GUID` in guiddef.h).
final class _Guid extends Struct {
@Uint32()
external int data1;
@Uint16()
external int data2;
@Uint16()
external int data3;
@Array(8)
external Array<Uint8> data4;
}
/// The header of the PDB info stream (`PDBStreamHeader` in LLVM's RawTypes.h,
/// https://llvm.org/docs/PDB/PdbStream.html).
final class _PdbStreamHeader extends Struct {
@Uint32()
external int version;
@Uint32()
external int signature;
@Uint32()
external int age;
external _Guid guid;
}
final _pdbStreamHeader = _StructReader(sizeOf<_PdbStreamHeader>(),
(data, offset) => Struct.create<_PdbStreamHeader>(data, offset));
/// The header of the string table in the /names stream
/// (`PDBStringTableHeader` in LLVM's RawTypes.h,
/// https://llvm.org/docs/PDB/StringTable.html).
final class _StringTableHeader extends Struct {
@Uint32()
external int signature;
@Uint32()
external int hashVersion;
@Uint32()
external int byteSize;
}
final _stringTableHeader = _StructReader(sizeOf<_StringTableHeader>(),
(data, offset) => Struct.create<_StringTableHeader>(data, offset));
/// The header of the TPI and IPI streams (`TpiStreamHeader` in LLVM's
/// RawTypes.h, https://llvm.org/docs/PDB/TpiStream.html).
final class _TpiStreamHeader extends Struct {
@Uint32()
external int version;
@Uint32()
external int headerSize;
@Uint32()
external int typeIndexBegin;
@Uint32()
external int typeIndexEnd;
@Uint32()
external int typeRecordBytes;
@Uint16()
external int hashStreamIndex;
@Uint16()
external int hashAuxStreamIndex;
@Uint32()
external int hashKeySize;
@Uint32()
external int numHashBuckets;
@Int32()
external int hashValueBufferOffset;
@Uint32()
external int hashValueBufferLength;
@Int32()
external int indexOffsetBufferOffset;
@Uint32()
external int indexOffsetBufferLength;
@Int32()
external int hashAdjBufferOffset;
@Uint32()
external int hashAdjBufferLength;
}
final _tpiStreamHeader = _StructReader(sizeOf<_TpiStreamHeader>(),
(data, offset) => Struct.create<_TpiStreamHeader>(data, offset));
/// The prefix of type and symbol records (`RecordPrefix` in LLVM's
/// RecordSerialization.h).
final class _RecordPrefix extends Struct {
/// The length of the record, not including this field.
@Uint16()
external int recordLen;
@Uint16()
external int recordKind;
}
final _recordPrefix = _StructReader(sizeOf<_RecordPrefix>(),
(data, offset) => Struct.create<_RecordPrefix>(data, offset));
/// An `LF_FUNC_ID` or `LF_MFUNC_ID` type record, followed by the name of the
/// function (`lfFuncId` and `lfMFuncId` in cvinfo.h).
final class _FuncIdRecord extends Struct {
/// The scope of an `LF_FUNC_ID`, or the parent type of an `LF_MFUNC_ID`.
@Uint32()
external int scope;
@Uint32()
external int type;
}
final _funcIdRecord = _StructReader(sizeOf<_FuncIdRecord>(),
(data, offset) => Struct.create<_FuncIdRecord>(data, offset));
/// The header of the DBI stream (`DbiStreamHeader` in LLVM's RawTypes.h,
/// `NewDBIHdr` in dbi.h, https://llvm.org/docs/PDB/DbiStream.html).
final class _DbiStreamHeader extends Struct {
@Int32()
external int versionSignature;
@Uint32()
external int versionHeader;
@Uint32()
external int age;
@Uint16()
external int globalSymbolStreamIndex;
@Uint16()
external int buildNumber;
@Uint16()
external int publicSymbolStreamIndex;
@Uint16()
external int pdbDllVersion;
@Uint16()
external int symRecordStreamIndex;
@Uint16()
external int pdbDllRbld;
@Int32()
external int modiSubstreamSize;
@Int32()
external int secContrSubstreamSize;
@Int32()
external int sectionMapSize;
@Int32()
external int fileInfoSize;
@Int32()
external int typeServerSize;
@Uint32()
external int mfcTypeServerIndex;
@Int32()
external int optionalDbgHdrSize;
@Int32()
external int ecSubstreamSize;
@Uint16()
external int flags;
@Uint16()
external int machine;
@Uint32()
external int reserved;
}
final _dbiStreamHeader = _StructReader(sizeOf<_DbiStreamHeader>(),
(data, offset) => Struct.create<_DbiStreamHeader>(data, offset));
/// A section contribution (`SectionContrib` in LLVM's RawTypes.h, `SC` in
/// dbi.h).
final class _SectionContrib extends Struct {
@Uint16()
external int iSect;
@Array(2)
external Array<Uint8> padding;
@Int32()
external int off;
@Int32()
external int size;
@Uint32()
external int characteristics;
@Uint16()
external int imod;
@Array(2)
external Array<Uint8> padding2;
@Uint32()
external int dataCrc;
@Uint32()
external int relocCrc;
}
/// The module info of a module in the DBI stream, followed by the names of the
/// module and of its object file (`ModuleInfoHeader` in LLVM's RawTypes.h,
/// `MODI_60_Persist` in dbi.h).
final class _ModuleInfoHeader extends Struct {
@Uint32()
external int mod;
external _SectionContrib sc;
@Uint16()
external int flags;
@Uint16()
external int moduleSymStream;
@Uint32()
external int symBytes;
@Uint32()
external int c11Bytes;
@Uint32()
external int c13Bytes;
@Uint16()
external int numFiles;
@Array(2)
external Array<Uint8> padding;
@Uint32()
external int fileNameOffs;
@Uint32()
external int srcFileNameNI;
@Uint32()
external int pdbFilePathNI;
}
final _moduleInfoHeader = _StructReader(sizeOf<_ModuleInfoHeader>(),
(data, offset) => Struct.create<_ModuleInfoHeader>(data, offset));
/// A section header in the section header stream (`IMAGE_SECTION_HEADER` in
/// winnt.h, `coff_section` in LLVM's COFF.h).
final class _SectionHeader extends Struct {
@Array(8)
external Array<Uint8> name;
@Uint32()
external int virtualSize;
@Uint32()
external int virtualAddress;
@Uint32()
external int sizeOfRawData;
@Uint32()
external int pointerToRawData;
@Uint32()
external int pointerToRelocations;
@Uint32()
external int pointerToLinenumbers;
@Uint16()
external int numberOfRelocations;
@Uint16()
external int numberOfLinenumbers;
@Uint32()
external int characteristics;
}
final _sectionHeader = _StructReader(sizeOf<_SectionHeader>(),
(data, offset) => Struct.create<_SectionHeader>(data, offset));
/// A procedure symbol, followed by its name (`PROCSYM32` in cvinfo.h).
@Packed(1)
final class _ProcSym extends Struct {
@Uint32()
external int parent;
@Uint32()
external int end;
@Uint32()
external int next;
@Uint32()
external int codeSize;
@Uint32()
external int dbgStart;
@Uint32()
external int dbgEnd;
@Uint32()
external int functionType;
@Uint32()
external int codeOffset;
@Uint16()
external int segment;
@Uint8()
external int flags;
}
final _procSym = _StructReader(sizeOf<_ProcSym>(),
(data, offset) => Struct.create<_ProcSym>(data, offset));
/// A public symbol, followed by its name (`PUBSYM32` in cvinfo.h).
@Packed(1)
final class _PublicSym32 extends Struct {
@Uint32()
external int flags;
@Uint32()
external int offset;
@Uint16()
external int segment;
}
final _publicSym32 = _StructReader(sizeOf<_PublicSym32>(),
(data, offset) => Struct.create<_PublicSym32>(data, offset));
/// An `S_INLINESITE` symbol, followed by the binary annotations of the call
/// (`INLINESITESYM` in cvinfo.h).
final class _InlineSiteSym extends Struct {
@Uint32()
external int parent;
@Uint32()
external int end;
@Uint32()
external int inlinee;
}
final _inlineSiteSym = _StructReader(sizeOf<_InlineSiteSym>(),
(data, offset) => Struct.create<_InlineSiteSym>(data, offset));
/// An `S_INLINESITE2` symbol, followed by the binary annotations of the call
/// (`INLINESITESYM2` in cvinfo.h).
final class _InlineSiteSym2 extends Struct {
@Uint32()
external int parent;
@Uint32()
external int end;
@Uint32()
external int inlinee;
@Uint32()
external int invocations;
}
final _inlineSiteSym2 = _StructReader(sizeOf<_InlineSiteSym2>(),
(data, offset) => Struct.create<_InlineSiteSym2>(data, offset));
/// An environment block symbol, followed by pairs of keys and values that end
/// with an empty key (`ENVBLOCKSYM` in cvinfo.h).
final class _EnvBlockSym extends Struct {
@Uint8()
external int flags;
}
final _envBlockSym = _StructReader(sizeOf<_EnvBlockSym>(),
(data, offset) => Struct.create<_EnvBlockSym>(data, offset));
/// The header of a C13 debug subsection (`DebugSubsectionHeader` in LLVM's
/// DebugSubsection.h).
final class _DebugSubsectionHeader extends Struct {
@Uint32()
external int kind;
@Uint32()
external int length;
}
final _debugSubsectionHeader = _StructReader(sizeOf<_DebugSubsectionHeader>(),
(data, offset) => Struct.create<_DebugSubsectionHeader>(data, offset));
/// The header of a `DEBUG_S_LINES` subsection, followed by line blocks
/// (`LineFragmentHeader` in LLVM's DebugLinesSubsection.h).
final class _LineFragmentHeader extends Struct {
@Uint32()
external int relocOffset;
@Uint16()
external int relocSegment;
@Uint16()
external int flags;
@Uint32()
external int codeSize;
}
final _lineFragmentHeader = _StructReader(sizeOf<_LineFragmentHeader>(),
(data, offset) => Struct.create<_LineFragmentHeader>(data, offset));
/// The header of a line block, followed by its line entries and, if the
/// subsection has columns, by its column entries (`LineBlockFragmentHeader` in
/// LLVM's DebugLinesSubsection.h).
final class _LineBlockFragmentHeader extends Struct {
/// The offset of the file checksum entry of the file of the block.
@Uint32()
external int nameIndex;
@Uint32()
external int numLines;
@Uint32()
external int blockSize;
}
final _lineBlockFragmentHeader = _StructReader(
sizeOf<_LineBlockFragmentHeader>(),
(data, offset) => Struct.create<_LineBlockFragmentHeader>(data, offset));
/// A line entry (`LineNumberEntry` in LLVM's DebugLinesSubsection.h,
/// `CV_Line_t` in cvinfo.h).
final class _LineNumberEntry extends Struct {
@Uint32()
external int offset;
/// The line number, and whether the line is a statement.
@Uint32()
external int flags;
}
final _lineNumberEntry = _StructReader(sizeOf<_LineNumberEntry>(),
(data, offset) => Struct.create<_LineNumberEntry>(data, offset));
/// A column entry (`ColumnNumberEntry` in LLVM's DebugLinesSubsection.h,
/// `CV_Column_t` in cvinfo.h).
final class _ColumnNumberEntry extends Struct {
@Uint16()
external int startColumn;
@Uint16()
external int endColumn;
}
final _columnNumberEntry = _StructReader(sizeOf<_ColumnNumberEntry>(),
(data, offset) => Struct.create<_ColumnNumberEntry>(data, offset));
/// An entry of a `DEBUG_S_FILECHKSMS` subsection, followed by the checksum
/// and aligned to 4 bytes (`FileChecksumEntryHeader` in LLVM's
/// DebugChecksumsSubsection.h).
@Packed(1)
final class _FileChecksumEntryHeader extends Struct {
/// The offset of the name of the file in the string table.
@Uint32()
external int fileNameOffset;
@Uint8()
external int checksumSize;
@Uint8()
external int checksumKind;
}
final _fileChecksumEntryHeader = _StructReader(
sizeOf<_FileChecksumEntryHeader>(),
(data, offset) => Struct.create<_FileChecksumEntryHeader>(data, offset));
/// An entry of a `DEBUG_S_INLINEELINES` subsection, followed by the extra
/// files of the inlined function if the subsection has them
/// (`InlineeSourceLineHeader` in LLVM's DebugInlineeLinesSubsection.h).
final class _InlineeSourceLineHeader extends Struct {
@Uint32()
external int inlinee;
/// The offset of the file checksum entry of the file of the function.
@Uint32()
external int fileId;
@Uint32()
external int sourceLineNum;
}
final _inlineeSourceLineHeader = _StructReader(
sizeOf<_InlineeSourceLineHeader>(),
(data, offset) => Struct.create<_InlineeSourceLineHeader>(data, offset));
/// The streams of an MSF (multi-stream format) file, the container format
/// of PDB files.
class _MsfFile {
static final _magic =
ascii.encode('Microsoft C/C++ MSF 7.00\r\n\x1aDS\x00\x00\x00');
static const _nilStreamSize = 0xffffffff;
final Uint8List _bytes;
final int _blockSize;
/// The size and the blocks of each stream.
final List<(int, List<int>)> _streams;
_MsfFile._(this._bytes, this._blockSize, this._streams);
static bool isMsfFile(Uint8List bytes) {
if (bytes.length < _magic.length) return false;
for (var i = 0; i < _magic.length; i++) {
if (bytes[i] != _magic[i]) return false;
}
return true;
}
static _MsfFile fromBytes(Uint8List bytes) {
final superBlock = _Cursor(bytes).read(_superBlock);
final blockSize = superBlock.blockSize;
if (blockSize == 0 || blockSize & (blockSize - 1) != 0) {
throw FormatException('Invalid MSF block size $blockSize');
}
final directorySize = superBlock.numDirectoryBytes;
int blockCount(int size) => (size + blockSize - 1) ~/ blockSize;
final blockMap = _Cursor(bytes, superBlock.blockMapAddr * blockSize);
final directoryBlocks = [
for (var i = 0; i < blockCount(directorySize); i++) blockMap.u32()
];
final directory =
_Cursor(_readBlocks(bytes, blockSize, directoryBlocks, directorySize));
final streamCount = directory.u32();
final sizes = <int>[];
for (var i = 0; i < streamCount; i++) {
final size = directory.u32();
sizes.add(size == _nilStreamSize ? 0 : size);
}
final streams = [
for (final size in sizes)
(size, [for (var i = 0; i < blockCount(size); i++) directory.u32()]),
];
return _MsfFile._(bytes, blockSize, streams);
}
/// Returns the contents of the stream with the given [index], which are
/// only copied if its blocks aren't contiguous.
Uint8List stream(int index) {
final (size, blocks) = _streams[index];
return _readBlocks(_bytes, _blockSize, blocks, size);
}
static Uint8List _readBlocks(
Uint8List bytes, int blockSize, List<int> blocks, int size) {
if (blocks.isEmpty) return Uint8List(0);
var contiguous = true;
for (var i = 1; i < blocks.length && contiguous; i++) {
contiguous = blocks[i] == blocks[i - 1] + 1;
}
final first = blocks.first * blockSize;
if (contiguous) return Uint8List.sublistView(bytes, first, first + size);
final result = Uint8List(size);
for (var i = 0; i < blocks.length; i++) {
final start = blocks[i] * blockSize;
final count = i == blocks.length - 1 ? size - i * blockSize : blockSize;
result.setRange(i * blockSize, i * blockSize + count, bytes, start);
}
return result;
}
}
// Indices of fixed streams.
const _pdbInfoStream = 1;
const _dbiStream = 3;
const _ipiStream = 4;
// Index of the section header stream in the optional debug header of the DBI
// stream.
const _sectionHeaderStreamIndex = 5;
// Symbol record kinds.
const _sEnd = 0x0006;
const _sThunk32 = 0x1102;
const _sBlock32 = 0x1103;
const _sWith32 = 0x1104;
const _sLProc32 = 0x110f;
const _sGProc32 = 0x1110;
const _sPub32 = 0x110e;
const _sSepCode = 0x1132;
const _sEnvBlock = 0x113d;
const _sLProc32Id = 0x1146;
const _sGProc32Id = 0x1147;
const _sInlineSite = 0x114d;
const _sInlineSiteEnd = 0x114e;
const _sProcIdEnd = 0x114f;
const _sLProc32Dpc = 0x1155;
const _sLProc32DpcId = 0x1156;
const _sInlineSite2 = 0x115d;
// Type record kinds.
const _lfFuncId = 0x1601;
const _lfMFuncId = 0x1602;
// Debug subsection kinds.
const _debugSLines = 0xf2;
const _debugSFileChecksums = 0xf4;
const _debugSInlineeLines = 0xf6;
const _debugSIgnore = 0x80000000;
const _linesHaveColumns = 0x1;
const _lineNumberMask = 0x00ffffff;
// The signature of DEBUG_S_INLINEELINES subsections whose entries have extra
// files.
const _inlineeLinesSignatureExtraFiles = 0x1;
/// The location of the code in [start, end), where offsets are relative to
/// the start of the procedure.
class _LineEntry {
final int start;
int end;
final String filename;
final int line;
final int column;
_LineEntry(this.start, this.end, this.filename, this.line, this.column);
}
/// Returns the entry for the code at [offset] in [entries], which are sorted
/// by start and don't overlap.
_LineEntry? _lineEntryAt(List<_LineEntry> entries, int offset) {
// The index of the first entry that starts after [offset].
var low = 0;
var high = entries.length;
while (low < high) {
final middle = (low + high) ~/ 2;
if (entries[middle].start <= offset) {
low = middle + 1;
} else {
high = middle;
}
}
if (low == 0) return null;
final entry = entries[low - 1];
return offset < entry.end ? entry : null;
}
/// A call to an inlined function.
class _InlineSite {
final String name;
final List<_LineEntry> lines;
final List<_InlineSite> children = [];
_InlineSite(this.name, this.lines);
bool contains(int offset) => _lineEntryAt(lines, offset) != null;
}
class _Procedure {
final String name;
final int segment;
final int offset;
final int size;
/// Whether this is a local procedure. Functions that are invisible in stack
/// traces are emitted as local procedures.
final bool isLocal;
final List<_InlineSite> inlineSites = [];
final List<_LineEntry> lines = [];
/// The relative virtual address of the procedure, set once the section
/// headers are known.
int start = 0;
_Procedure(this.name, this.segment, this.offset, this.size, this.isLocal);
}
/// The raw data of a call to an inlined function, which needs the file
/// checksums and inlinee lines of its module to be decoded.
class _RawInlineSite {
final int inlinee;
final Uint8List annotations;
final List<_RawInlineSite> children = [];
_RawInlineSite(this.inlinee, this.annotations);
}
int _decodeSigned(int value) => value & 1 != 0 ? -(value >> 1) : value >> 1;
// The name of the module of the linker, whose environment block has the
// working directory of the link.
const _linkerModuleName = '* Linker *';
// A Dart URI made absolute by LLD with an unknown working directory.
final _absolutizedUriRE = RegExp(r'[\\/]((?:dart|package):[^\\/].*)$');
/// Returns the original name of a file, undoing changes by linkers.
///
/// LLD makes file names that look relative absolute by prepending the working
/// directory of the link [cwd] and using backslashes, which turns URIs like
/// `dart:core/list.dart` into `<cwd>\dart:core\list.dart`, and relative paths
/// from `--resolve-dwarf-paths` like `sdk/lib/core/list.dart` into
/// `<cwd>\sdk\lib\core\list.dart`. The names in snapshots never contain
/// backslashes.
String _originalFilename(String name, String? cwd) {
if (cwd != null && cwd.isNotEmpty) {
final prefix = cwd.endsWith(r'\') ? cwd : '$cwd\\';
if (name.startsWith(prefix)) {
return name.substring(prefix.length).replaceAll(r'\', '/');
}
}
final match = _absolutizedUriRE.firstMatch(name);
if (match == null) return name;
return match[1]!.replaceAll(r'\', '/');
}
/// The debugging information in a PDB file.
///
/// The PDB is expected to be linked from a PE/COFF snapshot, whose CodeView
/// debugging information has a procedure per Code object with the calls to
/// inlined functions as inline sites, and where functions that are invisible
/// in stack traces are local procedures.
class Pdb extends Dwarf {
final String _buildId;
final List<_Procedure> _procedures;
final int? _isolateStart;
final int _moduleCount;
Pdb._(this._buildId, this._procedures, this._isolateStart, this._moduleCount);
/// Whether [reader] contains a PDB file.
static bool isPdb(Reader reader) => _MsfFile.isMsfFile(reader.bytes);
/// Reads the PDB file in [reader]. Returns null if it is not a PDB file, or
/// if it is malformed.
static Pdb? fromReader(Reader reader) {
if (!isPdb(reader)) return null;
try {
return _read(_MsfFile.fromBytes(reader.bytes));
} on RangeError {
return null;
} on FormatException {
return null;
}
}
static Pdb _read(_MsfFile msf) {
// The PDB info stream.
final info = _Cursor(msf.stream(_pdbInfoStream));
final infoHeader = info.read(_pdbStreamHeader);
final namesStream = _namedStreams(info)['/names'];
final names =
namesStream != null ? _StringTable(msf.stream(namesStream)) : null;
final inlineeNames = _functionIdNames(msf.stream(_ipiStream));
// The DBI stream.
final dbi = _Cursor(msf.stream(_dbiStream));
final dbiHeader = dbi.read(_dbiStreamHeader);
// The age in the DBI stream is the one in the CodeView record of the PE
// image, while tools that rewrite the PDB can bump the age in the PDB info
// stream.
final age = dbiHeader.age != 0 ? dbiHeader.age : infoHeader.age;
final modules = <({String name, _ModuleInfoHeader header})>[];
final moduleInfoStart = dbi.offset;
while (dbi.offset < moduleInfoStart + dbiHeader.modiSubstreamSize) {
final header = dbi.read(_moduleInfoHeader);
final name = dbi.string();
dbi.string(); // Object file name.
dbi.align(4, base: moduleInfoStart);
modules.add((name: name, header: header));
}
String? cwd;
for (final (:name, :header) in modules) {
if (name == _linkerModuleName && header.moduleSymStream != 0xffff) {
cwd = _workingDirectory(
msf.stream(header.moduleSymStream), header.symBytes);
}
}
final procedures = <_Procedure>[];
for (final (name: _, :header) in modules) {
if (header.moduleSymStream == 0xffff) continue;
procedures.addAll(_readModule(msf.stream(header.moduleSymStream), header,
names, inlineeNames, cwd));
}
// The section headers, from the optional debug header.
dbi.offset = moduleInfoStart +
dbiHeader.modiSubstreamSize +
dbiHeader.secContrSubstreamSize +
dbiHeader.sectionMapSize +
dbiHeader.fileInfoSize +
dbiHeader.typeServerSize +
dbiHeader.ecSubstreamSize;
final debugStreams = [
for (var i = 0; i < dbiHeader.optionalDbgHdrSize ~/ 2; i++) dbi.u16()
];
final sectionAddresses = <int>[];
if (debugStreams.length > _sectionHeaderStreamIndex &&
debugStreams[_sectionHeaderStreamIndex] != 0xffff) {
final sections =
_Cursor(msf.stream(debugStreams[_sectionHeaderStreamIndex]));
while (sections.offset + _sectionHeader.size <= sections.length) {
sectionAddresses.add(sections.read(_sectionHeader).virtualAddress);
}
}
int? relativeVirtualAddress(int segment, int offset) =>
segment >= 1 && segment <= sectionAddresses.length
? sectionAddresses[segment - 1] + offset
: null;
for (final procedure in procedures) {
procedure.start =
relativeVirtualAddress(procedure.segment, procedure.offset) ?? -1;
}
procedures.removeWhere((p) => p.start < 0);
procedures.sort((a, b) => a.start.compareTo(b.start));
// The start of the isolate instructions, from the public symbols. Like
// the ELF and Mach-O readers, falls back to the old symbol name.
int? textStart;
int? oldIsolateStart;
if (dbiHeader.symRecordStreamIndex != 0xffff) {
final symbols = _Cursor(msf.stream(dbiHeader.symRecordStreamIndex));
while (symbols.offset + _recordPrefix.size <= symbols.length) {
final (kind, end) = symbols.recordPrefix();
if (kind == _sPub32) {
final symbol = symbols.read(_publicSym32);
final name = symbols.string();
if (name == constants.textSymbolName) {
textStart = relativeVirtualAddress(symbol.segment, symbol.offset);
} else if (name == constants.oldIsolateSymbolName) {
oldIsolateStart =
relativeVirtualAddress(symbol.segment, symbol.offset);
}
}
symbols.offset = end;
}
}
return Pdb._(_buildIdOf(infoHeader.guid, age), procedures,
textStart ?? oldIsolateStart, modules.length);
}
/// Returns the build ID of the PE images linked with the PDB with the
/// [guid] and [age], formatted like the VM does: like in the paths of symbol
/// servers, where the first three fields of the GUID and the age are
/// integers.
static String _buildIdOf(_Guid guid, int age) {
String hex(int value, int digits) =>
value.toRadixString(16).padLeft(digits, '0');
return [
hex(guid.data1, 8),
hex(guid.data2, 4),
hex(guid.data3, 4),
for (var i = 0; i < 8; i++) hex(guid.data4[i], 2),
age.toRadixString(16),
].join();
}
/// Returns the working directory in the environment block of the symbols
/// of the linker module.
static String? _workingDirectory(Uint8List stream, int symbolsSize) {
final symbols = _Cursor(stream, 4); // Skip the signature.
while (symbols.offset + _recordPrefix.size <= symbolsSize) {
final (kind, end) = symbols.recordPrefix();
if (kind == _sEnvBlock) {
symbols.read(_envBlockSym);
// Pairs of keys and values, ending with an empty key.
while (symbols.offset < end) {
final key = symbols.string();
if (key.isEmpty) break;
final value = symbols.string();
if (key == 'cwd') return value;
}
}
symbols.offset = end;
}
return null;
}
/// Reads the named stream map of the PDB info stream.
static Map<String, int> _namedStreams(_Cursor info) {
final stringsSize = info.u32();
final stringsStart = info.offset;
info.offset += stringsSize;
final size = info.u32();
final capacity = info.u32();
List<int> readBitVector() => [
for (var i = info.u32(); i > 0; i--) info.u32(),
];
final present = readBitVector();
readBitVector(); // Deleted.
final result = <String, int>{};
for (var i = 0; i < capacity && result.length < size; i++) {
if (i ~/ 32 >= present.length || present[i ~/ 32] & (1 << i % 32) == 0) {
continue;
}
final nameOffset = info.u32();
final stream = info.u32();
final position = info.offset;
info.offset = stringsStart + nameOffset;
result[info.string()] = stream;
info.offset = position;
}
return result;
}
/// Returns the names of the function IDs in the IPI stream.
static Map<int, String> _functionIdNames(Uint8List stream) {
final result = <int, String>{};
if (stream.isEmpty) return result;
final ipi = _Cursor(stream);
final header = ipi.read(_tpiStreamHeader);
ipi.offset = header.headerSize;
var index = header.typeIndexBegin;
while (ipi.offset + _recordPrefix.size <= ipi.length) {
final (kind, end) = ipi.recordPrefix();
if (kind == _lfFuncId || kind == _lfMFuncId) {
ipi.read(_funcIdRecord);
result[index] = ipi.string();
}
ipi.offset = end;
index++;
}
return result;
}
static List<_Procedure> _readModule(
Uint8List stream,
_ModuleInfoHeader module,
_StringTable? names,
Map<int, String> inlineeNames,
String? cwd) {
// The symbols, with the inline sites of each procedure.
final procedures = <_Procedure>[];
final rawInlineSites = <_Procedure, List<_RawInlineSite>>{};
final symbols = _Cursor(stream, 4); // Skip the signature.
// The open scopes: procedures, inline sites, and other scopes (null).
final scopes = <Object?>[];
while (symbols.offset + _recordPrefix.size <= module.symBytes) {
final (kind, end) = symbols.recordPrefix();
switch (kind) {
case _sGProc32 || _sLProc32 || _sGProc32Id || _sLProc32Id:
final symbol = symbols.read(_procSym);
final procedure = _Procedure(
symbols.string(),
symbol.segment,
symbol.codeOffset,
symbol.codeSize,
kind == _sLProc32 || kind == _sLProc32Id);
procedures.add(procedure);
rawInlineSites[procedure] = [];
scopes.add(procedure);
case _sInlineSite || _sInlineSite2:
final inlinee = kind == _sInlineSite
? symbols.read(_inlineSiteSym).inlinee
: symbols.read(_inlineSiteSym2).inlinee;
final site =
_RawInlineSite(inlinee, symbols.bytes(end - symbols.offset));
final parent = scopes.lastWhere(
(s) => s is _Procedure || s is _RawInlineSite,
orElse: () => null);
if (parent is _RawInlineSite) {
parent.children.add(site);
} else if (parent is _Procedure) {
rawInlineSites[parent]!.add(site);
}
scopes.add(site);
// Other symbols that open scopes ended by S_END, like procedures of
// other producers.
case _sThunk32 ||
_sBlock32 ||
_sWith32 ||
_sSepCode ||
_sLProc32Dpc ||
_sLProc32DpcId:
scopes.add(null);
case _sEnd || _sProcIdEnd || _sInlineSiteEnd:
if (scopes.isNotEmpty) scopes.removeLast();
}
symbols.offset = end;
}
// The C13 debug subsections.
final fileChecksums = <int, String>{};
final inlineeLines = <int, (int, int)>{};
final lineSubsections = <Uint8List>[];
final subsections = _Cursor(stream, module.symBytes + module.c11Bytes);
final subsectionsEnd = subsections.offset + module.c13Bytes;
while (subsections.offset + _debugSubsectionHeader.size <= subsectionsEnd) {
final header = subsections.read(_debugSubsectionHeader);
final contents = subsections.bytes(header.length);
subsections.align(4);
if (header.kind & _debugSIgnore != 0) continue;
switch (header.kind) {
case _debugSLines:
lineSubsections.add(contents);
case _debugSFileChecksums:
final checksums = _Cursor(contents);
while (checksums.offset + _fileChecksumEntryHeader.size <=
checksums.length) {
final offset = checksums.offset;
final entry = checksums.read(_fileChecksumEntryHeader);
checksums.offset += entry.checksumSize;
checksums.align(4);
final name = names?.at(entry.fileNameOffset);
fileChecksums[offset] =
name != null ? _originalFilename(name, cwd) : '<unknown file>';
}
case _debugSInlineeLines:
final lines = _Cursor(contents);
final hasExtraFiles = lines.u32() == _inlineeLinesSignatureExtraFiles;
while (lines.offset + _inlineeSourceLineHeader.size <= lines.length) {
final entry = lines.read(_inlineeSourceLineHeader);
// As in LLVM, the first entry is used if the linker merged the
// IDs of different inlined functions.
inlineeLines.putIfAbsent(
entry.inlinee, () => (entry.fileId, entry.sourceLineNum));
if (hasExtraFiles) {
final extraFileCount = lines.u32();
lines.offset += 4 * extraFileCount;
}
}
}
}
String filename(int checksumOffset) =>
fileChecksums[checksumOffset] ?? '<unknown file>';
// The line tables of the procedures.
final proceduresByAddress = {
for (final p in procedures) (p.segment, p.offset): p,
};
for (final contents in lineSubsections) {
final lines = _Cursor(contents);
final header = lines.read(_lineFragmentHeader);
final procedure =
proceduresByAddress[(header.relocSegment, header.relocOffset)];
if (procedure == null) continue;
final hasColumns = header.flags & _linesHaveColumns != 0;
while (lines.offset + _lineBlockFragmentHeader.size <= lines.length) {
final block = lines.read(_lineBlockFragmentHeader);
final file = filename(block.nameIndex);
final entries = [
for (var i = 0; i < block.numLines; i++) lines.read(_lineNumberEntry)
];
final columns = [
if (hasColumns)
for (var i = 0; i < block.numLines; i++)
lines.read(_columnNumberEntry)
];
for (var i = 0; i < entries.length; i++) {
procedure.lines.add(_LineEntry(
entries[i].offset,
header.codeSize,
file,
entries[i].flags & _lineNumberMask,
hasColumns ? columns[i].startColumn : 0));
}
}
// Sorts stably, so that of the entries at the same offset, the last one
// in the line table covers the code.
final sorted = procedure.lines.indexed.toList()
..sort((a, b) {
final byStart = a.$2.start.compareTo(b.$2.start);
return byStart != 0 ? byStart : a.$1.compareTo(b.$1);
});
procedure.lines
..clear()
..addAll(sorted.map((e) => e.$2));
for (var i = 0; i < procedure.lines.length - 1; i++) {
procedure.lines[i].end = procedure.lines[i + 1].start;
}
}
// The inline sites, whose binary annotations are decoded now that the
// file checksums and inlinee lines are known.
_InlineSite decode(_RawInlineSite raw) {
final (fileId, startLine) = inlineeLines[raw.inlinee] ?? (-1, 0);
final site = _InlineSite(
inlineeNames[raw.inlinee] ?? '<unknown>',
_decodeAnnotations(
raw.annotations, filename(fileId), startLine, filename));
site.children.addAll(raw.children.map(decode));
return site;
}
for (final procedure in procedures) {
procedure.inlineSites.addAll(rawInlineSites[procedure]!.map(decode));
}
return procedures;
}
/// Decodes the binary annotations of an inline site into the locations of
/// its code.
static List<_LineEntry> _decodeAnnotations(Uint8List annotations,
String startFilename, int startLine, String Function(int) filename) {
final cursor = _Cursor(annotations);
int? read() {
if (cursor.done) return null;
final first = cursor.u8();
if (first & 0x80 == 0) return first;
if (first & 0xc0 == 0x80) {
if (cursor.done) return null;
return (first & 0x3f) << 8 | cursor.u8();
}
if (first & 0xe0 == 0xc0) {
if (cursor.offset + 3 > cursor.length) return null;
return (first & 0x1f) << 24 |
cursor.u8() << 16 |
cursor.u8() << 8 |
cursor.u8();
}
return null;
}
final entries = <_LineEntry>[];
var codeOffset = 0;
var file = startFilename;
var line = startLine;
var column = 0;
// Starts the code of a new entry at the current code offset, which ends
// the previous one unless it has an explicit length.
void startEntry() {
if (entries.isNotEmpty && entries.last.end < 0) {
entries.last.end = codeOffset;
}
entries.add(_LineEntry(codeOffset, -1, file, line, column));
}
// Ends the code of the current entry after [length] bytes.
void setLength(int length) {
if (entries.isEmpty) return;
entries.last.end = entries.last.start + length;
codeOffset = entries.last.end;
}
decoding:
while (true) {
final opcode = read();
if (opcode == null || opcode == 0) break;
final operand = read();
if (operand == null) break;
switch (opcode) {
case 1: // CodeOffset
codeOffset = operand;
startEntry();
case 2 || 3: // ChangeCodeOffsetBase, ChangeCodeOffset
codeOffset += operand;
startEntry();
case 4: // ChangeCodeLength
setLength(operand);
case 5: // ChangeFile
file = filename(operand);
case 6: // ChangeLineOffset
line += _decodeSigned(operand);
case 9: // ChangeColumnStart
column = operand;
case 11: // ChangeCodeOffsetAndLineOffset
line += _decodeSigned(operand >> 4);
codeOffset += operand & 0xf;
startEntry();
case 12: // ChangeCodeLengthAndCodeOffset
final delta = read();
if (delta == null) break decoding;
codeOffset += delta;
startEntry();
setLength(operand);
// ChangeLineEndDelta, ChangeRangeKind, ChangeColumnEndDelta and
// ChangeColumnEnd don't affect the start locations.
}
}
entries.removeWhere((e) => e.end < 0);
// _lineEntryAt needs the entries sorted. Code offsets only grow, except
// with CodeOffset, which can set any offset.
entries.sort((a, b) => a.start.compareTo(b.start));
return entries;
}
@override
String? buildId([String? arch]) => _buildId;
@override
int? vmStartAddress([String? arch]) => null;
@override
int? isolateStartAddress([String? arch]) => _isolateStart;
_Procedure? _procedureAt(int address) {
var low = 0;
var high = _procedures.length - 1;
while (low <= high) {
final mid = (low + high) ~/ 2;
final procedure = _procedures[mid];
if (address < procedure.start) {
high = mid - 1;
} else if (address >= procedure.start + procedure.size) {
low = mid + 1;
} else {
return procedure;
}
}
return null;
}
List<CallInfo>? _callInfoFor(int address) {
final procedure = _procedureAt(address);
if (procedure == null) return null;
final offset = address - procedure.start;
if (procedure.lines.isEmpty) {
return [StubCallInfo(name: procedure.name, offset: offset)];
}
// The calls to inlined functions containing the address, outermost first.
final sites = <_InlineSite>[];
var candidates = procedure.inlineSites;
while (true) {
final site = candidates.where((s) => s.contains(offset)).firstOrNull;
if (site == null) break;
sites.add(site);
candidates = site.children;
}
DartCallInfo callInfo(String function, List<_LineEntry> lines,
{required bool inlined, required bool internal}) {
final entry = _lineEntryAt(lines, offset);
return DartCallInfo(
function: function,
inlined: inlined,
internal: internal,
filename: entry?.filename ?? lines.first.filename,
line: entry?.line ?? 0,
column: entry?.column ?? 0);
}
return [
for (final site in sites.reversed)
callInfo(site.name, site.lines, inlined: true, internal: false),
callInfo(procedure.name, procedure.lines,
inlined: false,
// As for artificial DWARF subprograms, don't hide invisible
// functions at their start, which appear as Future listeners.
internal: procedure.isLocal && (sites.isNotEmpty || offset != 0)),
];
}
@override
Iterable<CallInfo>? callInfoForPCOffset(PCOffset pcOffset,
{bool includeInternalFrames = false}) {
final calls = _callInfoFor(virtualAddressOf(pcOffset));
if (!includeInternalFrames) {
return calls?.where((CallInfo c) => !c.isInternal);
}
return calls;
}
@override
int virtualAddressOf(PCOffset pcOffset) {
switch (pcOffset.section) {
case InstructionsSection.none:
// This address is already virtualized, so we don't need to change it.
return pcOffset.offset;
case InstructionsSection.vm:
throw 'Cannot locate VM instructions section in PDB';
case InstructionsSection.isolate:
final isolateStart = isolateStartAddress(pcOffset.architecture);
if (isolateStart == null) {
throw 'Cannot locate isolate instructions section in PDB';
}
return pcOffset.offset + isolateStart;
}
}
@override
void writeToStringBuffer(StringBuffer buffer) {
buffer
..writeln('PDB file')
..writeln(' Build ID: ${buildId()}')
..writeln(' Modules: $_moduleCount')
..writeln(' Procedures: ${_procedures.length}');
if (_isolateStart != null) {
buffer.writeln(' Isolate instructions: '
'0x${_isolateStart.toRadixString(16)}');
}
}
@override
String dumpFileInfo() {
final buffer = StringBuffer();
writeToStringBuffer(buffer);
for (final procedure in _procedures) {
buffer
..write(' 0x')
..write(procedure.start.toRadixString(16))
..write(' ')
..write(procedure.size)
..write(' ')
..writeln(procedure.name);
}
return buffer.toString();
}
}
/// The string table in the /names stream.
class _StringTable {
final Uint8List _strings;
_StringTable._(this._strings);
factory _StringTable(Uint8List stream) {
final cursor = _Cursor(stream);
final header = cursor.read(_stringTableHeader);
return _StringTable._(cursor.bytes(header.byteSize));
}
String? at(int offset) =>
offset < _strings.length ? _Cursor(_strings, offset).string() : null;
}