// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using System.Reflection.PortableExecutable;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using Antlr4.Runtime;
using Antlr4.Runtime.Misc;
namespace ILAssembler
{
internal sealed partial class GrammarActions
{
public (ImmutableArray<Diagnostic> Diagnostics, CompilationResult? Image) BuildImage()
{
// Default module name to output filename if no .module directive was provided
if (_entityRegistry.Module.Name is null && _options.OutputFileName is not null)
{
_entityRegistry.Module.Name = _options.OutputFileName;
}
// Apply DebuggableAttribute AFTER all source declarations have been processed,
// so that GetCoreLibAssemblyReference() can find the correct corelib assembly ref
// declared in the source (e.g., System.Runtime) instead of creating a fallback mscorlib.
if (_options.Debug || _options.DebugMode is not null)
{
ApplyDebuggableAttribute();
}
Machine requestedMachine = _options.Machine ?? Machine.I386;
Machine machine = requestedMachine == Machine.Unknown ? Machine.I386 : requestedMachine;
if (!(machine is Machine.I386 or Machine.Amd64 or Machine.Arm64))
{
throw new NotSupportedException($"Target machine '{machine}' is not supported.");
}
ImmutableArray<ValidatedVTableFixup> validatedVTableFixups =
ValidateVTableFixups(machine);
ImmutableArray<ValidatedVTableAssociation> validatedVTableAssociations =
ValidateVTableAssociations(validatedVTableFixups);
ImmutableArray<ILAssemblerPEBuilder.DataLabelFixup> validatedDataLabelFixups =
ValidateDataLabelFixups();
ImmutableArray<ValidatedExport> validatedExports =
ValidateExports(validatedVTableAssociations, machine);
if (_options.PseudoAttributes)
{
PseudoCustomAttributes.Lower(_entityRegistry, _diagnostics);
}
// Return early if there are structural errors that prevent building valid metadata.
// However, allow errors in method bodies (ILA0016-0019) to pass through so we can
// emit the assembly with the errors reported.
// In error-tolerant mode, continue despite errors.
var structuralErrors = _diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error && !IsRecoverableError(d.Id));
if (structuralErrors.Any() && !_options.ErrorTolerant)
{
return (_diagnostics.ToImmutable(), null);
}
BlobBuilder ilStream = new();
Blob mvidFixup = _entityRegistry.WriteContentTo(_metadataBuilder, ilStream, _mappedFieldDataNames, out ImmutableArray<DataLocation> mappedFields, _options.Deterministic, _options.Fold);
// MetadataRootBuilder only supports module-wide validation suppression, which is
// required because wrapped GenericParam numbers intentionally violate table ordering.
bool suppressMetadataValidation =
_options.ErrorTolerant &&
_diagnostics.Any(diagnostic => diagnostic.Id == DiagnosticIds.TooManyGenericParameters);
MetadataRootBuilder rootBuilder = new(
_metadataBuilder,
_options.MetadataVersion,
suppressValidation: suppressMetadataValidation);
// Apply command-line overrides
Subsystem subsystem = _options.Subsystem ?? _subsystem;
int fileAlignment = _options.FileAlignment ?? _alignment;
long imageBase = _options.ImageBase ?? _imageBase;
ushort majorSubsystemVersion = _options.SubsystemVersion?.Major ?? 4;
ushort minorSubsystemVersion = _options.SubsystemVersion?.Minor ?? 0;
// Build DllCharacteristics from options
DllCharacteristics dllCharacteristics = DllCharacteristics.DynamicBase | DllCharacteristics.NxCompatible | DllCharacteristics.NoSeh | DllCharacteristics.TerminalServerAware;
if (_options.AppContainer)
{
dllCharacteristics |= DllCharacteristics.AppContainer;
}
if (_options.HighEntropyVA)
{
dllCharacteristics |= DllCharacteristics.HighEntropyVirtualAddressSpace;
}
if (_options.StripReloc)
{
dllCharacteristics &= ~DllCharacteristics.DynamicBase;
}
Characteristics imageCharacteristics = Characteristics.ExecutableImage;
if (_options.Dll)
{
imageCharacteristics |= Characteristics.Dll;
}
if (machine == Machine.I386)
{
imageCharacteristics |= Characteristics.Bit32Machine;
}
else if (machine is Machine.Amd64 or Machine.Arm64)
{
imageCharacteristics |= Characteristics.LargeAddressAware;
}
// Compute stack reserve: command-line option overrides directive, which overrides default
ulong sizeOfStackReserve = (ulong)(_options.StackReserve ?? (_stackReserve != 0 ? _stackReserve : 0x00100000));
PEHeaderBuilder header = new(
machine: machine,
fileAlignment: fileAlignment,
imageBase: (ulong)imageBase,
subsystem: subsystem,
majorSubsystemVersion: majorSubsystemVersion,
minorSubsystemVersion: minorSubsystemVersion,
dllCharacteristics: dllCharacteristics,
imageCharacteristics: imageCharacteristics,
sizeOfStackReserve: sizeOfStackReserve);
MethodDefinitionHandle entryPoint = default;
if (_entityRegistry.EntryPoint is not null)
{
entryPoint = (MethodDefinitionHandle)_entityRegistry.EntryPoint.Handle;
}
// Build the PDB and the debug directory that references it, if a PDB is requested
DebugDirectoryBuilder? debugDirectoryBuilder = BuildDebugDirectory(entryPoint, out ImmutableArray<byte>? portablePdb);
Func<IEnumerable<Blob>, BlobContentId>? deterministicIdProvider = _options.Deterministic
? GetDeterministicContentId
: null;
ImmutableArray<ILAssemblerPEBuilder.VTableFixupInfo> vtableFixupInfos =
BuildVTableFixupInfos(
validatedVTableFixups,
validatedVTableAssociations);
ImmutableArray<ILAssemblerPEBuilder.ExportInfo> exports =
BuildExportInfos(validatedExports);
// Apply CorFlags from options or directive
CorFlags corFlags = _options.CorFlags ?? _corflags;
if (_options.Prefer32Bit)
{
corFlags |= CorFlags.Requires32Bit | CorFlags.Prefers32Bit;
}
ILAssemblerPEBuilder peBuilder = new(
header,
rootBuilder,
ilStream,
_mappedFieldData,
_manifestResources,
debugDirectoryBuilder: debugDirectoryBuilder,
entryPoint: entryPoint,
flags: corFlags,
deterministicIdProvider: deterministicIdProvider,
vtableFixups: vtableFixupInfos,
exports: exports,
dataLabelFixups: validatedDataLabelFixups,
tlsData: _tlsData,
cilData: _cilData,
mappedFields: mappedFields);
return (_diagnostics.ToImmutable(), new CompilationResult(peBuilder, mvidFixup, portablePdb));
}
private static BlobContentId GetDeterministicContentId(IEnumerable<Blob> content)
=> BlobContentId.FromHash(ComputeSha256(content));
private static byte[] ComputeSha256(IEnumerable<Blob> content)
{
using IncrementalHash hash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
foreach (Blob blob in content)
{
hash.AppendData(blob.GetBytes());
}
return hash.GetHashAndReset();
}
private ImmutableArray<ValidatedVTableFixup> ValidateVTableFixups(Machine machine)
{
if (_vtableFixups.Count == 0)
{
return ImmutableArray<ValidatedVTableFixup>.Empty;
}
var builder = ImmutableArray.CreateBuilder<ValidatedVTableFixup>(_vtableFixups.Count);
for (int i = 0; i < _vtableFixups.Count; i++)
{
VTableFixupDeclaration declaration = _vtableFixups[i];
if (!declaration.HasValidSlotCount)
{
continue;
}
VTableFixupSupport.VTableFixupEntry entry = declaration.Entry;
const ushort WidthMask =
VTableFixupSupport.COR_VTABLE_32BIT |
VTableFixupSupport.COR_VTABLE_64BIT;
ushort width = (ushort)(entry.Flags & WidthMask);
ushort expectedWidth = VTableFixupSupport.GetPointerSize(machine) == sizeof(long)
? VTableFixupSupport.COR_VTABLE_64BIT
: VTableFixupSupport.COR_VTABLE_32BIT;
if (width != expectedWidth)
{
ReportError(
DiagnosticIds.InvalidVTableWidth,
string.Format(
DiagnosticMessageTemplates.InvalidVTableWidth,
width,
machine,
expectedWidth == VTableFixupSupport.COR_VTABLE_64BIT
? "int64"
: "int32"),
declaration.Context);
continue;
}
if (!_mappedFieldDataNames.TryGetValue(entry.DataLabel, out DataLocation dataLocation))
{
ReportError(
DiagnosticIds.LabelNotFound,
string.Format(DiagnosticMessageTemplates.LabelNotFound, entry.DataLabel),
declaration.Context);
continue;
}
int availableBytes = GetAvailableMappedFieldDataBytes(dataLocation);
int requiredBytes = checked(
entry.SlotCount * VTableFixupSupport.GetSlotSize(entry.Flags));
if (requiredBytes > availableBytes)
{
ReportError(
DiagnosticIds.InsufficientVTableData,
string.Format(
DiagnosticMessageTemplates.InsufficientVTableData,
entry.DataLabel,
availableBytes,
requiredBytes),
declaration.Context);
continue;
}
builder.Add(new ValidatedVTableFixup(i + 1, entry, dataLocation));
}
return builder.ToImmutable();
}
private int GetAvailableMappedFieldDataBytes(DataLocation location)
{
int endOffset = GetDataBuilder(location.Section).Count;
foreach (DataLocation other in _mappedFieldDataNames.Values)
{
if (other.Section == location.Section &&
other.Offset > location.Offset && other.Offset < endOffset)
{
endOffset = other.Offset;
}
}
return endOffset - location.Offset;
}
private ImmutableArray<ILAssemblerPEBuilder.DataLabelFixup> ValidateDataLabelFixups()
{
var builder =
ImmutableArray.CreateBuilder<ILAssemblerPEBuilder.DataLabelFixup>(
_mappedFieldDataReferenceFixups.Count);
foreach (CILParser.DataLabelReferenceValue reference in _mappedFieldDataReferenceFixups)
{
if (!_mappedFieldDataNames.TryGetValue(reference.TargetLabel, out DataLocation target))
{
ReportError(
DiagnosticIds.LabelNotFound,
string.Format(
DiagnosticMessageTemplates.LabelNotFound,
reference.TargetLabel),
reference.Location);
continue;
}
builder.Add(new ILAssemblerPEBuilder.DataLabelFixup(
new DataLocation(reference.Section, reference.DataOffset),
target,
reference.PointerSize));
}
return builder.ToImmutable();
}
private ImmutableArray<ValidatedVTableAssociation> ValidateVTableAssociations(
ImmutableArray<ValidatedVTableFixup> validatedVTableFixups)
{
if (_vtableFixups.Count == 0)
{
return ImmutableArray<ValidatedVTableAssociation>.Empty;
}
var fixupsByOriginalIndex = new Dictionary<int, (int SerializedIndex, ValidatedVTableFixup Fixup)>(
validatedVTableFixups.Length);
for (int i = 0; i < validatedVTableFixups.Length; i++)
{
fixupsByOriginalIndex.Add(
validatedVTableFixups[i].OriginalIndex,
(i + 1, validatedVTableFixups[i]));
}
var builder = ImmutableArray.CreateBuilder<ValidatedVTableAssociation>();
foreach (EntityRegistry.MethodDefinitionEntity method in GetParsedMethods())
{
if (!_vtableEntryDirectiveContexts.TryGetValue(method, out ParserRuleContext? context))
{
continue;
}
if (!fixupsByOriginalIndex.TryGetValue(
method.VTableEntry,
out (int SerializedIndex, ValidatedVTableFixup Fixup) fixup))
{
ReportError(
DiagnosticIds.InvalidVTableEntry,
string.Format(
DiagnosticMessageTemplates.InvalidVTableEntry,
method.Name,
method.VTableEntry),
context);
continue;
}
if (method.VTableSlot <= 0 || method.VTableSlot > fixup.Fixup.Entry.SlotCount)
{
ReportError(
DiagnosticIds.InvalidVTableEntry,
string.Format(
DiagnosticMessageTemplates.InvalidVTableSlot,
method.Name,
method.VTableSlot,
method.VTableEntry,
fixup.Fixup.Entry.SlotCount),
context);
continue;
}
builder.Add(new ValidatedVTableAssociation(method, fixup.SerializedIndex));
}
return builder.ToImmutable();
}
private ImmutableArray<ValidatedExport> ValidateExports(
ImmutableArray<ValidatedVTableAssociation> validatedVTableAssociations,
Machine machine)
{
if (_vtableFixups.Count == 0)
{
return ImmutableArray<ValidatedExport>.Empty;
}
var associationsByMethod =
new Dictionary<EntityRegistry.MethodDefinitionEntity, ValidatedVTableAssociation>(
validatedVTableAssociations.Length);
foreach (ValidatedVTableAssociation association in validatedVTableAssociations)
{
associationsByMethod.Add(association.Method, association);
}
var candidates = ImmutableArray.CreateBuilder<ValidatedExport>();
foreach (EntityRegistry.MethodDefinitionEntity method in GetParsedMethods())
{
if (method.ExportOrdinal < 0)
{
continue;
}
ParserRuleContext context = _exportDirectiveContexts[method];
string exportName = method.ExportAlias ?? method.Name;
if (!ILAssemblerPEBuilder.IsExportMachineSupported(machine))
{
ReportError(
DiagnosticIds.UnsupportedNativeExportMachine,
string.Format(
DiagnosticMessageTemplates.UnsupportedNativeExportMachine,
machine),
context);
continue;
}
if (!associationsByMethod.TryGetValue(
method,
out ValidatedVTableAssociation association))
{
if (!_vtableEntryDirectiveContexts.ContainsKey(method))
{
ReportError(
DiagnosticIds.InvalidVTableExport,
string.Format(
DiagnosticMessageTemplates.InvalidVTableExport,
exportName),
context);
}
continue;
}
candidates.Add(new ValidatedExport(method, association.VTableEntryIndex));
}
if (candidates.Count == 0)
{
return ImmutableArray<ValidatedExport>.Empty;
}
var exportsByOrdinal = new Dictionary<int, ValidatedExport>();
var nonConflictingExports = ImmutableArray.CreateBuilder<ValidatedExport>(candidates.Count);
foreach (ValidatedExport export in candidates)
{
if (exportsByOrdinal.TryGetValue(
export.Method.ExportOrdinal,
out ValidatedExport existingExport) &&
(existingExport.VTableEntryIndex != export.VTableEntryIndex ||
existingExport.Method.VTableSlot != export.Method.VTableSlot))
{
ReportError(
DiagnosticIds.DuplicateExportOrdinal,
string.Format(
DiagnosticMessageTemplates.DuplicateExportOrdinal,
export.Method.ExportAlias ?? export.Method.Name,
export.Method.ExportOrdinal),
_exportDirectiveContexts[export.Method]);
continue;
}
exportsByOrdinal.TryAdd(export.Method.ExportOrdinal, export);
nonConflictingExports.Add(export);
}
int baseOrdinal = nonConflictingExports.Min(export => export.Method.ExportOrdinal);
var validatedExports =
ImmutableArray.CreateBuilder<ValidatedExport>(nonConflictingExports.Count);
foreach (ValidatedExport export in nonConflictingExports)
{
long ordinalIndex = (long)export.Method.ExportOrdinal - baseOrdinal;
if (ordinalIndex > ushort.MaxValue)
{
ReportError(
DiagnosticIds.ExportOrdinalRangeTooLarge,
string.Format(
DiagnosticMessageTemplates.ExportOrdinalRangeTooLarge,
export.Method.ExportOrdinal,
baseOrdinal,
ushort.MaxValue),
_exportDirectiveContexts[export.Method]);
continue;
}
validatedExports.Add(export);
}
return validatedExports.ToImmutable();
}
private static ImmutableArray<ILAssemblerPEBuilder.VTableFixupInfo> BuildVTableFixupInfos(
ImmutableArray<ValidatedVTableFixup> validatedVTableFixups,
ImmutableArray<ValidatedVTableAssociation> validatedVTableAssociations)
{
var builder =
ImmutableArray.CreateBuilder<ILAssemblerPEBuilder.VTableFixupInfo>(
validatedVTableFixups.Length);
for (int i = 0; i < validatedVTableFixups.Length; i++)
{
ValidatedVTableFixup fixup = validatedVTableFixups[i];
var methodTokens = ImmutableArray.CreateBuilder<int>(fixup.Entry.SlotCount);
methodTokens.Count = fixup.Entry.SlotCount;
foreach (ValidatedVTableAssociation association in validatedVTableAssociations)
{
if (association.VTableEntryIndex == i + 1)
{
methodTokens[association.Method.VTableSlot - 1] =
MetadataTokens.GetToken(association.Method.Handle);
}
}
builder.Add(new ILAssemblerPEBuilder.VTableFixupInfo(
fixup.DataLocation,
fixup.Entry.SlotCount,
fixup.Entry.Flags,
methodTokens.MoveToImmutable()));
}
return builder.MoveToImmutable();
}
private static ImmutableArray<ILAssemblerPEBuilder.ExportInfo> BuildExportInfos(
ImmutableArray<ValidatedExport> validatedExports)
{
var builder =
ImmutableArray.CreateBuilder<ILAssemblerPEBuilder.ExportInfo>(
validatedExports.Length);
foreach (ValidatedExport export in validatedExports)
{
builder.Add(new ILAssemblerPEBuilder.ExportInfo(
export.Method.ExportOrdinal,
export.Method.ExportAlias ?? export.Method.Name,
export.VTableEntryIndex,
export.Method.VTableSlot));
}
return builder.MoveToImmutable();
}
private IEnumerable<EntityRegistry.MethodDefinitionEntity> GetParsedMethods()
{
foreach (EntityRegistry.TypeDefinitionEntity type in _entityRegistry.GetSeenEntities(TableIndex.TypeDef))
{
foreach (EntityRegistry.MethodDefinitionEntity method in type.Methods)
{
yield return method;
}
}
}
/// <summary>
/// Gets whether a PDB is requested: <see cref="Options.Debug"/>, <see cref="Options.DebugMode"/> or
/// <see cref="Options.Pdb"/> is set. Information that only the PDB uses, such as the lexical scopes of
/// method bodies, is kept only when this is <see langword="true"/>.
/// </summary>
private bool GeneratesPdb => _options.Debug || _options.DebugMode is not null || _options.Pdb;
/// <summary>
/// Builds the Portable PDB and the debug directory that references it, when a PDB is requested.
/// </summary>
/// <remarks>
/// <para>
/// A PDB is produced when <see cref="Options.Debug"/>, <see cref="Options.DebugMode"/> or
/// <see cref="Options.Pdb"/> is set; <c>.line</c> directives alone do not produce one. Otherwise this
/// returns <see langword="null"/>, the image has no debug directory (<see cref="ILAssemblerPEBuilder"/>),
/// and <paramref name="portablePdb"/> is <see langword="null"/>.
/// </para>
/// <para>
/// The PDB is returned in <paramref name="portablePdb"/> for the caller to write as a separate file;
/// it is not embedded in the image. The debug directory has these entries, in this order:
/// a CodeView entry that names the PDB file (<see cref="GetPdbFilePath"/>) and carries the PDB id;
/// a PdbChecksum entry holding the SHA-256 hash of the PDB with its 20-byte id zeroed
/// (the PDB Checksum Debug Directory Entry in docs/design/specs/PE-COFF.md); and, with
/// <see cref="Options.Deterministic"/>, a Reproducible entry.
/// </para>
/// <para>
/// With <see cref="Options.Deterministic"/>, the PDB id is derived from that same hash
/// (<see cref="BlobContentId.FromHash(ImmutableArray{byte})"/>), so the same input gives the same PDB
/// bytes. The PDB bytes do not depend on <see cref="Options.PdbFilePath"/>; the image does, because
/// the CodeView entry records that path as given (after <see cref="Options.PathMap"/>), so the same input
/// and the same recorded PDB path give the same image bytes, and a PDB path that is a file name alone
/// keeps the image independent of its directory. Without <see cref="Options.Deterministic"/>, the id's
/// GUID is random and its stamp is the current Unix time in seconds. The checksum is the
/// content hash in both cases.
/// </para>
/// </remarks>
private DebugDirectoryBuilder? BuildDebugDirectory(MethodDefinitionHandle entryPoint, out ImmutableArray<byte>? portablePdb)
{
portablePdb = null;
// Only /DEBUG (any mode) or /PDB produces a PDB. Without them, .line
// directives are still parsed, but their PDB coordinates are not validated or recorded.
if (!GeneratesPdb)
{
return null;
}
// Build PDB metadata
BuildPdbMetadata();
// Get row counts from main metadata for the portable PDB
var typeSystemRowCounts = _metadataBuilder.GetRowCounts();
// PortablePdbBuilder hands the id provider the serialized PDB with its 20-byte id zeroed.
// The SHA-256 of that content is the PdbChecksum value, and the deterministic id.
byte[]? pdbContentHash = null;
Func<IEnumerable<Blob>, BlobContentId> timeBasedIdProvider = BlobContentId.GetTimeBasedProvider();
BlobContentId PdbIdProvider(IEnumerable<Blob> content)
{
pdbContentHash = ComputeSha256(content);
return _options.Deterministic
? BlobContentId.FromHash(pdbContentHash)
: timeBasedIdProvider(content);
}
// Create the portable PDB
var pdbBuilder = new PortablePdbBuilder(
_pdbBuilder,
typeSystemRowCounts,
entryPoint,
idProvider: PdbIdProvider);
var pdbBlob = new BlobBuilder();
var pdbContentId = pdbBuilder.Serialize(pdbBlob);
byte[] pdbChecksum = pdbContentHash
?? throw new InvalidOperationException("The Portable PDB was serialized without calling its id provider.");
// Reference the PDB file from the image: CodeView, then PdbChecksum, then (deterministic only)
// Reproducible, as the C# compiler does.
var debugDirectoryBuilder = new DebugDirectoryBuilder();
debugDirectoryBuilder.AddCodeViewEntry(
GetPdbFilePath(),
pdbContentId,
pdbBuilder.FormatVersion);
debugDirectoryBuilder.AddPdbChecksumEntry(
"SHA256",
ImmutableArray.Create(pdbChecksum));
if (_options.Deterministic)
{
debugDirectoryBuilder.AddReproducibleEntry();
}
portablePdb = pdbBlob.ToImmutableArray();
return debugDirectoryBuilder;
}
/// <summary>
/// Gets the PDB path recorded in the CodeView entry: <see cref="Options.PdbFilePath"/> mapped by
/// <see cref="Options.PathMap"/> when set, otherwise <see cref="Options.OutputFileName"/> with its extension
/// replaced by <c>.pdb</c>, otherwise <c>assembly.pdb</c>.
/// </summary>
/// <remarks>
/// The two fallback names have no directory for the map to replace, so they are returned as they are.
/// </remarks>
private string GetPdbFilePath()
{
if (_options.PdbFilePath is not null)
{
return _options.PathMap.Map(_options.PdbFilePath);
}
return _options.OutputFileName is not null
? Path.ChangeExtension(_options.OutputFileName, ".pdb")
: "assembly.pdb";
}
/// <summary>
/// Adds the Document rows, one MethodDebugInformation row per MethodDef row, and each method's LocalScope and
/// LocalVariable rows (<see cref="AddLocalScopes"/>) to the PDB metadata.
/// </summary>
/// <remarks>
/// The documents are added in <see cref="PdbDocumentTable"/> order. A method without sequence points, or
/// without an IL body (<see cref="EntityRegistry.MethodDefinitionEntity.HasBody"/>), gets a row with a nil
/// document and no sequence points blob; a <c>.line</c> directive in a method without a body still defines
/// its file and makes it the current document. A method whose sequence points all belong to one
/// document names that document in its row; a method whose points span several documents has a nil
/// document in its row, and its blob names the documents (<see cref="EncodeSequencePoints"/>).
/// </remarks>
private void BuildPdbMetadata()
{
IReadOnlyList<PdbDocument> documents = _pdbDocuments.Documents;
var documentHandles = new DocumentHandle[documents.Count];
for (int i = 0; i < documents.Count; i++)
{
PdbDocument document = documents[i];
documentHandles[i] = _pdbBuilder.AddDocument(
_pdbBuilder.GetOrAddDocumentName(document.Name),
hashAlgorithm: default,
hash: default,
language: document.Language != Guid.Empty ? _pdbBuilder.GetOrAddGuid(document.Language) : default);
}
foreach (var entity in _entityRegistry.GetSeenEntities(TableIndex.MethodDef))
{
if (entity is not EntityRegistry.MethodDefinitionEntity method)
{
continue;
}
List<EntityRegistry.SequencePoint> sequencePoints = method.DebugInfo.SequencePoints;
if (sequencePoints.Count == 0 || !method.HasBody)
{
// Points are recorded as instructions are emitted, so a method without an IL body has none
// to map; a .line directive inside it applies to the next instruction, in a later method.
_pdbBuilder.AddMethodDebugInformation(default, default);
}
else
{
int firstDocument = sequencePoints[0].DocumentIndex;
bool singleDocument = true;
for (int i = 1; i < sequencePoints.Count && singleDocument; i++)
{
singleDocument = sequencePoints[i].DocumentIndex == firstDocument;
}
BlobBuilder sequencePointsBlob = EncodeSequencePoints(
sequencePoints,
method.DebugInfo.LocalSignature,
documentHandles,
singleDocument);
// docs/design/specs/PortablePdb-Metadata.md, MethodDebugInformation table: Document is "the row id of the
// single document containing all sequence points of the method, or 0 if the method doesn't have sequence
// points or spans multiple documents", and "_InitialDocument_ is only present if the _Document_ field of
// the _MethodDebugInformation_ table is nil"; the blob then names the documents (EncodeSequencePoints).
_pdbBuilder.AddMethodDebugInformation(
singleDocument ? documentHandles[firstDocument] : default,
_pdbBuilder.GetOrAddBlob(sequencePointsBlob));
}
AddLocalScopes((MethodDefinitionHandle)method.Handle, method.DebugInfo.LocalScopes);
}
}
/// <summary>
/// Adds a method's LocalScope rows, each followed by its LocalVariable rows, to the PDB metadata
/// (docs/design/specs/PortablePdb-Metadata.md, "LocalScope Table" and "LocalVariable Table").
/// </summary>
/// <param name="method">The method's MethodDef row. Methods must be added in MethodDef order.</param>
/// <param name="scopes">The method's lexical scopes (<see cref="EntityRegistry.MethodDebugInfo.LocalScopes"/>).</param>
/// <remarks>
/// <para>
/// A scope gets a row only when it declares a named local, and an unnamed local gets no
/// row; so the root scope, which spans the whole body, has a row only when the method-level <c>.locals</c>
/// name a local. Each variable's Index is its slot in the local signature, and its attributes are 0.
/// Import scopes and local constants are not recorded.
/// </para>
/// <para>
/// The rows are written as declared: each scope that declares a named local gets a
/// LocalScope row whatever its length, and each named local of the scope gets a LocalVariable row, in
/// declaration order. Rows that the specification does not allow are reported as errors, so they are
/// written only to error-tolerant output (<see cref="Options.ErrorTolerant"/>):
/// </para>
/// <list type="bullet">
/// <item><description>
/// A scope without IL (a block with no instructions, or the root scope of a method without instructions)
/// has length 0, but a LocalScope row's Length is in "(0..0x80000000)". It is reported at the scope's first
/// named local (<see cref="DiagnosticIds.LocalScopeWithoutInstructions"/>).
/// </description></item>
/// <item><description>
/// "There shall be no duplicate rows in the LocalVariable table, based upon owner and _Name_", and none
/// "based upon owner and _Index_". A named local whose name an earlier named local of the scope has is
/// reported at its declaration (<see cref="DiagnosticIds.LocalNameDeclaredTwiceInScope"/>), and so is one
/// whose slot an earlier named local of the scope has
/// (<see cref="DiagnosticIds.LocalSlotNamedTwiceInScope"/>); a local that repeats both is reported twice.
/// Unnamed locals have no row, so they take no part.
/// </description></item>
/// </list>
/// <para>
/// The scopes are recorded only when a PDB is requested, so neither is checked without one.
/// </para>
/// <para>
/// The rows are sorted by start offset, then by length with the longest first, as the table requires;
/// scopes with the same range keep their source order, enclosing scope first. Scopes of a method nest or
/// are disjoint because blocks do, and the variables of a scope are added together right after it, so its
/// VariableList run holds exactly its variables.
/// </para>
/// </remarks>
private void AddLocalScopes(MethodDefinitionHandle method, List<EntityRegistry.LocalScopeRecord> scopes)
{
// Most methods name no local; they get no rows and need no sorting.
if (!scopes.Exists(static scope => !scope.Variables.IsEmpty))
{
return;
}
IEnumerable<EntityRegistry.LocalScopeRecord> ordered = scopes
.Where(scope => !scope.Variables.IsEmpty)
.OrderBy(scope => scope.StartOffset)
.ThenByDescending(scope => scope.Length)
.ThenBy(scope => scope.Order);
var names = new HashSet<string>(StringComparer.Ordinal);
var slotNames = new Dictionary<int, string>();
foreach (EntityRegistry.LocalScopeRecord scope in ordered)
{
if (scope.Length == 0)
{
EntityRegistry.LocalVariableRecord first = scope.Variables[0];
ReportError(
DiagnosticIds.LocalScopeWithoutInstructions,
string.Format(DiagnosticMessageTemplates.LocalScopeWithoutInstructions, first.Name),
first.Location);
}
_pdbBuilder.AddLocalScope(
method,
importScope: default,
variableList: MetadataTokens.LocalVariableHandle(_pdbBuilder.GetRowCount(TableIndex.LocalVariable) + 1),
constantList: default,
scope.StartOffset,
scope.Length);
names.Clear();
slotNames.Clear();
foreach (EntityRegistry.LocalVariableRecord variable in scope.Variables)
{
if (!names.Add(variable.Name))
{
ReportError(
DiagnosticIds.LocalNameDeclaredTwiceInScope,
string.Format(DiagnosticMessageTemplates.LocalNameDeclaredTwiceInScope, variable.Name),
variable.Location);
}
if (!slotNames.TryAdd(variable.Slot, variable.Name))
{
ReportError(
DiagnosticIds.LocalSlotNamedTwiceInScope,
string.Format(
DiagnosticMessageTemplates.LocalSlotNamedTwiceInScope,
variable.Name,
variable.Slot,
slotNames[variable.Slot]),
variable.Location);
}
_pdbBuilder.AddLocalVariable(
LocalVariableAttributes.None,
variable.Slot,
_pdbBuilder.GetOrAddString(variable.Name));
}
}
}
/// <summary>
/// Encodes a method's sequence points as a sequence points blob (docs/design/specs/PortablePdb-Metadata.md,
/// "Sequence Points Blob").
/// </summary>
/// <param name="sequencePoints">The method's sequence points, in increasing IL offset order. Not empty.</param>
/// <param name="localSignature">
/// The local signature the method body references (<see cref="EntityRegistry.MethodDebugInfo.LocalSignature"/>),
/// or a nil handle when it has none.
/// </param>
/// <param name="documentHandles">The Document rows, indexed by <see cref="EntityRegistry.SequencePoint.DocumentIndex"/>.</param>
/// <param name="singleDocument">
/// <see langword="true"/> when every point belongs to the document of the first point, which the method's
/// MethodDebugInformation row then names.
/// </param>
/// <remarks>
/// The header is the LocalSignature, the StandAloneSig row number of the body's local signature or 0
/// when the body has none, followed, when the points span several documents, by the InitialDocument:
/// the document of the first point. A document-record precedes each non-hidden point whose
/// document differs from the current one. A hidden point has no document-record of its own and
/// belongs to the current document.
/// </remarks>
private static BlobBuilder EncodeSequencePoints(
List<EntityRegistry.SequencePoint> sequencePoints,
StandaloneSignatureHandle localSignature,
DocumentHandle[] documentHandles,
bool singleDocument)
{
Debug.Assert(sequencePoints.Count > 0);
var builder = new BlobBuilder();
// LocalSignature (0 for a nil handle)
builder.WriteCompressedInteger(MetadataTokens.GetRowNumber(localSignature));
int currentDocument = sequencePoints[0].DocumentIndex;
if (!singleDocument)
{
// InitialDocument
builder.WriteCompressedInteger(MetadataTokens.GetRowNumber(documentHandles[currentDocument]));
}
int previousOffset = 0;
int previousStartLine = -1;
int previousStartColumn = -1;
foreach (var sp in sequencePoints)
{
if (!sp.IsHidden && sp.DocumentIndex != currentDocument)
{
// document-record: a zero IL offset delta, then the Document row number.
currentDocument = sp.DocumentIndex;
builder.WriteCompressedInteger(0);
builder.WriteCompressedInteger(MetadataTokens.GetRowNumber(documentHandles[currentDocument]));
}
// IL offset delta
int offsetDelta = sp.ILOffset - previousOffset;
builder.WriteCompressedInteger(offsetDelta);
previousOffset = sp.ILOffset;
if (sp.IsHidden)
{
// Hidden sequence point: delta lines = 0, delta columns = 0
builder.WriteCompressedInteger(0);
builder.WriteCompressedInteger(0);
}
else
{
// Delta lines
int deltaLines = sp.EndLine - sp.StartLine;
builder.WriteCompressedInteger(deltaLines);
// Delta columns
int deltaColumns = sp.EndColumn - sp.StartColumn;
if (deltaLines == 0)
{
builder.WriteCompressedInteger(deltaColumns);
}
else
{
builder.WriteCompressedSignedInteger(deltaColumns);
}
// Start line delta (signed)
if (previousStartLine < 0)
{
builder.WriteCompressedInteger(sp.StartLine);
}
else
{
builder.WriteCompressedSignedInteger(sp.StartLine - previousStartLine);
}
// Start column delta (signed)
if (previousStartColumn < 0)
{
builder.WriteCompressedInteger(sp.StartColumn);
}
else
{
builder.WriteCompressedSignedInteger(sp.StartColumn - previousStartColumn);
}
previousStartLine = sp.StartLine;
previousStartColumn = sp.StartColumn;
}
}
return builder;
}
/// <summary>
/// Add DebuggableAttribute to the assembly based on debug options.
/// - /DEBUG: 0x101 = Default | DisableOptimizations
/// - /DEBUG=OPT: 0x03 = Default | IgnoreSymbolStoreSequencePoints
/// - /DEBUG=IMPL: 0x103 = Default | IgnoreSymbolStoreSequencePoints | DisableOptimizations
/// </summary>
private void ApplyDebuggableAttribute()
{
// DebuggingModes enum values from System.Diagnostics.DebuggableAttribute:
// None = 0x00, Default = 0x01, IgnoreSymbolStoreSequencePoints = 0x02,
// EnableEditAndContinue = 0x04, DisableOptimizations = 0x100
const int DebuggingModesDefault = 0x101; // Default | DisableOptimizations
const int DebuggingModesOpt = 0x03; // Default | IgnoreSymbolStoreSequencePoints
const int DebuggingModesImpl = 0x103; // Default | IgnoreSymbolStoreSequencePoints | DisableOptimizations
int debuggingModes = _options.DebugMode switch
{
DebugMode.Opt => DebuggingModesOpt,
DebugMode.Impl => DebuggingModesImpl,
_ => DebuggingModesDefault
};
// Get reference to core library
var coreAsmRef = _entityRegistry.GetCoreLibAssemblyReference();
// Create reference to System.Diagnostics.DebuggableAttribute
var debuggableAttrType = _entityRegistry.GetOrCreateTypeReference(
coreAsmRef,
new TypeName(null, "System.Diagnostics.DebuggableAttribute"));
var debuggingModesType = _entityRegistry.GetOrCreateTypeReference(
debuggableAttrType,
new TypeName(null, "DebuggingModes"));
BlobBuilder ctorSig = new();
var sigEncoder = new BlobEncoder(ctorSig);
sigEncoder.MethodSignature(SignatureCallingConvention.Default, 0, isInstanceMethod: true)
.Parameters(1,
returnType => returnType.Void(),
parameters => parameters.AddParameter().Type().Type(debuggingModesType.Handle, isValueType: true));
var ctor = _entityRegistry.CreateLazilyRecordedMemberReference(debuggableAttrType, ".ctor", ctorSig);
BlobBuilder attrValue = new();
attrValue.WriteUInt16(0x0001); // Prolog
attrValue.WriteInt32(debuggingModes);
attrValue.WriteUInt16(0x0000); // No named arguments
// Create and attach the custom attribute
var customAttr = _entityRegistry.CreateCustomAttribute(ctor, attrValue);
// Netmodules carry assembly attributes on this TypeRef for the assembly linker.
customAttr.Owner = (EntityRegistry.EntityBase?)_entityRegistry.Assembly
?? _entityRegistry.GetOrCreateTypeReference(
coreAsmRef,
new TypeName(null, "System.Runtime.CompilerServices.AssemblyAttributesGoHere"));
}
}
}