Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/tools/ilasm/ilasm.slnx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<Solution>
<Project Path="src/ilasm/ilasm.csproj" />
<Project Path="src/ILAssembler/ILAssembler.csproj" />
<Project Path="tests/ILAssembler.Tests/ILAssembler.Tests.csproj" />
</Solution>
1 change: 1 addition & 0 deletions src/tools/ilasm/src/ILAssembler/Diagnostic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public static class DiagnosticIds
public const string ParameterIndexOutOfRange = "ILA0029";
public const string DuplicateMethod = "ILA0030";
public const string MissingExportedTypeImplementation = "ILA0031";
public const string KeyFileError = "ILA0032";
}

internal static class DiagnosticMessageTemplates
Expand Down
7 changes: 5 additions & 2 deletions src/tools/ilasm/src/ILAssembler/DocumentCompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,20 @@ public sealed class DocumentCompiler
diagnostics.Add(new Diagnostic("Preprocessor", DiagnosticSeverity.Error, msg, new Location(new(start, length), loadedDocuments[source])));
};

CILParser parser = new(new CommonTokenStream(lexer));
CILParser parser = new(new CommonTokenStream(preprocessor));
var result = parser.decls();
GrammarVisitor visitor = new GrammarVisitor(loadedDocuments, options, resourceLocator);
_ = result.Accept(visitor);

var image = visitor.BuildImage();

bool anyErrors = diagnostics.Any(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error);
anyErrors |= image.Diagnostics.Any(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error);

diagnostics.AddRange(image.Diagnostics);

return (diagnostics.ToImmutable(), anyErrors ? null : image.Image);
// In error-tolerant mode, return image even with errors (like native ilasm /ERR)
bool returnImage = !anyErrors || options.ErrorTolerant;
return (diagnostics.ToImmutable(), returnImage ? image.Image : null);
}
}
29 changes: 25 additions & 4 deletions src/tools/ilasm/src/ILAssembler/EntityRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,21 @@ public void WriteContentTo(MetadataBuilder builder, BlobBuilder ilStream, IReadO
builder.GetOrAddBlob(declSecurity.PermissionSet));
}

foreach (CustomAttributeEntity customAttr in GetSeenEntities(TableIndex.CustomAttribute))
{
EntityHandle parent = customAttr.Owner switch
{
AssemblyEntity => EntityHandle.AssemblyDefinition,
ModuleEntity => EntityHandle.ModuleDefinition,
{ Handle: var h } => h,
_ => default
};
builder.AddCustomAttribute(
parent,
customAttr.Constructor.Handle,
builder.GetOrAddBlob(customAttr.Value));
}

foreach (StandaloneSignatureEntity standaloneSig in GetSeenEntities(TableIndex.StandAloneSig))
{
builder.AddStandaloneSignature(
Expand Down Expand Up @@ -490,8 +505,9 @@ static EntityHandle GetHandleForList(IReadOnlyList<EntityBase> list, IReadOnlyLi
return MetadataTokens.EntityHandle(tokenType, MetadataTokens.GetRowNumber(otherList[otherList.Count - 1].Handle) + 1);
}
}
// If all lists are empty, return a nil handle for the right table
return MetadataTokens.EntityHandle(tokenType, 0);
// If all lists are empty, return row 1 (first potential entry).
// ECMA-335 metadata rows are 1-indexed, so row 0 is invalid.
return MetadataTokens.EntityHandle(tokenType, 1);
}
}

Expand Down Expand Up @@ -547,12 +563,17 @@ private TypeReferenceEntity ResolveFromCoreAssembly(string typeName)
{
// Match native ilasm behavior: check for assembly refs in order of preference,
// then fall back to creating mscorlib if none found
AssemblyReferenceEntity coreAsmRef = FindAssemblyReference("System.Private.CoreLib")
AssemblyReferenceEntity coreAsmRef = GetCoreLibAssemblyReference();
return GetOrCreateTypeReference(coreAsmRef, new TypeName(null, typeName));
}

public AssemblyReferenceEntity GetCoreLibAssemblyReference()
{
return FindAssemblyReference("System.Private.CoreLib")
?? FindAssemblyReference("System.Runtime")
?? FindAssemblyReference("mscorlib")
?? FindAssemblyReference("netstandard")
?? GetOrCreateAssemblyReference("mscorlib", new Version(4, 0), culture: null, publicKeyOrToken: null, 0, ProcessorArchitecture.None);
return GetOrCreateTypeReference(coreAsmRef, new TypeName(null, typeName));
}

public interface IHasHandle
Expand Down
192 changes: 181 additions & 11 deletions src/tools/ilasm/src/ILAssembler/GrammarVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
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;
Expand Down Expand Up @@ -134,8 +136,9 @@ private void ReportWarning(string id, string message, Antlr4.Runtime.ParserRuleC
// 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 (like native ilasm /ERR), continue despite errors.
var structuralErrors = _diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error && !IsRecoverableError(d.Id));
if (structuralErrors.Any())
if (structuralErrors.Any() && !_options.ErrorTolerant)
{
return (_diagnostics.ToImmutable(), null);
}
Expand All @@ -157,17 +160,44 @@ private void ReportWarning(string id, string message, Antlr4.Runtime.ParserRuleC

BlobBuilder ilStream = new();
_entityRegistry.WriteContentTo(_metadataBuilder, ilStream, _mappedFieldDataNames);
MetadataRootBuilder rootBuilder = new(_metadataBuilder);
MetadataRootBuilder rootBuilder = new(_metadataBuilder, _options.MetadataVersion);

// Compute metadata size from the MetadataSizes
// We need this for data label fixup RVA calculations
var sizes = rootBuilder.Sizes;
int metadataSize = ComputeMetadataSize(sizes);

// 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;
Machine machine = _options.Machine ?? Machine.I386;

// 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;
}

PEHeaderBuilder header = new(
fileAlignment: _alignment,
imageBase: (ulong)_imageBase,
subsystem: _subsystem);
machine: machine,
fileAlignment: fileAlignment,
imageBase: (ulong)imageBase,
subsystem: subsystem,
majorSubsystemVersion: majorSubsystemVersion,
minorSubsystemVersion: minorSubsystemVersion,
dllCharacteristics: dllCharacteristics);

MethodDefinitionHandle entryPoint = default;
if (_entityRegistry.EntryPoint is not null)
Expand All @@ -183,6 +213,13 @@ private void ReportWarning(string id, string message, Antlr4.Runtime.ParserRuleC
{
var vtableFixupInfos = BuildVTableFixupInfos();

// Apply CorFlags from options or directive
CorFlags corFlags = _options.CorFlags ?? _corflags;
if (_options.Prefer32Bit)
{
corFlags |= CorFlags.Prefers32Bit;
}

VTableExportPEBuilder peBuilder = new(
header,
rootBuilder,
Expand All @@ -191,7 +228,7 @@ private void ReportWarning(string id, string message, Antlr4.Runtime.ParserRuleC
_manifestResources,
debugDirectoryBuilder: debugDirectoryBuilder,
entryPoint: entryPoint,
flags: CorFlags.ILOnly,
flags: corFlags,
vtableFixups: vtableFixupInfos,
exports: exports.ToImmutable(),
mappedFieldDataOffsets: _mappedFieldDataNames,
Expand All @@ -202,15 +239,36 @@ private void ReportWarning(string id, string message, Antlr4.Runtime.ParserRuleC
return (_diagnostics.ToImmutable(), peBuilder);
}

// Apply CorFlags from options or directive
CorFlags standardCorFlags = _options.CorFlags ?? _corflags;
if (_options.Prefer32Bit)
{
standardCorFlags |= CorFlags.Prefers32Bit;
}

// Deterministic ID provider for reproducible builds
Func<IEnumerable<Blob>, BlobContentId>? deterministicIdProvider = _options.Deterministic
? content =>
{
using var hash = IncrementalHash.CreateHash(System.Security.Cryptography.HashAlgorithmName.SHA256);
foreach (var blob in content)
{
hash.AppendData(blob.GetBytes());
}
return BlobContentId.FromHash(hash.GetHashAndReset());
}
: null;

ManagedPEBuilder standardBuilder = new(
header,
rootBuilder,
ilStream,
_mappedFieldData,
_manifestResources,
flags: CorFlags.ILOnly,
flags: standardCorFlags,
entryPoint: entryPoint,
debugDirectoryBuilder: debugDirectoryBuilder);
debugDirectoryBuilder: debugDirectoryBuilder,
deterministicIdProvider: deterministicIdProvider);

return (_diagnostics.ToImmutable(), standardBuilder);
}
Expand Down Expand Up @@ -271,7 +329,9 @@ private void ReportWarning(string id, string message, Antlr4.Runtime.ParserRuleC
}
}

if (!hasDebugInfo)
// Generate PDB if we have debug info OR if --debug/--pdb options are set
bool generatePdb = hasDebugInfo || _options.Debug || _options.Pdb;
if (!generatePdb)
{
return null;
}
Expand Down Expand Up @@ -530,16 +590,123 @@ public GrammarResult VisitAsmOrRefDecl(CILParser.AsmOrRefDeclContext context)
GrammarResult ICILVisitor<GrammarResult>.VisitAssemblyBlock(CILParser.AssemblyBlockContext context) => VisitAssemblyBlock(context);
public GrammarResult VisitAssemblyBlock(CILParser.AssemblyBlockContext context)
{
_entityRegistry.Assembly ??= new EntityRegistry.AssemblyEntity(VisitDottedName(context.dottedName()).Value);
// Use command-line override if specified, otherwise use the name from the .assembly directive
string assemblyName = _options.AssemblyName ?? VisitDottedName(context.dottedName()).Value;
_entityRegistry.Assembly ??= new EntityRegistry.AssemblyEntity(assemblyName);
var attr = VisitAsmAttr(context.asmAttr()).Value;
(_entityRegistry.Assembly.ProcessorArchitecture, _entityRegistry.Assembly.Flags) = GetArchAndFlags(attr);
foreach (var decl in context.assemblyDecls().assemblyDecl())
{
VisitAssemblyDecl(decl);
}

// Apply command-line key file override (overrides .publickey directive)
if (_options.KeyFile is not null)
{
ApplyKeyFile(_options.KeyFile);
}

// Apply DebuggableAttribute for --debug option
if (_options.Debug || _options.DebugMode is not null)
{
ApplyDebuggableAttribute();
}

return GrammarResult.SentinelValue.Result;
}

/// <summary>
/// Add DebuggableAttribute to the assembly based on debug options.
/// Native ilasm values:
/// - /DEBUG: 0x101 = Default | DisableOptimizations
/// - /DEBUG=OPT: 0x03 = Default | IgnoreSymbolStoreSequencePoints
/// - /DEBUG=IMPL: 0x103 = Default | DisableOptimizations | EnableEditAndContinue
/// </summary>
private void ApplyDebuggableAttribute()
{
if (_entityRegistry.Assembly is null)
{
return;
}

// Determine the DebuggingModes value
// System.Diagnostics.DebuggableAttribute.DebuggingModes enum values:
// None = 0, Default = 1, DisableOptimizations = 256, IgnoreSymbolStoreSequencePoints = 2, EnableEditAndContinue = 4
int debuggingModes;
if (_options.DebugMode?.Equals("opt", StringComparison.OrdinalIgnoreCase) == true)
{
// /DEBUG=OPT: Default | IgnoreSymbolStoreSequencePoints = 0x03
debuggingModes = 0x03;
}
else if (_options.DebugMode?.Equals("impl", StringComparison.OrdinalIgnoreCase) == true)
{
// /DEBUG=IMPL: Default | DisableOptimizations | EnableEditAndContinue = 0x103
debuggingModes = 0x103;
}
else
{
// /DEBUG (default): Default | DisableOptimizations = 0x101
debuggingModes = 0x101;
}

// 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"));

// Create reference to nested type DebuggingModes
var debuggingModesType = _entityRegistry.GetOrCreateTypeReference(
debuggableAttrType,
new TypeName(null, "DebuggingModes"));

// Create constructor signature: .ctor(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);

// Create custom attribute blob: prolog (0x0001) + int32 value + named args count (0x0000)
BlobBuilder attrValue = new();
attrValue.WriteUInt16(0x0001); // Prolog
attrValue.WriteInt32(debuggingModes); // DebuggingModes value
attrValue.WriteUInt16(0x0000); // No named arguments

// Create and attach the custom attribute
var customAttr = _entityRegistry.CreateCustomAttribute(ctor, attrValue);
customAttr.Owner = _entityRegistry.Assembly;
}

private void ApplyKeyFile(string keyFilePath)
{
if (_entityRegistry.Assembly is null)
{
return;
}

try
{
byte[] keyBytes = File.ReadAllBytes(keyFilePath);
BlobBuilder blob = new();
blob.WriteBytes(keyBytes);
_entityRegistry.Assembly.PublicKeyOrToken = blob;
_entityRegistry.Assembly.Flags |= AssemblyFlags.PublicKey;
}
catch (Exception ex)
{
// Create a location pointing to the first document
var firstDoc = _documents.Values.First();
var location = new Location(new SourceSpan(0, 0), firstDoc);
_diagnostics.Add(new Diagnostic(DiagnosticIds.KeyFileError, DiagnosticSeverity.Error, $"Failed to read key file '{keyFilePath}': {ex.Message}", location));
}
}

GrammarResult ICILVisitor<GrammarResult>.VisitAssemblyDecl(CILParser.AssemblyDeclContext context) => VisitAssemblyDecl(context);
public GrammarResult VisitAssemblyDecl(CILParser.AssemblyDeclContext context)
{
Expand Down Expand Up @@ -1644,7 +1811,10 @@ public GrammarResult VisitDecl(CILParser.DeclContext context)
if (implementation is null)
{
offset = (uint)_manifestResources.Count;
_manifestResources.WriteBytes(_resourceLocator(alias));
byte[] resourceData = _resourceLocator(alias);
// ECMA-335: Each resource is prefixed with a 4-byte length
_manifestResources.WriteInt32(resourceData.Length);
_manifestResources.WriteBytes(resourceData);
}
var res = _entityRegistry.CreateManifestResource(name, offset);
res.Attributes = flags;
Expand Down
Loading