diff --git a/Directory.Build.props b/Directory.Build.props index cd1dada47..fd7226b04 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -7,7 +7,7 @@ https://github.com/Washi1337/AsmResolver git 10 - 5.4.0 + 5.5.0 true diff --git a/README.md b/README.md index 2b260318e..f9884c27e 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ AsmResolver has a lot of features. Below is a non-exhaustive list of the highlig - [x] Easy metadata importing and cloning. - [x] Managed resource file serializers and deserializers. - [x] Support for AppHost / SingleFileHost bundles. + - [x] Support for ReadyToRun binaries. - [x] Read PDB symbols. - [x] Fully managed cross-platform API (No DIA or similar required). - [x] .NET Standard 2.0 compatible. diff --git a/appveyor.yml b/appveyor.yml index a550e2f6d..270821ada 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -4,7 +4,7 @@ - master image: Visual Studio 2022 - version: 5.4.0-master-build.{build} + version: 5.5.0-master-build.{build} configuration: Release skip_commits: @@ -33,7 +33,7 @@ - development image: Visual Studio 2022 - version: 5.4.0-dev-build.{build} + version: 5.5.0-dev-build.{build} configuration: Release skip_commits: diff --git a/docs/guides/dotnet/advanced-pe-image-building.md b/docs/guides/dotnet/advanced-pe-image-building.md index 72b41a5c9..98c85ed1c 100644 --- a/docs/guides/dotnet/advanced-pe-image-building.md +++ b/docs/guides/dotnet/advanced-pe-image-building.md @@ -7,73 +7,77 @@ The easiest way to write a .NET module to the disk is by using the module.Write(@"C:\Path\To\Output\Binary.exe"); ``` -This method is essentially a shortcut for invoking the -`ManagedPEImageBuilder` and `ManagedPEFileBuilder` classes, and will -completely reconstruct the PE image, serialize it into a PE file and -write the PE file to the disk. +Behind the scenes, this creates and invokes a +`ManagedPEImageBuilder` and a `ManagedPEFileBuilder` with their default +settings, and will completely reconstruct the PE image, serialize it into +a PE file and write the PE file to the disk. -While this is easy, and would probably work for most .NET module -processing, it does not provide much flexibility. To get more control -over the construction of the new PE image, it is therefore not -recommended to use a different overload of the `Write` method that takes -instances of `IPEImageBuilder` instead: +To get more control over the construction of the new PE image, we can use +and configure our own instance of an `IPEImageBuilder` instead: ``` csharp var imageBuilder = new ManagedPEImageBuilder(); -/* Configuration of imageBuilder here... */ - -module.Write(@"C:\Path\To\Output\Binary.exe", imageBuilder); +/* ... Configuration of imageBuilder here... */ ``` -Alternatively, it is possible to call `ModuleDefinition::ToPEImage` to -turn the module into a `PEImage` first, that can then later be -post-processed and transformed into a `PEFile` to write it to the disk: +After configuring, the builder can then be passed onto the `ModuleDefinition::Write` +method as a secondary parameter: ``` csharp -var imageBuilder = new ManagedPEImageBuilder(); +module.Write(@"C:\Path\To\Output\Binary.exe", imageBuilder); +``` -/* Configuration of imageBuilder here... */ +It is also possible to call `ModuleDefinition::ToPEImage` to turn +the module into a `PEImage` first. This image can then be post-processed +and later transformed into a `PEFile` to write it to the disk: -// Construct image. +``` csharp +// Turn module into a new PE image. var image = module.ToPEImage(imageBuilder); -// Write image to the disk. +/* ... Post processing of the PE image here ... */ + +// Construct a new PE file. var fileBuilder = new ManagedPEFileBuilder(); var file = fileBuilder.CreateFile(image); + +/* ... Post processing of the PE file here ... */ + +// Write PE file to disk. file.Write(@"C:\Path\To\Output\Binary.exe"); ``` -To get even more control, it is possible to call the `CreateImage` -method from the image builder directly. This allows for inspecting all -build artifacts, as well as post-processing of the constructed PE image -before it is written to the disk. +To get access to additional build artifacts, such as new metadata tokens +and builder diagnostics, it is possible to call the `CreateImage` +method from the image builder directly, and inspect the resulting +`PEImageBuildResult` object: ``` csharp -var imageBuilder = new ManagedPEImageBuilder(); - -/* Configuration of imageBuilder here... */ - // Construct image. var result = imageBuilder.CreateImage(module); -/* Inspect build result ... */ +/* ... Inspect build result here ... */ // Obtain constructed PE image. var image = result.ConstructedImage; -/* Post processing of image happens here... */ +/* ... Post processing of the PE image here ... */ -// Write image to the disk. +// Construct a new PE file. var fileBuilder = new ManagedPEFileBuilder(); var file = fileBuilder.CreateFile(image); + +/* ... Post processing of the PE file here ... */ + +// Write PE file to disk. file.Write(@"C:\Path\To\Output\Binary.exe"); ``` This article explores various features about the `ManagedPEImageBuilder` class. -## Token mappings +## Token Mappings Upon constructing a new PE image for a module, members defined in the module might be re-ordered. This can make post-processing of the PE @@ -98,7 +102,7 @@ var mainMethodRow = result.ConstructedImage.DotNetDirectory.Metadata .GetByRid(newToken.Rid); ``` -## Preserving raw metadata structure +## Preserving Raw Metadata Structure Some .NET modules are carefully crafted and rely on the raw structure of all metadata streams. These kinds of modules often rely on one of the @@ -135,7 +139,7 @@ blob data and all metadata tokens to type references: ``` csharp var factory = new DotNetDirectoryFactory(); -factory.MetadataBuilderFlags = MetadataBuilderFlags.PreserveBlobIndices +factory.MetadataBuilderFlags = MetadataBuilderFlags.PreserveBlobIndices | MetadataBuilderFlags.PreserveTypeReferenceIndices; imageBuilder.DotNetDirectoryFactory = factory; ``` @@ -159,7 +163,7 @@ imageBuilder.DotNetDirectoryFactory = factory; > `#~` to `#-`, and the file size might increase. -## String folding in #Strings stream +## String Folding in #Strings Stream Named metadata members (such as types, methods and fields) are assigned a name by referencing a string in the `#Strings` stream by its starting @@ -194,7 +198,27 @@ factory.MetadataBuilderFlags |= MetadataBuilderFlags.NoStringsStreamOptimization > However, it will still try to reuse these original strings as much as > possible. -## Preserving maximum stack depth +## Deduplication of Embedded Resource Data + +By default, when adding two embedded resources to a file with identical +contents, AsmResolver will not add the second copy of the data to the +output file and instead reuse the first blob. This can drastically +reduce the size of the final output file, especially for larger applications +with many (small) identical resource files (e.g., many Windows Forms +Applications). + +While supported by most implementations of the .NET runtime, some assembly +post-processors (e.g., obfuscators) may not work well with this or depend +on individual resource items to be present. + +To stop AsmResolver from performing this optimization, specify the +`NoResourceDataDeduplication` metadata builder flag: + +``` csharp +factory.MetadataBuilderFlags |= MetadataBuilderFlags.NoResourceDataDeduplication; +``` + +## Preserving Maximum Stack Depth CIL method bodies work with a stack, and the stack has a pre-defined size. This pre-defined size is defined by the `MaxStack` property of the @@ -205,13 +229,16 @@ disk. However, this is not always desirable. To override this behaviour, set `ComputeMaxStackOnBuild` to `false` on all method bodies to exclude in the maximum stack depth calculation. -Alternatively, if you want to force the maximum stack depths should be -either preserved or recalculated, it is possible to provide a custom -implemenmtation of the `IMethodBodySerializer`, or configure the -`CilMethodBodySerializer`. +``` csharp +MethodDefinition method = ... +method.CilMethodBody.ComputeMaxStackOnBuild = false; +``` -Below an example on how to preserve maximum stack depths for all methods -in the assembly: +Alternatively, if you want to force the maximum stack depths should be +either preserved or recalculated for **all** methods defined in the target +assembly, it is possible to provide a custom implementation of the +`IMethodBodySerializer`, or set up a new `CilMethodBodySerializer` with +the `ComputeMaxStackOnBuildOverride` property set to any overriding value: ``` csharp DotNetDirectoryFactory factory = ...; @@ -225,7 +252,7 @@ factory.MethodBodySerializer = new CilMethodBodySerializer > Disabling max stack computation may have unexpected side-effects (such > as rendering certain CIL method bodies invalid). -## Strong name signing +## Strong Name Signing Assemblies can be signed with a strong-name signature. Open a strong name private key from a file: @@ -295,8 +322,8 @@ imageBuilder.ErrorListener = EmptyErrorListener.Instance; > [!NOTE] > Setting an instance of `IErrorListener` in the image builder will only > affect the building process. If the input module is initialized from a -> file containing invalid metadata, you may still experience reader -> errors, even if an `EmptyErrorListener` is specified. See +> file containing invalid metadata, **you may still experience reader +> errors, even if an `EmptyErrorListener` is specified to the builder**. See > [Advanced Module Reading](advanced-module-reading.md) for > handling reader diagnostics. diff --git a/docs/guides/dotnet/cloning.md b/docs/guides/dotnet/cloning.md index 37b54fd21..40b6fc79a 100644 --- a/docs/guides/dotnet/cloning.md +++ b/docs/guides/dotnet/cloning.md @@ -82,8 +82,8 @@ look up the members by metadata token. ``` csharp var sourceModule = ModuleDefinition.FromFile(typeof(Rectangle).Assembly.Location); -var rectangleType = (TypeDefinition) sourceModule.LookupMember(typeof(Rectangle).MetadataToken); -var vectorType = (TypeDefinition) sourceModule.LookupMember(typeof(Vector2).MetadataToken); +var rectangleType = sourceModule.LookupMember(typeof(Rectangle).MetadataToken); +var vectorType = sourceModule.LookupMember(typeof(Vector2).MetadataToken); ``` We can then use `MemberCloner.Include` to include the types in the @@ -103,7 +103,7 @@ cloner.Include(rectangleType); cloner.Include(vectorType); ``` -`Include` returns the same `MemberCloner` instance, allowing for more fluent +`Include` returns the same `MemberCloner` instance, allowing for more fluent syntax: ``` csharp @@ -305,8 +305,8 @@ foreach (var clonedType in clonedTypes) ``` Injecting the cloned top level types is a very common use-case for the cloner. -AsmResolver defines the `InjectTypeClonerListener` class that implements a -cloner listener that injects all top-level types automatically into +AsmResolver defines the `InjectTypeClonerListener` class that implements a +cloner listener that injects all top-level types automatically into the destination module. In such a case, the code can be reduced to the following: ``` csharp @@ -337,4 +337,4 @@ new MemberCloner(destinationModule) The `AssignTokensClonerListener` uses the target module's `TokenAllocator`. See [Metadata Token Allocation](token-allocation.md) for more information on how this -class operates. \ No newline at end of file +class operates. diff --git a/docs/guides/dotnet/managed-method-bodies.md b/docs/guides/dotnet/managed-method-bodies.md index 6cda54423..69f658699 100644 --- a/docs/guides/dotnet/managed-method-bodies.md +++ b/docs/guides/dotnet/managed-method-bodies.md @@ -1,5 +1,10 @@ # CIL Method Bodies +Most methods defined in a .NET module are implemented using the Common +Intermediate Language (CIL), and AsmResolver provides high-level +disassembler and assembler capabilities for creating, reading and writing +method bodies written in this language. + The relevant models in this document can be found in the following namespaces: @@ -33,7 +38,9 @@ blocks: - `ExceptionHandlers`: A collection of regions protected by an exception handler. -## Basic structure of CIL instructions +## Instructions + +### Basic structure of CIL instructions Instructions that are assembled into the method body are automatically disassembled and put in a mutable collection of `CilInstruction`, @@ -53,27 +60,27 @@ The `CilInstruction` class defines three basic properties: By default, depending on the value of `OpCode.OperandType`, `Operand` contains (and always should contain) one of the following: -|OpCode.OperandType |Type of Operand | -|------------------------------------|----------------------------------------| -|`CilOperandType.InlineNone` |N/A (is always `null`) | -|`CilOperandType.ShortInlineI` |`sbyte` | -|`CilOperandType.InlineI` |`int` | -|`CilOperandType.InlineI8` |`long` | -|`CilOperandType.ShortInlineR` |`float` | -|`CilOperandType.InlineR` |`double` | -|`CilOperandType.InlineString` |`string` or `MetadataToken` | -|`CilOperandType.InlineBrTarget` |`ICilLabel` or `int` | -|`CilOperandType.ShortInlineBrTarget`|`ICilLabel` or `sbyte` | -|`CilOperandType.InlineSwitch` |`IList` | -|`CilOperandType.ShortInlineVar` |`CilLocalVariable` or `byte` | -|`CilOperandType.InlineVar` |`CilLocalVariable` or `ushort` | -|`CilOperandType.ShortInlineArgument`|`Parameter` or `byte` | -|`CilOperandType.InlineArgument` |`Parameter` or `ushort` | -|`CilOperandType.InlineField` |`IFieldDescriptor` or `MetadataToken` | -|`CilOperandType.InlineMethod` |`IMethodDescriptor` or `MetadataToken` | -|`CilOperandType.InlineSig` |`StandAloneSignature` or `MetadataToken`| -|`CilOperandType.InlineTok` |`IMetadataMember` or `MetadataToken` | -|`CilOperandType.InlineType` |`ITypeDefOrRef` or `MetadataToken` | +|OpCode.OperandType |Type of Operand | +|------------------------------------|----------------------------------------| +|`CilOperandType.InlineNone` |N/A (is always `null`) | +|`CilOperandType.ShortInlineI` |`sbyte` | +|`CilOperandType.InlineI` |`int` | +|`CilOperandType.InlineI8` |`long` | +|`CilOperandType.ShortInlineR` |`float` | +|`CilOperandType.InlineR` |`double` | +|`CilOperandType.InlineString` |`string` or `MetadataToken` | +|`CilOperandType.InlineBrTarget` |`ICilLabel` or `int` | +|`CilOperandType.ShortInlineBrTarget`|`ICilLabel` or `sbyte` | +|`CilOperandType.InlineSwitch` |`IList` | +|`CilOperandType.ShortInlineVar` |`CilLocalVariable` or `byte` | +|`CilOperandType.InlineVar` |`CilLocalVariable` or `ushort` | +|`CilOperandType.ShortInlineArgument`|`Parameter` or `byte` | +|`CilOperandType.InlineArgument` |`Parameter` or `ushort` | +|`CilOperandType.InlineField` |`IFieldDescriptor` or `MetadataToken` | +|`CilOperandType.InlineMethod` |`IMethodDescriptor` or `MetadataToken` | +|`CilOperandType.InlineSig` |`StandAloneSignature` or `MetadataToken`| +|`CilOperandType.InlineTok` |`IMetadataMember` or `MetadataToken` | +|`CilOperandType.InlineType` |`ITypeDefOrRef` or `MetadataToken` | > [!WARNING] @@ -103,7 +110,7 @@ instructions.Add(CilOpCodes.Ldstr, "Hello, World!"); instructions.Add(CilOpCodes.Ret); ``` -## Pushing 32-bit integer constants onto the stack +### Pushing 32-bit integer constants onto the stack In CIL, pushing integer constants onto the stack is done using one of the `ldc.i4` instruction variants. @@ -124,7 +131,7 @@ If we want to get the pushed value, we can use the the `ldc.i4` variants, including all the macro opcodes that do not explicitly define an operand such as `ldc.i4.1`. -## Branching Instructions +### Branching Instructions Branch instructions are instructions that (might) transfer control to another part of the method body. To reference the instruction to jump to @@ -160,7 +167,7 @@ The `switch` operation uses a `IList` instead. > code stream. This can be disabled by setting `VerifyLabelsOnBuild` to > `false`. -## Finding instructions by offset +### Finding Instructions by Offset Instructions stored in a method body are indexed not by offset, but by order of occurrence. If it is required to find an instruction by offset, @@ -187,7 +194,7 @@ int index = body.Instructions.GetIndexByOffset(0x0012); instruction1 = body.Instructions[index]; ``` -## Referencing members +### Referencing Members As specified by the table above, operations such as a `call` require a member as operand. @@ -211,7 +218,7 @@ More information on the capabilities and limitations of the `ReferenceImporter` can be found in [Reference Importing](importing.md). -## Expanding and optimising macros +### Expanding and Optimizing Macros CIL defines a couple of macro operations that do the same as their full counterpart, but require less space to be encoded. For example, the @@ -240,7 +247,7 @@ body.Instructions.ExpandMacros(); // instruction is now expanded to "ldc.i4 1". ``` -## Pretty printing CIL instructions +### Pretty printing CIL Instructions Instructions can be formatted using e.g. an instance of the `CilInstructionFormatter`: @@ -251,7 +258,7 @@ foreach (CilInstruction instruction in body.Instructions) Console.WriteLine(formatter.FormatInstruction(instruction)); ``` -## Patching CIL instructions +### Patching CIL Instructions Instructions can be added or removed using the `Add`, `Insert`, `Remove` and `RemoveAt` methods: @@ -297,7 +304,32 @@ helper function: body.Instructions[i].ReplaceWithNop(); ``` -## Exception handlers +## Local Variables + +Most methods will define local variables to temporarily store state +throughout the execution of the method's code. Local variables are +exposed through the `CilMethodBody.LocalVariables` property, and are +represented using the `CilLocalVariable` class. + +```csharp +CilMethodBody body = ...; +foreach (var local in body.LocalVariables) + Console.WriteLine($"{local.Index}: {local.VariableType}"); +``` + +New variables can be created by calling its constructor: + +```csharp +ModuleDefinition module = ...; +var local = new CilLocalVariable(module.CorLibTypeFactory.Int32); +``` + +New variables can be added to the method: +```csharp +body.LocalVariables.Add(local); +``` + +## Exception Handlers Exception handlers are regions in the method body that are protected from exceptions. In AsmResolver, they are represented by the @@ -321,6 +353,45 @@ from exceptions. In AsmResolver, they are represented by the Depending on the value of `HandlerType`, either `FilterStart` or `ExceptionType`, or neither has a value. +```csharp +CilMethodBody body = ...; +foreach (var handler in body.ExceptionHandlers) +{ + Console.WriteLine($"HandlerType: {handler.HandlerType}"); + Console.WriteLine($"TryStart: {handler.TryStart}"); + Console.WriteLine($"TryEnd: {handler.TryEnd}"); + Console.WriteLine($"HandlerStart: {handler.HandlerStart}"); + Console.WriteLine($"HandlerEnd: {handler.HandlerEnd}"); + + if (handler.HandlerType == CilExceptionHandlerType.Exception) + { + // handler is a try-catch with an exception type. + Console.WriteLine($"ExceptionType: {handler.ExceptionType}"); + } + else if if (handler.HandlerType == CilExceptionHandlerType.Filter) + { + // handler is a try-catch with a custom filter: + Console.WriteLine($"FilterStart: {handler.FilterStart}"); + } +} +``` + +New handlers can be added to the method body: + +```csharp +body.ExceptionHandlers.Add(new CilExceptionHandler +{ + HandlerType = CilExceptionHandlerType.Exception, + TryStart = body.Instructions[0].CreateLabel(), + TryEnd = body.Instructions[4].CreateLabel(), + HandlerStart = body.Instructions[4].CreateLabel(), + HandlerEnd = body.Instructions[8].CreateLabel(), + ExceptionType = module.CorLibTypeFactory.CorLibScope + .CreateTypeReference("System", "Exception") + .ImprotWith(module.DefaultImporter) +}); +``` + > [!NOTE] > Similar to branch instructions, when an exception handler contains a > `null` label or a label that references an instruction that is not @@ -328,7 +399,8 @@ Depending on the value of `HandlerType`, either `FilterStart` or > serializing the code stream. This can be disabled by setting > `VerifyLabelsOnBuild` to `false`. -## Maximum stack depth + +## Maximum Stack Depth CIL method bodies work with a stack, and the stack has a pre-defined size. This pre-defined size is defined by the `MaxStack` property. diff --git a/docs/guides/dotnet/member-tree.md b/docs/guides/dotnet/member-tree.md index ec0e7a972..6ebeeba08 100644 --- a/docs/guides/dotnet/member-tree.md +++ b/docs/guides/dotnet/member-tree.md @@ -1,6 +1,6 @@ # The Member Tree -## Assemblies and modules +## Assemblies and Modules The root of every .NET assembly is represented by the `AssemblyDefinition` class. This class exposes basic information such as @@ -20,14 +20,32 @@ Most .NET assemblies only have one module. This main module is also known as the manifest module, and can be accessed directly through the `AssemblyDefinition.ManifestModule` property. -## Obtaining types in a module +Executable modules can have an entry point, which can be obtained using +the `ManagedEntryPoint` property: -Types are represented by the `TypeDefinition` class. To get the types -defined in a module, use the `ModuleDefinition.TopLevelTypes` property. -A top level types is any non-nested type. Nested types are exposed -through the `TypeDefinition.NestedTypes`. Alternatively, to get all -types, including nested types, it is possible to call the -`ModuleDefinition.GetAllTypes` method instead. +```csharp +var entryPoint = module.ManagedEntryPoint; +``` + +Often, modules also contain a static module constructor, which is executed +the moment the module is loaded into memory by the CLR (and thus before the +entry point is executed). AsmResolver provides helper methods to quickly +locate such a constructor: + +```csharp +var cctor = module.GetModuleConstructor(); +``` + +## Types + +Types form logical units or data type defined in a module, and are +represented by the `TypeDefinition` class. + +### Inspecting Types in a Module + +Types defined in a module are exposed through the `ModuleDefinition.TopLevelTypes` +property. A top level types is any non-nested type. Nested types +are exposed through the `TypeDefinition.NestedTypes`. Below is an example program that iterates through all types recursively and prints them: @@ -55,23 +73,164 @@ private static void DumpTypes(IEnumerable types, int indentation } ``` -## Obtaining methods and fields +Alternatively, you can get all the types including nested types using the +`ModuleDefinition.GetAllTypes()` method: + +``` csharp +var module = ModuleDefinition.FromFile(...); +foreach (var type in module.GetAllTypes()) + Console.WriteLine(type.FullName); +``` + +### Creating New Types + +New types can be created by calling one of its constructors: + +```csharp +ModuleDefinition module = ... +var newType = new TypeDefinition( + "Namespace", + "Name", + TypeAttributes.Public, + module.CorLibTypeFactory.Object); +``` + +> [!WARNING] +> For classes, ensure that you specify a non-null base type or the CLR will +> not load the binary properly. + +For structures, make sure that your type inherits from `System.ValueType`: + +```csharp +ModuleDefinition module = ... +var newType = new TypeDefinition( + "Namespace", + "Name", + TypeAttributes.Public, + module.CorLibTypeFactory.CorLibScope + .CreateTypeReference("System", "ValueType") + .ImportWith(module.DefaultImporter)); +``` + +Interfaces in a .NET module do not have a base type, and as such, creating +new interfaces will not require specifying one: + +```csharp +ModuleDefinition module = ... +var newType = new TypeDefinition( + "Namespace", + "IName", + TypeAttributes.Public | TypeAttributes.Interface); +``` + +Once a type has been constructed, it can be added to either a `ModuleDefinition` +as a top-level type, or to another `TypeDefinition` as a nested type: + +```csharp +ModuleDefinition module = ...; +module.TopLevelTypes.Add(newType); +``` +```csharp +TypeDefinition type = ...; +type.NestedTypes.Add(newType); +``` + +## Fields + +Fields comprise all the data a type stores, and form the internal structure +of a class or value type. They are represented using the `FieldDefinition` +class. + +### Inspecting Fields in a Type + +The `TypeDefinition` class exposes a collection of fields that the type +defines: + +``` csharp +foreach (var field in type.Fields) + Console.WriteLine($"{field.Name} : {field.MetadataToken}"); +``` + +Fields have a signature which contains the field's type. + +``` csharp +FieldDefinition field = ... +Console.WriteLine($"Field type: {field.Signature.FieldType}"); +``` + +Fields can also have constants attached, exposed via the `Constant` +property: + +``` csharp +FieldDefinition field = ... +if (field.Constant is { } constant) + Console.WriteLine($"Field Constant Data: {BitConverter.ToString(constant.Value>Data)}"); +``` + +For fields that have an RVA attached (such as fields with an initial +value set), you can access the `FieldRva` property containing the +`ISegment` value with the raw data. This is in particular useful for +inspecting fields containing the initial raw data of an array. + +``` csharp +FieldDefinition field = ... +if (field.FieldRva is { } rva) + Console.WriteLine($"Field Initial Data: {BitConverter.ToString(rva.WriteIntoArray())}"); +``` + +Refer to [Reading and Writing File Segments](../core/segments.md) for more +information on how to use `ISegment`s. + + +### Creating New Fields + +Creating and adding new fields can be done by using one of its constructors. + +``` csharp +ModuleDefinition module = ...; +var field = new FieldDefinition( + "MyField", + FieldAttributes.Public, + module.CorLibTypeFactory.Int32);" +``` + +Fields can be added to a type: + +```csharp +TypeDefinition type = ...; +type.Fields.Add(field); +``` + +Most properties in `FieldDefinition` are mutable, allowing you to configure +however you want your new field to be. + + +## Methods + +Methods are functions defined in a type, and provide a way to define +operations that can be applied to a type. They are represented using +the `MethodDefinition` class. -The `TypeDefinition` class exposes collections of methods and fields -that the type defines: +### Inspecting Methods in a Type + +The `TypeDefinition` class exposes a collection of methods that the type +defines: ``` csharp foreach (var method in type.Methods) Console.WriteLine($"{method.Name} : {method.MetadataToken}"); ``` +AsmResolver provides helper methods to find constructors in a type: + ``` csharp -foreach (var field in type.Fields) - Console.WriteLine($"{field.Name} : {field.MetadataToken}"); +var parameterlessCtor = type.GetConstructor(); +var parameterizedCtor = type.GetConstructor(module.CorLibFactory.Int32); +var cctor = type.GetStaticConstructor(); ``` Methods and fields have a `Signature` property, that contain the return -and parameter types, or the field type respectively. +and parameter types: ``` csharp MethodDefinition method = ... @@ -79,11 +238,6 @@ Console.WriteLine($"Return type: {method.Signature.ReturnType}"); Console.WriteLine($"Parameter types: {string.Join(", ", method.Signature.ParameterTypes)}"); ``` -``` csharp -FieldDefinition field = ... -Console.WriteLine($"Field type: {field.Signature.FieldType}"); -``` - However, for reading parameters from a method definition, it is preferred to use the `Parameters` property instead of the `ParameterTypes` property stored in the signature. This is because the @@ -96,27 +250,129 @@ foreach (var parameter in method.Parameters) Console.WriteLine($"{parameter.Name} : {parameter.ParameterType}"); ``` -## Obtaining properties and events +Methods may or may not be assigned a method body. This can be verified +using `HasMethodBody`: -Obtaining properties and events is similar to obtaining methods and -fields; `TypeDefinition` exposes them in a list as well: +```csharp +if (method.HasMethodBody) +{ + // ... +} +``` + + +Typically, a method body implemented using the Common Intermediate +Language (CIL), the bytecode used by .NET. This method body can be +inspected using the `CilMethodBody` property: + +```csharp +if (method.CilMethodBody is { } body) +{ + foreach (var instruction in body.Instructions) + Console.WriteLine(instruction); +} +``` + +For more information on CIL method bodies, refer to +[CIL Method Bodies](managed-method-bodies.md). + + +### Creating New Methods + +Creating new methods can be done either through one of its constructors, +taking a name, attributes, and a method signature. + +For static methods, use the `MethodSignature.CreateStatic` to create +the signature: ``` csharp -foreach (var @event in type.Events) - Console.WriteLine($"{@event.Name} : {@event.MetadataToken}"); +ModuleDefinition module = ...; +var method = new MethodDefinition( + "MyMethod", + MethodAttributes.Public | MethodAttributes.Static, + MethodSignature.CreateStatic( + module.CorLibTypeFactory.Void, // Return type + module.CorLibTypeFactory.Int32, // Parameter 1 + module.CorLibTypeFactory.String // Parameter 2 + )); ``` +Similarly, for instance methods, use the `MethodSignature.CreateInstance` +to create the signature: + +``` csharp +ModuleDefinition module = ...; +var method = new MethodDefinition( + "MyMethod", + MethodAttributes.Public, + MethodSignature.CreateInstance( + module.CorLibTypeFactory.Void, // Return type + module.CorLibTypeFactory.Int32, // Parameter 1 + module.CorLibTypeFactory.String // Parameter 2 + )); +``` + +AsmResolver provides helper methods to create special methods such as +constructors that automatically set the right attributes and initialize +it with a default method body. + +```csharp +ModuleDefinition module = ...; +var ctor = MethodDefinition.CreateConstructor(module.CorLibTypeFactory.Int32); +var cctor = MethodDefinition.CreateStaticConstructor(); +``` + +After creating methods, they can be added to a type: + +```csharp +TypeDefinition type = ...; +type.Methods.Add(method); +``` + +Most properties in `MethodDefinition` are mutable, allowing you to configure +however you want your new method to be. + + +## Properties and Events + +Properties and Events add special semantics to groups of methods, and are +represented using the `PropertyDefinition` and `EventDefinition` classes +respectively. + +Obtaining properties and events is similar to obtaining methods and +fields; `TypeDefinition` exposes them in a list as well: + ``` csharp foreach (var property in type.Properties) Console.WriteLine($"{property.Name} : {property.MetadataToken}"); ``` +``` csharp +foreach (var @event in type.Events) + Console.WriteLine($"{@event.Name} : {@event.MetadataToken}"); +``` + Properties and events have methods associated to them. These are accessible through the `Semantics` property: ``` csharp foreach (MethodSemantics semantic in property.Semantics) -{ Console.WriteLine($"{semantic.Attributes} {semantic.Method.Name} : {semantic.MetadataToken}"); -} +``` + +For properties, there are helpers defined to quickly access the getter +or setter methods of the property: + +```csharp +MethodDefinition getter = property.GetMethod; +MethodDefinition setter = property.SetMethod; +``` + +Similarly, for events, there exists helpers for obtaining the add, +remove, and fire methods: + +```csharp +MethodDefinition adder = property.AddMethod; +MethodDefinition remover = property.RemoveMethod; +MethodDefinition fire = property.FireMethod; ``` diff --git a/docs/guides/peimage/dotnet.md b/docs/guides/peimage/dotnet.md index f4106ef61..6ec04d65e 100644 --- a/docs/guides/peimage/dotnet.md +++ b/docs/guides/peimage/dotnet.md @@ -154,7 +154,7 @@ TablesStream tablesStream = metadata.GetStream(); ``` Metadata tables are represented by the `IMetadataTable` interface. -Individal tables can be accessed using the `GetTable` method: +Individal tables can be accessed using the `GetTable` method: ```csharp IMetadataTable typeDefTable = tablesStream.GetTable(TableIndex.TypeDef); @@ -162,13 +162,13 @@ IMetadataTable typeDefTable = tablesStream.GetTable(TableIndex.TypeDef); Tables can also be obtained by their row type: ```csharp -MetadataTable typeDefTable = tablesStream.GetTable(); +MetadataTable typeDefTable = tablesStream.GetTable(); ``` -The latter option is the preferred option, as it allows for a more type-safe +The latter option is the preferred option, as it allows for a more type-safe interaction with the table as well and avoids boxing of each row in the table. Each metadata table is associated with its own row structure. Below a table of -all row definitions: +all row definitions: | Table index | Name (as per specification) | AsmResolver row structure name | @@ -220,8 +220,8 @@ all row definitions: | 44 | GenericParamConstraint | `GenericParamConstraintRow` } -Metadata tables are similar to normal`ICollection\` instances. They provide -enumerators, indexers and methods to add or remove rows from the table. +Metadata tables are similar to normal`ICollection\` instances. They provide +enumerators, indexers and methods to add or remove rows from the table. ``` csharp Console.WriteLine($"Number of types: {typeDefTable.Count}"); @@ -235,13 +235,13 @@ foreach (var typeRow in typeDefTable) } ``` -Members can also be accessed by their RID using the `GetByRid` or `TryGetByRid` helper functions: +Members can also be accessed by their RID using the `GetByRid` or `TryGetByRid` helper functions: ```csharp TypeDefinitionRow thirdTypeRow = typeDefTable.GetByRid(3); ``` -Using the other metadata streams, it is possible to resolve all columns. +Using the other metadata streams, it is possible to resolve all columns. Below an example that prints the name and namespace of each type row in the type definition table in a file. @@ -260,7 +260,7 @@ foreach (var typeRow in typeDefTable) // Resolve name and namespace columns using the #Strings stream. string ns = stringsStream.GetStringByIndex(typeRow.Namespace); string name = stringsStream.GetStringByIndex(typeRow.Name); - + // Print name and namespace: Console.WriteLine(string.IsNullOrEmpty(ns) ? name : $"{ns}.{name}"); } @@ -268,20 +268,20 @@ foreach (var typeRow in typeDefTable) ## Method and FieldRVA Every row structure defined in AsmResolver respects the specification described by the CLR itself. However, there are two exceptions to this rule, and those are -the **Method** and **FieldRVA** rows. According to the specification, both of +the **Method** and **FieldRVA** rows. According to the specification, both of these rows have an **RVA** column that references a segment in the original PE -file. Since this second layer of abstraction attempts to abstract away any file -offset or virtual address, these columns are replaced with properties called +file. Since this second layer of abstraction attempts to abstract away any file +offset or virtual address, these columns are replaced with properties called `Body` and `Data` respectively, both of type`ISegmentReference`instead. -`ISegmentReference`exposes a method`CreateReader()`, which automatically -resolves the RVA that was stored in the row, and creates a new input stream -that can be used to parse e.g. method bodies or field data. +`ISegmentReference`exposes a method`CreateReader()`, which automatically +resolves the RVA that was stored in the row, and creates a new input stream +that can be used to parse e.g. method bodies or field data. ### Reading method bodies: -Reading a managed CIL method body can be done using -`CilRawMethodBody.FromReader` method: +Reading a managed CIL method body can be done using +`CilRawMethodBody.FromReader` method: ```csharp var methodTable = tablesStream.GetTable(); @@ -289,20 +289,20 @@ var firstMethod = methodTable[0]; var methodBody = CilRawMethodBody.FromReader(firstMethod.Body.CreateReader()); ``` -It is important to note that the user is not bound to use `CilRawMethodBody`. -In the case that the `Native` (`0x0001`) flag is set in -`MethodDefinitionRow.ImplAttributes`, the implementation of the method body is +It is important to note that the user is not bound to use `CilRawMethodBody`. +In the case that the `Native` (`0x0001`) flag is set in +`MethodDefinitionRow.ImplAttributes`, the implementation of the method body is not written in CIL, but using native code that uses an instruction set dependent -on the platform that this application is targeting. Since the bounds of such a -method body is not always well-defined, AsmResolver does not do any parsing on -its own. However, using the`CreateReader()`method, it is still possible to -decode instructions from this method body, using a custom instruction decoder. +on the platform that this application is targeting. Since the bounds of such a +method body is not always well-defined, AsmResolver does not do any parsing on +its own. However, using the`CreateReader()`method, it is still possible to +decode instructions from this method body, using a custom instruction decoder. ### Reading field data -Reading field data can be done in a similar fashion as reading method bodies. -Again use the `CreateReader()` method to gain access to the raw data of the -initial value of the field referenced by a **FieldRVA** row. +Reading field data can be done in a similar fashion as reading method bodies. +Again use the `CreateReader()` method to gain access to the raw data of the +initial value of the field referenced by a **FieldRVA** row. ```csharp var fieldRvaTable = tablesStream.GetTable(); @@ -312,22 +312,22 @@ var reader = firstRva.Data.CreateReader(); ### Creating new segment references Creating new segment references not present in the current PE image yet can be -done using the `ISegment.ToReference()` extension method: +done using the `ISegment.ToReference()` extension method: ```csharp var myData = new DataSegment(new byte[] {1, 2, 3, 4}); var fieldRva = new FieldRvaRow(myData.ToReference(), 0); ``` -## TypeReference Hash (TRH) +## TypeReference Hash (TRH) Similar to the [Import Hash](imports.md#import-hash), the TypeReference Hash (TRH) can be used -to help identify malware families written in a .NET language. However, unlike -the Import Hash, the TRH is based on the names of all imported type references +to help identify malware families written in a .NET language. However, unlike +the Import Hash, the TRH is based on the names of all imported type references instead of the symbols specified in the imports directory of the PE. This is a -more accurate representation for .NET images, as virtually every .NET image -only uses one native symbol (either `mscoree.dll!_CorExeMain` or -`mscoree.dll!_CorDllMain` ). AsmResolver includes a built-in implementation +more accurate representation for .NET images, as virtually every .NET image +only uses one native symbol (either `mscoree.dll!_CorExeMain` or +`mscoree.dll!_CorDllMain` ). AsmResolver includes a built-in implementation for this that is based on [the reference implementation provided by GData](https://www.gdatasoftware.com/blog/2020/06/36164-introducing-the-typerefhash-trh). The hash can be obtained using the `GetTypeReferenceHash` extension method on `IPEImage`or on `IMetadata`: diff --git a/docs/guides/peimage/ready-to-run.md b/docs/guides/peimage/ready-to-run.md new file mode 100644 index 000000000..8c634ece7 --- /dev/null +++ b/docs/guides/peimage/ready-to-run.md @@ -0,0 +1,234 @@ +# .NET ReadyToRun Directory + +ReadyToRun (RTR or R2R) is a technology where managed method bodies written +in the Common Intermediate Language (CIL) are compiled ahead of time (AoT). +This can speed up startup times of a .NET application, as the program is not +required to JIT compile the pre-compiled method bodies at runtime anymore. + +Low level ReadyToRun metadata can be found in the .NET data directory, under +the managed native header entry. AsmResolver provides rich support for various +ReadyToRun metadata structures found in this header. + +To test whether a PE image has ReadyToRun metadata, query the +`ManagedNativeHeader` property and test if it is of type `ReadyToRunDirectory`: + +```csharp +using AsmResolver.PE.DotNet.ReadyToRun; + +IPEImage image = ... + +var header = image.DotNetDirectory.ManagedNativeHeader; +if (header is ReadyToRunDirectory directory) +{ + // Application has ReadyToRun metadata. +} +``` + +In the following, we will assume `directory` is the root `ReadyToRunDirectory` +instance obtained in a similar manner. + + +## Sections + +The ReadyToRun data directory consists of various sections, which can be accessed +through the `Sections` property. + +```csharp +ReadyToRunDirectory directory = ... + +for (int i = 0; i < directory.Sections.Count; i++) + Console.WriteLine($"{i}: {directory.Sections[i].Type}"); +``` + +`Sections` is mutable. New sections can be added and existing ones can be removed: +```csharp +directory.Sections.Add(new CustomReadyToRunSection(...)); +directory.Sections.RemoveAt(0); +``` + +Individual sections can be obtained by the `GetSection` method: + +```csharp +var section = directory.GetSection(ReadyToRunSectionType.CompilerIdentifier); +``` + +```csharp +var section = directory.GetSection(); +``` + +`GetSection` throws if the section is not present in the directory. +Alternatively, it is possible to use the non-throwing `TryGetSection` +that returns a `bool` instead: + +```csharp +if (directory.TryGetSection(ReadyToRunSectionType.CompilerIdentifier, out var section)) +{ + // Section exists. +} +``` + +```csharp +if (directory.TryGetSection(out CompilerIdentifierSection? section)) +{ + // Section exists. +} +``` + +The raw binary contents of many sections can be read using `CreateReader`, +which returns a `BinaryStreamReader`: + +```csharp +if (section.CanRead) +{ + var reader = section.CreateReader(); + // parse data here... +} +``` + +AsmResolver provides parsing for various sections out of the box. All +supported section formats have a designated class that interpret and expose +the data stored in these sections. +Below is a table of currently supported section types. + +| Section Index | Section Type (as per specification) | AsmResolver Type | +|---------------|:------------------------------------|:----------------------------------| +| 100 | `CompilerIdentifier` | `CompilerIdentifierSection` | +| 101 | `ImportSections` | `SerializedImportSectionsSection` | +| 102 | `RuntimeFunctions` | `RuntimeFunctionsSection` | +| 103 | `MethodDefEntryPoints` | `MethodEntryPointsSection` | +| 105 | `DebugInfo` | `DebugInfoSection` | + +Any other unsupported section is represented using `CustomReadyToRunSection` +exposing the raw data as an `ISegment`. + + +## Runtime Functions + +References to the precompiled native code of managed methods is stored in the +`RuntimeFunctions` section, represented by the `RuntimeFunctionsSection` class. + +```csharp +var section = directory.GetSection(); + +foreach (var function in section.GetFunctions()) + Console.WriteLine($"Rva: {function.Begin.Rva:X8}"); +``` + +To start reading the native code of such a function, use `CreateReader` on the +exposed `ISegmentReference`s: + +```csharp +if (function.Begin.CanRead) +{ + var reader = function.Begin.CreateReader(); + // ... +} +``` + +The remaining format of each function follows the `RUNTIME_FUNCTION` structure +as found in the Exceptions Data Directory of a PE file, and thus is +platform-specific. + +Below an example can be found for inspecting fields such as unwind info in this +structure for AMD64 PEs: + +```csharp +var section = directory.GetSection(); + +foreach (var function in section.GetFunctions().OfType()) +{ + Console.WriteLine($"Rva: {function.Begin.Rva:X8}"); + + var unwindInfo = function.UnwindInfo; + Console.WriteLine($"Size of Prolog: {function.UnwindInfo.SizeOfProlog}"); +} +``` + +Refer to the documentation of the [Exceptions Directory](exceptions.md) for +ways to casting and interpreting each supported specific formats. + + +## Method Entry Points + +Method entry points describe the starting runtime function and references +fixups in the `ImportSections` section that need to be called when calling a +pre-compiled managed method. In AsmResolver, entry points are exposed by the +`MethodEntryPointsSection` class. + +```csharp +var section = directory.GetSection(); +``` + +The method entry points section is ordered in such a way that the `i`-th entry +point maps to the `i`-th method in the method table. Note that not every method +is required to have an entry point specified. Some entries in this list +may therefore be `null`. + +Below an example that iterates all entry points and their fixups: + +```csharp +for (int i = 0; i < section.EntryPoints.Count; i++) +{ + var token = new MetadataToken(TableIndex.Method, (uint) (i + 1)); + var entryPoint = section.EntryPoints[i]; + + if (entryPoint is null) + { + Console.WriteLine($"Method {token} is not mapped to a runtime function."); + } + else + { + Console.WriteLine($"Method {token} is mapped to runtime function {entryPoint.RuntimeFunctionIndex}."); + foreach (var fixup in entryPoint.Fixups) + Console.WriteLine($"- Import {fixup.ImportIndex}, Slot {fixup.SlotIndex}"); + } +} +``` + +## Import Sections + +Method entry points reference fixups defined in the import section. The +imports can be extracted using the `ImportSectionsSection` class. + +```csharp +var section = directory.GetSection(); +``` + +This section contains various sub-sections of type `ImportSection`. +Every section contains a set of `Slots`, a list of references that will +receive addresses to strings, IL code or other stub dispatches. + +```csharp +foreach (var import in section.Sections) +{ + Console.WriteLine($"Import Type: {import.Type}"); + for (int i = 0; i < import.Slots.Count; i++) + Console.WriteLine($" {i}: {import.Slots[i].Rva}"); +} +``` + +Some sections have a parallel list of signature binary blobs, which +describe the general shape and parameters of the associated slot. + +```csharp +foreach (var import in section.Sections) +{ + Console.WriteLine($"Import Type: {import.Type}"); + + for (int i = 0; i < import.Slots.Count; i++) + { + Console.WriteLine($" {i}: {import.Slots[i].Rva}"); + + if (i < import.Signatures.Count && import.Signatures[i].CanRead) + { + var reader = import.Signatures[i].CreateReader(); + // Parse signature here ... + } + } +} +``` + +> [!NOTE] +> AsmResolver does not provide any high-level parsing for signatures on +> this level of abstraction. It is expected to be added to the +> `AsmResolver.DotNet` package in the future. diff --git a/docs/guides/toc.yml b/docs/guides/toc.yml index ff55f16ac..8cf8b1e53 100644 --- a/docs/guides/toc.yml +++ b/docs/guides/toc.yml @@ -41,6 +41,8 @@ href: peimage/tls.md - name: .NET Data Directories href: peimage/dotnet.md +- name: .NET ReadyToRun Directory + href: peimage/ready-to-run.md - name: PE File Building href: peimage/pe-building.md diff --git a/src/AsmResolver.DotNet.Dynamic/AsmResolver.DotNet.Dynamic.csproj b/src/AsmResolver.DotNet.Dynamic/AsmResolver.DotNet.Dynamic.csproj index f3e66f4b9..f6c65a741 100644 --- a/src/AsmResolver.DotNet.Dynamic/AsmResolver.DotNet.Dynamic.csproj +++ b/src/AsmResolver.DotNet.Dynamic/AsmResolver.DotNet.Dynamic.csproj @@ -6,7 +6,7 @@ exe pe directories imports exports resources dotnet cil inspection manipulation assembly disassembly dynamic true 1701;1702;NU5105 - net6.0;netcoreapp3.1;netstandard2.0 + net6.0;netcoreapp3.1;netstandard2.0;netstandard2.1 enable diff --git a/src/AsmResolver.DotNet/AsmResolver.DotNet.csproj b/src/AsmResolver.DotNet/AsmResolver.DotNet.csproj index 79a63a67f..aaa73fef2 100644 --- a/src/AsmResolver.DotNet/AsmResolver.DotNet.csproj +++ b/src/AsmResolver.DotNet/AsmResolver.DotNet.csproj @@ -7,7 +7,7 @@ true 1701;1702;NU5105 enable - net6.0;netcoreapp3.1;netstandard2.0 + net6.0;netcoreapp3.1;netstandard2.0;netstandard2.1 true diff --git a/src/AsmResolver.DotNet/AssemblyResolverBase.cs b/src/AsmResolver.DotNet/AssemblyResolverBase.cs index 64d057c99..00c0f9ce9 100644 --- a/src/AsmResolver.DotNet/AssemblyResolverBase.cs +++ b/src/AsmResolver.DotNet/AssemblyResolverBase.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using AsmResolver.DotNet.Signatures; @@ -15,7 +16,7 @@ public abstract class AssemblyResolverBase : IAssemblyResolver private static readonly string[] BinaryFileExtensions = {".dll", ".exe"}; private static readonly SignatureComparer Comparer = new(SignatureComparisonFlags.AcceptNewerVersions); - private readonly Dictionary _cache = new(new SignatureComparer()); + private readonly ConcurrentDictionary _cache = new(new SignatureComparer()); /// /// Initializes the base of an assembly resolver. @@ -46,14 +47,18 @@ public IList SearchDirectories /// public AssemblyDefinition? Resolve(AssemblyDescriptor assembly) { - if (_cache.TryGetValue(assembly, out var assemblyDef)) - return assemblyDef; + AssemblyDefinition? result; - assemblyDef = ResolveImpl(assembly); - if (assemblyDef is not null) - _cache.Add(assembly, assemblyDef); + while (!_cache.TryGetValue(assembly, out result)) + { + var candidate = ResolveImpl(assembly); + if (candidate is null) + break; - return assemblyDef; + _cache.TryAdd(assembly, candidate); + } + + return result; } /// @@ -65,11 +70,11 @@ public void AddToCache(AssemblyDescriptor descriptor, AssemblyDefinition definit if (!Comparer.Equals(descriptor, definition)) throw new ArgumentException("Assembly descriptor and definition do not refer to the same assembly."); - _cache.Add(descriptor, definition); + _cache.TryAdd(descriptor, definition); } /// - public bool RemoveFromCache(AssemblyDescriptor descriptor) => _cache.Remove(descriptor); + public bool RemoveFromCache(AssemblyDescriptor descriptor) => _cache.TryRemove(descriptor, out _); /// public bool HasCached(AssemblyDescriptor descriptor) => _cache.ContainsKey(descriptor); diff --git a/src/AsmResolver.DotNet/Builder/DotNetDirectoryBuffer.MemberTree.cs b/src/AsmResolver.DotNet/Builder/DotNetDirectoryBuffer.MemberTree.cs index 617253eef..bda5aeee2 100644 --- a/src/AsmResolver.DotNet/Builder/DotNetDirectoryBuffer.MemberTree.cs +++ b/src/AsmResolver.DotNet/Builder/DotNetDirectoryBuffer.MemberTree.cs @@ -74,31 +74,43 @@ public void FinalizeModule(ModuleDefinition module) AddFileReferencesInModule(module); AddExportedTypesInModule(module); - AddResourcesInModule(module); AddCustomAttributes(token, module); } - private void AddResourcesInModule(ModuleDefinition module) + /// + /// Adds a collection of manifest resources to the directory buffer. + /// + /// The resources to add. + /// + /// true if resource data can be reused when identical, false when each embedded resource should + /// get its own data offset. + /// + public void DefineManifestResources(IEnumerable resources, bool deduplicateData = true) { - for (int i = 0; i < module.Resources.Count; i++) - AddManifestResource(module.Resources[i]); + foreach (var resource in resources) + DefineManifestResource(resource, deduplicateData); } /// /// Adds a single manifest resource to the buffer. /// /// The resource to add. + /// + /// true if resource data can be reused when identical, false when each embedded resource should + /// get its own data offset. + /// /// The new metadata token of the resource. - public MetadataToken AddManifestResource(ManifestResource resource) + public MetadataToken DefineManifestResource(ManifestResource resource, bool deduplicateData = true) { uint offset = resource.Offset; if (resource.IsEmbedded) { - if (resource.EmbeddedDataSegment is {} segment) + if (resource.EmbeddedDataSegment is { } segment) { - using var stream = new MemoryStream(); - segment.Write(new BinaryStreamWriter(stream)); - offset = Resources.GetResourceDataOffset(stream.ToArray()); + byte[] data = segment.WriteIntoArray(); + offset = deduplicateData + ? Resources.GetResourceDataOffset(data) + : Resources.AppendLengthPrefixedData(data); } else { @@ -117,6 +129,7 @@ public MetadataToken AddManifestResource(ManifestResource resource) var token = table.Add(row); _tokenMapping.Register(resource, token); AddCustomAttributes(token, resource); + return token; } diff --git a/src/AsmResolver.DotNet/Builder/DotNetDirectoryBuffer.TokenProvider.cs b/src/AsmResolver.DotNet/Builder/DotNetDirectoryBuffer.TokenProvider.cs index b3eca662b..49a3b15aa 100644 --- a/src/AsmResolver.DotNet/Builder/DotNetDirectoryBuffer.TokenProvider.cs +++ b/src/AsmResolver.DotNet/Builder/DotNetDirectoryBuffer.TokenProvider.cs @@ -38,7 +38,7 @@ public MetadataToken AddTypeReference(TypeReference? type, bool allowDuplicates, Metadata.StringsStream.GetStringIndex(type.Name), Metadata.StringsStream.GetStringIndex(type.Namespace)); - var token = preserveRid + var token = preserveRid && type.MetadataToken.Rid != 0 ? table.Insert(type.MetadataToken.Rid, row, allowDuplicates) : table.Add(row, allowDuplicates); @@ -205,7 +205,7 @@ public MetadataToken AddAssemblyReference(AssemblyReference? assembly, bool allo Metadata.StringsStream.GetStringIndex(assembly.Culture), Metadata.BlobStream.GetBlobIndex(assembly.HashValue)); - var token = preserveRid + var token = preserveRid && assembly.MetadataToken.Rid != 0 ? table.Insert(assembly.MetadataToken.Rid, row, allowDuplicates) : table.Add(row, allowDuplicates); @@ -243,7 +243,7 @@ public MetadataToken AddModuleReference(ModuleReference? reference, bool allowDu var table = Metadata.TablesStream.GetDistinctTable(TableIndex.ModuleRef); var row = new ModuleReferenceRow(Metadata.StringsStream.GetStringIndex(reference.Name)); - var token = preserveRid + var token = preserveRid && reference.MetadataToken.Rid != 0 ? table.Insert(reference.MetadataToken.Rid, row, allowDuplicates) : table.Add(row, allowDuplicates); diff --git a/src/AsmResolver.DotNet/Builder/DotNetDirectoryFactory.cs b/src/AsmResolver.DotNet/Builder/DotNetDirectoryFactory.cs index 7f2e56455..aad8b0755 100644 --- a/src/AsmResolver.DotNet/Builder/DotNetDirectoryFactory.cs +++ b/src/AsmResolver.DotNet/Builder/DotNetDirectoryFactory.cs @@ -118,6 +118,11 @@ public virtual DotNetDirectoryBuildResult CreateDotNetDirectory( if (module.Assembly?.ManifestModule == module) buffer.DefineAssembly(module.Assembly); + // Add resources (if any). + buffer.DefineManifestResources( + module.Resources, + (MetadataBuilderFlags & MetadataBuilderFlags.NoResourceDataDeduplication) == 0); + // Finalize module. buffer.FinalizeModule(module); @@ -215,9 +220,6 @@ private IMetadataBuffer CreateMetadataBuffer(ModuleDefinition module) private void ImportBasicTablesIfSpecified(ModuleDefinition module, DotNetDirectoryBuffer buffer) { - if (module.DotNetDirectory is null) - return; - // NOTE: The order of this table importing is crucial. // // Assembly refs should always be imported prior to importing type refs, which should be imported before @@ -246,9 +248,6 @@ private void ImportBasicTablesIfSpecified(ModuleDefinition module, DotNetDirecto private void ImportTypeSpecsIfSpecified(ModuleDefinition module, DotNetDirectoryBuffer buffer) { - if (module.DotNetDirectory is null) - return; - if ((MetadataBuilderFlags & MetadataBuilderFlags.PreserveTypeSpecificationIndices) != 0) { ImportTables(module, TableIndex.TypeSpec, @@ -258,9 +257,6 @@ private void ImportTypeSpecsIfSpecified(ModuleDefinition module, DotNetDirectory private void ImportMemberRefsIfSpecified(ModuleDefinition module, DotNetDirectoryBuffer buffer) { - if (module.DotNetDirectory is null) - return; - if ((MetadataBuilderFlags & MetadataBuilderFlags.PreserveMemberReferenceIndices) != 0) { ImportTables(module, TableIndex.MemberRef, @@ -270,9 +266,6 @@ private void ImportMemberRefsIfSpecified(ModuleDefinition module, DotNetDirector private void ImportRemainingTablesIfSpecified(ModuleDefinition module, DotNetDirectoryBuffer buffer) { - if (module.DotNetDirectory is null) - return; - if ((MetadataBuilderFlags & MetadataBuilderFlags.PreserveStandAloneSignatureIndices) != 0) { ImportTables(module, TableIndex.StandAloneSig, @@ -289,10 +282,10 @@ private void ImportRemainingTablesIfSpecified(ModuleDefinition module, DotNetDir private static void ImportTables(ModuleDefinition module, TableIndex tableIndex, Func importAction) { - int count = module.DotNetDirectory!.Metadata - !.GetStream() + int count = module.DotNetDirectory?.Metadata? + .GetStream() .GetTable(tableIndex) - .Count; + .Count ?? 0; for (uint rid = 1; rid <= count; rid++) importAction((TMember) module.LookupMember(new MetadataToken(tableIndex, rid))); diff --git a/src/AsmResolver.DotNet/Builder/MetadataBuilderFlags.cs b/src/AsmResolver.DotNet/Builder/MetadataBuilderFlags.cs index 04672823d..393547f60 100644 --- a/src/AsmResolver.DotNet/Builder/MetadataBuilderFlags.cs +++ b/src/AsmResolver.DotNet/Builder/MetadataBuilderFlags.cs @@ -155,5 +155,18 @@ public enum MetadataBuilderFlags /// /// NoStringsStreamOptimization = 0x20000, + + /// + /// + /// By default, when adding two embedded resources to a file with identical contents, AsmResolver will not + /// add the second copy of the data to the output file and instead reuse the first blob. This can drastically + /// reduce the size of the final output file. + /// + /// + /// While supported by the .NET runtime, some post-processors (e.g., obfuscators) may not work well with this + /// or depend on individual resource items to be present. Setting this flag will disable this optimization. + /// + /// + NoResourceDataDeduplication = 0x40000 } } diff --git a/src/AsmResolver.DotNet/Builder/Resources/DotNetResourcesDirectoryBuffer.cs b/src/AsmResolver.DotNet/Builder/Resources/DotNetResourcesDirectoryBuffer.cs index 073df1eec..e5125192e 100644 --- a/src/AsmResolver.DotNet/Builder/Resources/DotNetResourcesDirectoryBuffer.cs +++ b/src/AsmResolver.DotNet/Builder/Resources/DotNetResourcesDirectoryBuffer.cs @@ -34,8 +34,9 @@ public DotNetResourcesDirectoryBuffer() /// The data to append. /// The index to the start of the data. /// - /// This method does not index the resource data. Calling or - /// on the same data will append the data a second time. + /// This method does not index the resource data. Calling , + /// or on the same data will + /// append the data a second time. /// public uint AppendRawData(byte[] data) { @@ -44,6 +45,24 @@ public uint AppendRawData(byte[] data) return offset; } + /// + /// Appends raw data to the stream, prepending the data with a length. + /// + /// The data to append. + /// The index to the start of the prefixed data. + /// + /// This method does not index the resource data. Calling , + /// or on the same data will + /// append the data a second time. + /// + public uint AppendLengthPrefixedData(byte[] data) + { + uint offset = (uint) _rawStream.Length; + _writer.WriteUInt32((uint) data.Length); + AppendRawData(data); + return offset; + } + /// /// Gets the index to the provided resource data. If the blob is not present in the buffer, it will be appended /// to the end of the stream. @@ -57,9 +76,7 @@ public uint GetResourceDataOffset(byte[]? data) if (!_dataOffsets.TryGetValue(data, out uint offset)) { - offset = (uint) _rawStream.Length; - _writer.WriteUInt32((uint) data.Length); - AppendRawData(data); + offset = AppendLengthPrefixedData(data); _dataOffsets.Add(data, offset); } diff --git a/src/AsmResolver.DotNet/Builder/TokenMapping.cs b/src/AsmResolver.DotNet/Builder/TokenMapping.cs index 678dc1753..8b91287b9 100644 --- a/src/AsmResolver.DotNet/Builder/TokenMapping.cs +++ b/src/AsmResolver.DotNet/Builder/TokenMapping.cs @@ -64,12 +64,14 @@ public void Register(IMetadataMember member, MetadataToken newToken) if (member.MetadataToken.Table != newToken.Table) throw new ArgumentException($"Cannot assign a {newToken.Table} metadata token to a {member.MetadataToken.Table}."); - if (TryGetNewToken(member, out var existingToken)) - { - if (existingToken.Rid != newToken.Rid) - throw new ArgumentException($"Member {member.SafeToString()} was already assigned a metadata token."); + // We allow for members to be duplicated in the mapping, but we will only keep track of the first token that + // was registered for this member. This may result in a slightly different binary if token preservation is + // enabled (e.g., a member may originally have referenced a duplicated member as opposed to the first one, + // and this would always make it reference the first one), but since both members are semantically equivalent, + // referencing one or the other should also be semantics-preserving. Note that both duplicated members will + // still be present in the final binary with their original RIDs. + if (TryGetNewToken(member, out _)) return; - } switch (member.MetadataToken.Table) { diff --git a/src/AsmResolver.DotNet/Cloning/CloneContextAwareReferenceImporter.cs b/src/AsmResolver.DotNet/Cloning/CloneContextAwareReferenceImporter.cs index ea1b7d335..6e7f76182 100644 --- a/src/AsmResolver.DotNet/Cloning/CloneContextAwareReferenceImporter.cs +++ b/src/AsmResolver.DotNet/Cloning/CloneContextAwareReferenceImporter.cs @@ -1,3 +1,5 @@ +using System; + namespace AsmResolver.DotNet.Cloning { /// @@ -49,11 +51,16 @@ public override IMethodDefOrRef ImportMethod(IMethodDefOrRef method) /// protected override ITypeDefOrRef ImportType(TypeReference type) { - return type.Namespace == "System" - && type.Name == nameof(System.Object) - && (type.Scope?.GetAssembly()?.IsCorLib ?? false) - ? _context.Module.CorLibTypeFactory.Object.Type - : base.ImportType(type); + // Special case for System.Object. + if (type.IsTypeOf(nameof(System), nameof(Object)) && (type.Scope?.GetAssembly()?.IsCorLib ?? false)) + return _context.Module.CorLibTypeFactory.Object.Type; + + // Rare case where a type reference could point to one of the included type definitions + // (e.g., in custom attributes type arguments, see https://github.com/Washi1337/AsmResolver/issues/482). + if (_context.ClonedTypes.TryGetValue(type, out var clonedType)) + return (ITypeDefOrRef) clonedType; + + return base.ImportType(type); } } } diff --git a/src/AsmResolver.DotNet/Cloning/MemberCloneContext.cs b/src/AsmResolver.DotNet/Cloning/MemberCloneContext.cs index 9c0a297c1..244ea807d 100644 --- a/src/AsmResolver.DotNet/Cloning/MemberCloneContext.cs +++ b/src/AsmResolver.DotNet/Cloning/MemberCloneContext.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using AsmResolver.DotNet.Signatures; namespace AsmResolver.DotNet.Cloning { @@ -49,5 +50,17 @@ public IDictionary ClonedMembers { get; } = new Dictionary(); + + /// + /// Gets a mapping of original types to their cloned counterparts. + /// + /// + /// This dictionary performs lookups based on value using a instead of object + /// identity, and can thus be used to translate type references to included type definitions. + /// + public IDictionary ClonedTypes + { + get; + } = new Dictionary(SignatureComparer.Default); } } diff --git a/src/AsmResolver.DotNet/Cloning/MemberCloner.cs b/src/AsmResolver.DotNet/Cloning/MemberCloner.cs index cf9862e78..07a7d0cc0 100644 --- a/src/AsmResolver.DotNet/Cloning/MemberCloner.cs +++ b/src/AsmResolver.DotNet/Cloning/MemberCloner.cs @@ -3,6 +3,7 @@ using AsmResolver.DotNet.Signatures; using AsmResolver.DotNet.Signatures.Marshal; using AsmResolver.DotNet.Signatures.Security; +using AsmResolver.DotNet.Signatures.Types; using AsmResolver.PE.DotNet.Metadata.Tables; namespace AsmResolver.DotNet.Cloning @@ -320,6 +321,7 @@ private static void CreateTypeStub(MemberCloneContext context, TypeDefinition ty var typeStub = new TypeDefinition(type.Namespace, type.Name, type.Attributes); context.ClonedMembers.Add(type, typeStub); + context.ClonedTypes.Add(type, typeStub); } private void DeepCopyMembers(MemberCloneContext context) @@ -442,11 +444,19 @@ private static CustomAttributeArgument CloneCustomAttributeArgument(MemberCloneC // Copy all elements. for (int i = 0; i < argument.Elements.Count; i++) - clonedArgument.Elements.Add(argument.Elements[i]); + clonedArgument.Elements.Add(CloneElement(context, argument.Elements[i])); return clonedArgument; } + private static object? CloneElement(MemberCloneContext context, object? element) + { + if (element is TypeSignature type) + return context.Importer.ImportTypeSignature(type); + + return element; + } + private static ImplementationMap? CloneImplementationMap(MemberCloneContext context, ImplementationMap? map) { if (map is null) diff --git a/src/AsmResolver.DotNet/DefaultMetadataResolver.cs b/src/AsmResolver.DotNet/DefaultMetadataResolver.cs index e80d5cae4..519e769da 100644 --- a/src/AsmResolver.DotNet/DefaultMetadataResolver.cs +++ b/src/AsmResolver.DotNet/DefaultMetadataResolver.cs @@ -177,6 +177,13 @@ public TypeResolution(IAssemblyResolver resolver) switch (scope.MetadataToken.Table) { case TableIndex.AssemblyRef: + if (reference.Module?.Assembly is { } assembly) + { + // Are we referencing the current assembly the reference was declared in? + if (SignatureComparer.Default.Equals(scope.GetAssembly(), assembly)) + return FindTypeInModule(reference.Module, reference.Namespace, reference.Name); + } + var assemblyDefScope = _assemblyResolver.Resolve((AssemblyReference) scope); return assemblyDefScope is not null ? FindTypeInAssembly(assemblyDefScope, reference.Namespace, reference.Name) diff --git a/src/AsmResolver.DotNet/MethodDefinition.cs b/src/AsmResolver.DotNet/MethodDefinition.cs index 234396cb2..00105ab2d 100644 --- a/src/AsmResolver.DotNet/MethodDefinition.cs +++ b/src/AsmResolver.DotNet/MethodDefinition.cs @@ -8,6 +8,8 @@ using AsmResolver.DotNet.Code.Native; using AsmResolver.DotNet.Collections; using AsmResolver.DotNet.Signatures; +using AsmResolver.DotNet.Signatures.Types; +using AsmResolver.PE.DotNet.Cil; using AsmResolver.PE.DotNet.Metadata.Tables; using AsmResolver.PE.DotNet.Metadata.Tables.Rows; @@ -689,6 +691,56 @@ public UnmanagedExportInfo? ExportInfo set => _exportInfo.SetValue(value); } + /// + /// Creates a new private static constructor for a type that is executed when its declaring type is loaded by the CLR. + /// + /// The target module the method will be added to. + /// The constructor. + /// + /// The resulting method's body will consist of a single ret instruction. + /// + public static MethodDefinition CreateStaticConstructor(ModuleDefinition module) + { + var cctor = new MethodDefinition(".cctor", + MethodAttributes.Private + | MethodAttributes.Static + | MethodAttributes.SpecialName + | MethodAttributes.RuntimeSpecialName, + MethodSignature.CreateStatic(module.CorLibTypeFactory.Void)); + + cctor.CilMethodBody = new CilMethodBody(cctor); + cctor.CilMethodBody.Instructions.Add(CilOpCodes.Ret); + + return cctor; + } + + /// + /// Creates a new public constructor for a type that is executed when its declaring type is loaded by the CLR. + /// + /// The target module the method will be added to. + /// An ordered list of types the parameters of the constructor should have. + /// The constructor. + /// + /// The resulting method's body will consist of a single ret instruction, and does not contain a call to + /// any of the declaring type's base classes. For an idiomatic .NET binary, this should be added. + /// + public static MethodDefinition CreateConstructor(ModuleDefinition module, params TypeSignature[] parameterTypes) + { + var ctor = new MethodDefinition(".ctor", + MethodAttributes.Public + | MethodAttributes.SpecialName + | MethodAttributes.RuntimeSpecialName, + MethodSignature.CreateInstance(module.CorLibTypeFactory.Void, parameterTypes)); + + for (int i = 0; i < parameterTypes.Length; i++) + ctor.ParameterDefinitions.Add(new ParameterDefinition(null)); + + ctor.CilMethodBody = new CilMethodBody(ctor); + ctor.CilMethodBody.Instructions.Add(CilOpCodes.Ret); + + return ctor; + } + MethodDefinition IMethodDescriptor.Resolve() => this; /// diff --git a/src/AsmResolver.DotNet/Signatures/CustomAttributeArgument.cs b/src/AsmResolver.DotNet/Signatures/CustomAttributeArgument.cs index 8683f8925..2ae57418d 100644 --- a/src/AsmResolver.DotNet/Signatures/CustomAttributeArgument.cs +++ b/src/AsmResolver.DotNet/Signatures/CustomAttributeArgument.cs @@ -19,7 +19,9 @@ public class CustomAttributeArgument /// The type of the argument to read. /// The input stream. /// The argument. - public static CustomAttributeArgument FromReader(in BlobReaderContext context, TypeSignature argumentType, + public static CustomAttributeArgument FromReader( + in BlobReaderContext context, + TypeSignature argumentType, ref BinaryStreamReader reader) { var elementReader = CustomAttributeArgumentReader.Create(); diff --git a/src/AsmResolver.DotNet/Signatures/Types/CorLibTypeFactory.cs b/src/AsmResolver.DotNet/Signatures/Types/CorLibTypeFactory.cs index 76e35fcdf..516342867 100644 --- a/src/AsmResolver.DotNet/Signatures/Types/CorLibTypeFactory.cs +++ b/src/AsmResolver.DotNet/Signatures/Types/CorLibTypeFactory.cs @@ -11,16 +11,6 @@ namespace AsmResolver.DotNet.Signatures.Types /// public class CorLibTypeFactory { - /// - /// Creates a new type factory that references mscorlib 4.0.0.0. - /// - /// The factory. - public static CorLibTypeFactory CreateMscorlib40TypeFactory(ModuleDefinition module) - { - var importer = new ReferenceImporter(module); - return new CorLibTypeFactory(importer.ImportScope(KnownCorLibs.MsCorLib_v4_0_0_0)); - } - private CorLibTypeSignature? _void; private CorLibTypeSignature? _boolean; private CorLibTypeSignature? _char; @@ -146,6 +136,16 @@ public IResolutionScope CorLibScope /// Gets the element type signature for . /// public CorLibTypeSignature Object => GetOrCreateCorLibTypeSignature(ref _object, ElementType.Object, nameof(Object)); + + /// + /// Creates a new type factory that references mscorlib 4.0.0.0. + /// + /// The factory. + public static CorLibTypeFactory CreateMscorlib40TypeFactory(ModuleDefinition module) + { + var importer = new ReferenceImporter(module); + return new CorLibTypeFactory(importer.ImportScope(KnownCorLibs.MsCorLib_v4_0_0_0)); + } /// /// Transforms the provided type descriptor to a common object runtime type signature. diff --git a/src/AsmResolver.DotNet/TypeDefinition.cs b/src/AsmResolver.DotNet/TypeDefinition.cs index b3792ad8b..c05c1ee5f 100644 --- a/src/AsmResolver.DotNet/TypeDefinition.cs +++ b/src/AsmResolver.DotNet/TypeDefinition.cs @@ -647,51 +647,91 @@ public ClassLayout? ClassLayout } /// - /// Determines whether the type inherits from a particular type + /// Determines whether the type inherits from a particular type. /// /// The full name of the type - /// Whether the current inherits the type - public bool InheritsFrom(string fullName) - { - var type = this; - do - { - if (type.FullName == fullName) - return true; - - var current = type; - type = type.BaseType?.Resolve(); + /// + /// true whether the current inherits from the type, + /// false otherwise. + /// + public bool InheritsFrom(string fullName) => FindInTypeTree(x => x.FullName == fullName); - // This prevents an issue where the base type is the same as itself - // ... so basically a cyclic dependency - if (current == type) - return false; - } while (type is {}); + /// + /// Determines whether the type inherits from a particular type. + /// + /// The namespace of the type. + /// The name of the type. + /// + /// true whether the current inherits from the type, + /// false otherwise. + /// + public bool InheritsFrom(string? ns, string name) => FindInTypeTree(x => x.IsTypeOf(ns, name)); - return false; - } + /// + /// Determines whether the type inherits from a particular type. + /// + /// The namespace of the type. + /// The name of the type. + /// + /// true whether the current inherits from the type, + /// false otherwise. + /// + public bool InheritsFrom(Utf8String? ns, Utf8String name) => FindInTypeTree(x => x.IsTypeOfUtf8(ns, name)); /// - /// Determines whether the type implements a particular interface + /// Determines whether the type implements a particular interface. /// /// The full name of the interface - /// Whether the type implements the interface + /// + /// true whether the current implements the interface, + /// false otherwise. + /// public bool Implements(string fullName) { + return FindInTypeTree(x => x.Interfaces.Any(@interface => @interface.Interface?.FullName == fullName)); + } + + /// + /// Determines whether the type implements a particular interface. + /// + /// The namespace of the type. + /// The name of the type. + /// + /// true whether the current implements the interface, + /// false otherwise. + /// + public bool Implements(string? ns, string name) => FindInTypeTree( + x => x.Interfaces.Any(@interface => @interface.Interface?.IsTypeOf(ns, name) ?? false)); + + /// + /// Determines whether the type implements a particular interface. + /// + /// The namespace of the type. + /// The name of the type. + /// + /// true whether the current implements the interface, + /// false otherwise. + /// + public bool Implements(Utf8String? ns, Utf8String name) => FindInTypeTree( + x => x.Interfaces.Any(@interface => @interface.Interface?.IsTypeOfUtf8(ns, name) ?? false)); + + private bool FindInTypeTree(Predicate condition) + { + var visited = new List(); + var type = this; do { - if (type.Interfaces.Any(@interface => @interface.Interface?.FullName == fullName)) + // Protect against malicious cyclic dependency graphs. + if (visited.Contains(type)) + return false; + + if (condition(type)) return true; - var current = type; + visited.Add(type); type = type.BaseType?.Resolve(); - - // This prevents an issue where the base type is the same as itself - // ... so basically a cyclic dependency - if (current == type) - return false; - } while (type is {}); + } while (type is not null); return false; } @@ -792,15 +832,18 @@ public TypeReference ToTypeReference() } /// - /// Gets the static constructor that is executed when the CLR loads this type. + /// Finds the static constructor that is executed when the CLR loads this type. /// /// The static constructor, or null if none is present. public MethodDefinition? GetStaticConstructor() { - return Methods.FirstOrDefault(m => - m.IsConstructor - && m.IsStatic - && m.Parameters.Count == 0); + for (int i = 0; i < Methods.Count; i++) + { + if (Methods[i] is {IsConstructor: true, IsStatic: true, Parameters.Count: 0} method) + return method; + } + + return null; } /// @@ -829,22 +872,73 @@ public MethodDefinition GetOrCreateStaticConstructor(ModuleDefinition? module) if (module == null) throw new ArgumentNullException(nameof(module)); - cctor = new MethodDefinition(".cctor", - MethodAttributes.Private - | MethodAttributes.Static - | MethodAttributes.SpecialName - | MethodAttributes.RuntimeSpecialName, - MethodSignature.CreateStatic(module.CorLibTypeFactory.Void)); - - cctor.CilMethodBody = new CilMethodBody(cctor); - cctor.CilMethodBody.Instructions.Add(new CilInstruction(0, CilOpCodes.Ret)); - + cctor = MethodDefinition.CreateStaticConstructor(module); Methods.Insert(0, cctor); } return cctor; } + /// + /// Finds the instance parameterless constructor this type defines. + /// + /// The constructor, or null if none is present. + public MethodDefinition? GetConstructor() + { + return GetConstructor(SignatureComparer.Default, (IList) Array.Empty()); + } + + /// + /// Finds the instance constructor with the provided parameter types this type defines. + /// + /// An ordered list of types the parameters of the constructor should have. + /// The constructor, or null if none is present. + public MethodDefinition? GetConstructor(params TypeSignature[] parameterTypes) + { + return GetConstructor(SignatureComparer.Default, parameterTypes); + } + + /// + /// Finds the instance constructor with the provided parameter types this type defines. + /// + /// The signature comparer to use when comparing the parameter types. + /// An ordered list of types the parameters of the constructor should have. + /// The constructor, or null if none is present. + public MethodDefinition? GetConstructor(SignatureComparer comparer, params TypeSignature[] parameterTypes) + { + return GetConstructor(comparer, (IList) parameterTypes); + } + + /// + /// Finds the instance constructor with the provided parameter types this type defines. + /// + /// The signature comparer to use when comparing the parameter types. + /// An ordered list of types the parameters of the constructor should have. + /// The constructor, or null if none is present. + public MethodDefinition? GetConstructor(SignatureComparer comparer, IList parameterTypes) + { + for (int i = 0; i < Methods.Count; i++) + { + if (Methods[i] is not {IsConstructor: true, IsStatic: false} method) + continue; + + if (method.Parameters.Count != parameterTypes.Count) + continue; + + bool fullMatch = true; + for (int j = 0; j < method.Parameters.Count && fullMatch; j++) + { + if (!comparer.Equals(method.Parameters[j].ParameterType, parameterTypes[j])) + fullMatch = false; + } + + if (fullMatch) + return method; + } + + return null; + } + /// /// Obtains the namespace of the type definition. /// diff --git a/src/AsmResolver.DotNet/TypeReference.cs b/src/AsmResolver.DotNet/TypeReference.cs index 5e5294d7b..23abe8a89 100644 --- a/src/AsmResolver.DotNet/TypeReference.cs +++ b/src/AsmResolver.DotNet/TypeReference.cs @@ -140,7 +140,8 @@ public TypeSignature ToTypeSignature(bool isValueType) } /// - public bool IsImportedInModule(ModuleDefinition module) => Module == module; + public bool IsImportedInModule(ModuleDefinition module) => + Module == module && (Scope?.IsImportedInModule(module) ?? false); /// /// Imports the type reference using the provided reference importer object. diff --git a/src/AsmResolver.PE.File/AsmResolver.PE.File.csproj b/src/AsmResolver.PE.File/AsmResolver.PE.File.csproj index b89b4cfe0..aa5d496e7 100644 --- a/src/AsmResolver.PE.File/AsmResolver.PE.File.csproj +++ b/src/AsmResolver.PE.File/AsmResolver.PE.File.csproj @@ -8,7 +8,7 @@ 1701;1702;NU5105 true enable - net6.0;netcoreapp3.1;netstandard2.0 + net6.0;netcoreapp3.1;netstandard2.0;netstandard2.1 true diff --git a/src/AsmResolver.PE.File/PEFile.cs b/src/AsmResolver.PE.File/PEFile.cs index f41e15716..19df07c3a 100644 --- a/src/AsmResolver.PE.File/PEFile.cs +++ b/src/AsmResolver.PE.File/PEFile.cs @@ -186,7 +186,9 @@ public static PEFile FromReader(in BinaryStreamReader reader, PEMappingMode mode new SerializedPEFile(reader, mode); /// - public ISegmentReference GetReferenceToRva(uint rva) => new PESegmentReference(this, rva); + public ISegmentReference GetReferenceToRva(uint rva) => rva != 0 + ? new PESegmentReference(this, rva) + : SegmentReference.Null; /// public uint FileOffsetToRva(ulong fileOffset) => diff --git a/src/AsmResolver.PE.Win32Resources/AsmResolver.PE.Win32Resources.csproj b/src/AsmResolver.PE.Win32Resources/AsmResolver.PE.Win32Resources.csproj index c32f0f81b..b2fc17031 100644 --- a/src/AsmResolver.PE.Win32Resources/AsmResolver.PE.Win32Resources.csproj +++ b/src/AsmResolver.PE.Win32Resources/AsmResolver.PE.Win32Resources.csproj @@ -6,7 +6,7 @@ true 1701;1702;NU5105 enable - net6.0;netcoreapp3.1;netstandard2.0 + net6.0;netcoreapp3.1;netstandard2.0;netstandard2.1 true diff --git a/src/AsmResolver.PE/AsmResolver.PE.csproj b/src/AsmResolver.PE/AsmResolver.PE.csproj index b867a61e3..1ec5862cf 100644 --- a/src/AsmResolver.PE/AsmResolver.PE.csproj +++ b/src/AsmResolver.PE/AsmResolver.PE.csproj @@ -7,7 +7,7 @@ true 1701;1702;NU5105 enable - net6.0;netcoreapp3.1;netstandard2.0 + net6.0;netcoreapp3.1;netstandard2.0;netstandard2.1 true @@ -24,9 +24,9 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/AsmResolver.PE/DotNet/CustomManagedNativeHeader.cs b/src/AsmResolver.PE/DotNet/CustomManagedNativeHeader.cs new file mode 100644 index 000000000..3f01f3818 --- /dev/null +++ b/src/AsmResolver.PE/DotNet/CustomManagedNativeHeader.cs @@ -0,0 +1,52 @@ +using AsmResolver.IO; + +namespace AsmResolver.PE.DotNet +{ + /// + /// Represents a managed native header of a .NET Portable Executable that is in an unsupported or unknown file format. + /// + public class CustomManagedNativeHeader : SegmentBase, IManagedNativeHeader + { + /// + /// Creates a new custom managed native header. + /// + /// The signature to use. + /// The contents of the header, excluding the signature. + public CustomManagedNativeHeader(ManagedNativeHeaderSignature signature, ISegment contents) + { + Signature = signature; + Contents = contents; + } + + /// + public ManagedNativeHeaderSignature Signature + { + get; + } + + /// + /// Gets the contents of the header, excluding the signature. + /// + public ISegment Contents + { + get; + } + + /// + public override void UpdateOffsets(in RelocationParameters parameters) + { + base.UpdateOffsets(parameters); + Contents.UpdateOffsets(parameters.WithAdvance(sizeof(ManagedNativeHeaderSignature))); + } + + /// + public override uint GetPhysicalSize() => sizeof(ManagedNativeHeaderSignature) + Contents.GetPhysicalSize(); + + /// + public override void Write(IBinaryStreamWriter writer) + { + writer.WriteUInt32((uint) Signature); + Contents.Write(writer); + } + } +} diff --git a/src/AsmResolver.PE/DotNet/DotNetDirectory.cs b/src/AsmResolver.PE/DotNet/DotNetDirectory.cs index f41bd379c..1aea3b67e 100644 --- a/src/AsmResolver.PE/DotNet/DotNetDirectory.cs +++ b/src/AsmResolver.PE/DotNet/DotNetDirectory.cs @@ -17,7 +17,7 @@ public class DotNetDirectory : SegmentBase, IDotNetDirectory private readonly LazyVariable _codeManagerTable; private readonly LazyVariable _exportAddressTable; private readonly LazyVariable _vtableFixups; - private readonly LazyVariable _managedNativeHeader; + private readonly LazyVariable _managedNativeHeader; /// /// Creates a new .NET data directory. @@ -30,7 +30,7 @@ public DotNetDirectory() _codeManagerTable = new LazyVariable(x => x.GetCodeManagerTable()); _exportAddressTable = new LazyVariable(x => x.GetExportAddressTable()); _vtableFixups = new LazyVariable(x => x.GetVTableFixups()); - _managedNativeHeader = new LazyVariable(x => x.GetManagedNativeHeader()); + _managedNativeHeader = new LazyVariable(x => x.GetManagedNativeHeader()); } /// @@ -104,7 +104,7 @@ public IReadableSegment? ExportAddressTable } /// - public IReadableSegment? ManagedNativeHeader + public IManagedNativeHeader? ManagedNativeHeader { get => _managedNativeHeader.GetValue(this); set => _managedNativeHeader.SetValue(value); @@ -202,6 +202,6 @@ directoryContents is not null /// /// This method is called upon initialization of the property /// - protected virtual IReadableSegment? GetManagedNativeHeader() => null; + protected virtual IManagedNativeHeader? GetManagedNativeHeader() => null; } } diff --git a/src/AsmResolver.PE/DotNet/IDotNetDirectory.cs b/src/AsmResolver.PE/DotNet/IDotNetDirectory.cs index ba1524cdb..c8c70d94b 100644 --- a/src/AsmResolver.PE/DotNet/IDotNetDirectory.cs +++ b/src/AsmResolver.PE/DotNet/IDotNetDirectory.cs @@ -116,7 +116,7 @@ DotNetEntryPoint EntryPoint /// /// Gets or sets the data directory containing the managed native header of a mixed mode application (if available). /// - IReadableSegment? ManagedNativeHeader + IManagedNativeHeader? ManagedNativeHeader { get; set; diff --git a/src/AsmResolver.PE/DotNet/IManagedNativeHeader.cs b/src/AsmResolver.PE/DotNet/IManagedNativeHeader.cs new file mode 100644 index 000000000..6a2b2b5d9 --- /dev/null +++ b/src/AsmResolver.PE/DotNet/IManagedNativeHeader.cs @@ -0,0 +1,16 @@ +namespace AsmResolver.PE.DotNet +{ + /// + /// Represents a managed native header of a .NET module, containing Ahead-of-Time (AOT) compilation metadata. + /// + public interface IManagedNativeHeader : ISegment + { + /// + /// Gets the signature of the native header, indicating the type of metadata that is stored. + /// + ManagedNativeHeaderSignature Signature + { + get; + } + } +} diff --git a/src/AsmResolver.PE/DotNet/ManagedNativeHeaderSignature.cs b/src/AsmResolver.PE/DotNet/ManagedNativeHeaderSignature.cs new file mode 100644 index 000000000..e32a2a3cc --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ManagedNativeHeaderSignature.cs @@ -0,0 +1,18 @@ +namespace AsmResolver.PE.DotNet +{ + /// + /// Provides members defining all possible managed native header types. + /// + public enum ManagedNativeHeaderSignature : uint + { + /// + /// Indicates the managed native header is in the Native Image Generator (NGEN) file format. + /// + NGen = 0x4e45474e, + + /// + /// Indicates the managed native header is in the ReadyToRun (RTR) file format. + /// + Rtr = 0x00525452 + } +} diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/CompilerIdentifierSection.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/CompilerIdentifierSection.cs new file mode 100644 index 000000000..736b2aeaf --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/CompilerIdentifierSection.cs @@ -0,0 +1,51 @@ +using System.Diagnostics; +using System.Text; +using AsmResolver.IO; + +namespace AsmResolver.PE.DotNet.ReadyToRun +{ + /// + /// Represents a section in a ReadyToRun .NET portable executable that contains an identifier of the compiler + /// that was used to generate the image. + /// + [DebuggerDisplay("CompilerIdentifier ({" + nameof(Identifier) + "})")] + public class CompilerIdentifierSection : SegmentBase, IReadyToRunSection + { + /// + /// Creates a new compiler identifier section. + /// + /// The compiler identifier. + public CompilerIdentifierSection(string identifier) + { + Identifier = identifier; + } + + /// + public ReadyToRunSectionType Type => ReadyToRunSectionType.CompilerIdentifier; + + /// + public bool CanRead => true; + + /// + /// Gets or sets the identifier of the compiler that was used to create the ReadyToRun image. + /// + public string Identifier + { + get; + set; + } + + /// + public BinaryStreamReader CreateReader() => new(Encoding.ASCII.GetBytes($"{Identifier}\0")); + + /// + public override uint GetPhysicalSize() => (uint) Encoding.ASCII.GetByteCount(Identifier) + sizeof(byte); + + /// + public override void Write(IBinaryStreamWriter writer) + { + writer.WriteAsciiString(Identifier); + writer.WriteByte(0); + } + } +} diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/CustomReadyToRunSection.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/CustomReadyToRunSection.cs new file mode 100644 index 000000000..4f7a32ea8 --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/CustomReadyToRunSection.cs @@ -0,0 +1,57 @@ +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using AsmResolver.IO; + +namespace AsmResolver.PE.DotNet.ReadyToRun +{ + /// + /// Represents a ReadyToRun section with a custom or unsupported file format. + /// + [DebuggerDisplay(nameof(CustomReadyToRunSection) + " ({" + nameof(Type) + "})")] + public sealed class CustomReadyToRunSection : SegmentBase, IReadyToRunSection + { + /// + /// Creates a new ReadyToRun section with a custom format. + /// + /// The type of the section. + /// The contents of the section. + public CustomReadyToRunSection(ReadyToRunSectionType type, ISegment contents) + { + Type = type; + Contents = contents; + } + + /// + public ReadyToRunSectionType Type + { + get; + } + + /// + [MemberNotNullWhen(true, nameof(Contents))] + public bool CanRead => Contents is IReadableSegment; + + /// + /// Gets or sets the contents of the section. + /// + public ISegment Contents + { + get; + } + + /// + public override uint GetPhysicalSize() => Contents.GetPhysicalSize(); + + /// + public override void Write(IBinaryStreamWriter writer) => Contents.Write(writer); + + /// + public BinaryStreamReader CreateReader() + { + if (!CanRead) + throw new InvalidOperationException("Contents of the ReadyToRun section is not readable."); + return ((IReadableSegment) Contents).CreateReader(); + } + } +} diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/DebugInfo.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/DebugInfo.cs new file mode 100644 index 000000000..6a9684e82 --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/DebugInfo.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using AsmResolver.IO; +using AsmResolver.PE.File.Headers; + +namespace AsmResolver.PE.DotNet.ReadyToRun +{ + /// + /// Provides additional debug information to a precompiled method body. + /// + public class DebugInfo : SegmentBase + { + private IList? _bounds; + private IList? _variables; + private byte[]? _serialized; + private bool _is32Bit; + + /// + /// Gets a collection of bounds information associated to the method. + /// + public IList Bounds + { + get + { + if (_bounds is null) + Interlocked.CompareExchange(ref _bounds, GetBounds(), null); + return _bounds; + } + } + + /// + /// Gets a collection of native variable information associated to the method. + /// + public IList Variables + { + get + { + if (_variables is null) + Interlocked.CompareExchange(ref _variables, GetVariables(), null); + return _variables; + } + } + + /// + /// Obtains the bounds information of the method. + /// + /// The bounds. + /// + /// This method is called upon initialization of the property. + /// + protected virtual IList GetBounds() => new List(); + + /// + /// Obtains the native variable information of the method. + /// + /// The variables. + /// + /// This method is called upon initialization of the property. + /// + protected virtual IList GetVariables() => new List(); + + /// + public override void UpdateOffsets(in RelocationParameters parameters) + { + base.UpdateOffsets(in parameters); + _serialized = Serialize(); + _is32Bit = parameters.Is32Bit; + } + + /// + public override uint GetPhysicalSize() + { + _serialized ??= Serialize(); + return (uint) _serialized.Length; + } + + /// + public override void Write(IBinaryStreamWriter writer) + { + _serialized ??= Serialize(); + writer.WriteBytes(_serialized); + } + + private byte[] Serialize() + { + byte[] bounds = SerializeBounds(); + byte[] variables = SerializeVariables(); + + using var stream = new MemoryStream(); + var writer = new BinaryStreamWriter(stream); + + NativeFormat.EncodeUnsigned(writer, 0); // lookback + + var nibbleWriter = new NibbleWriter(writer); + nibbleWriter.Write3BitEncodedUInt((uint) bounds.Length); + nibbleWriter.Write3BitEncodedUInt((uint) variables.Length); + nibbleWriter.Flush(); + + writer.WriteBytes(bounds); + writer.WriteBytes(variables); + + return stream.ToArray(); + } + + private byte[] SerializeBounds() + { + if (Bounds.Count == 0) + return Array.Empty(); + + using var stream = new MemoryStream(); + var writer = new NibbleWriter(new BinaryStreamWriter(stream)); + + writer.Write3BitEncodedUInt((uint) Bounds.Count); + + uint nativeOffset = 0; + for (int i = 0; i < Bounds.Count; i++) + { + var bound = Bounds[i]; + + writer.Write3BitEncodedUInt(bound.NativeOffset - nativeOffset); + writer.Write3BitEncodedUInt(bound.ILOffset - DebugInfoBounds.EpilogOffset); + writer.Write3BitEncodedUInt((uint) bound.Attributes); + + nativeOffset = bound.NativeOffset; + } + + writer.Flush(); + return stream.ToArray(); + } + + private byte[] SerializeVariables() + { + if (Variables.Count == 0) + return Array.Empty(); + + using var stream = new MemoryStream(); + var writer = new NibbleWriter(new BinaryStreamWriter(stream)); + + writer.Write3BitEncodedUInt((uint) Variables.Count); + + for (int i = 0; i < Variables.Count; i++) + Variables[i].Write(_is32Bit ? MachineType.I386 : MachineType.Amd64, ref writer); + + writer.Flush(); + return stream.ToArray(); + } + + } + +} diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/DebugInfoAttributes.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/DebugInfoAttributes.cs new file mode 100644 index 000000000..5da7bac4f --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/DebugInfoAttributes.cs @@ -0,0 +1,42 @@ +using System; + +namespace AsmResolver.PE.DotNet.ReadyToRun +{ + /// + /// Provides members defining all possible attributes that can be assigned to a single + /// entry. + /// + [Flags] + public enum DebugInfoAttributes + { + /// + /// Indicates that no other options apply + /// + SourceTypeInvalid = 0x00, + + /// + /// Indicates the debugger asked for it. + /// + SequencePoint = 0x01, + + /// + /// Indicates the stack is empty here. + /// + StackEmpty = 0x02, + + /// + /// Indicates this is a call site. + /// + CallSite = 0x04, + + /// + /// Indicates an epilog endpoint. + /// + NativeEndOffsetUnknown = 0x08, + + /// + /// Indicates the actual instruction of a call. + /// + CallInstruction = 0x10 + } +} diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/DebugInfoBounds.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/DebugInfoBounds.cs new file mode 100644 index 000000000..f60d23303 --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/DebugInfoBounds.cs @@ -0,0 +1,97 @@ +using System; +using System.Diagnostics; + +namespace AsmResolver.PE.DotNet.ReadyToRun +{ + /// + /// Describes bounds within a precompiled method body and attaches additional semantics to it. + /// + [DebuggerDisplay("NativeOffset = {NativeOffset}, ILOffset = {ILOffset} ({Attributes})")] + public readonly struct DebugInfoBounds : IEquatable + { + /// + /// A special offset indicating the native code was not mapped to IL code. + /// + public const uint NoMappingOffset = 0xffffffff; + + /// + /// A special offset indicating the native code maps to the function's prologue. + /// + public const uint PrologOffset = 0xfffffffe; + + /// + /// A special offset indicating the native code maps to the function's epilogue. + /// + public const uint EpilogOffset = 0xfffffffd; + + /// + /// Creates a new bounds descriptor. + /// + /// The starting native offset. + /// The starting IL offset. + /// The attributes describing the semantics of the bounds. + public DebugInfoBounds(uint nativeOffset, uint ilOffset, DebugInfoAttributes attributes) + { + NativeOffset = nativeOffset; + ILOffset = ilOffset; + Attributes = attributes; + } + + /// + /// Gets the starting native offset of the bounds. + /// + public uint NativeOffset + { + get; + } + + /// + /// Gets the associated starting IL offset of the bounds. + /// + public uint ILOffset + { + get; + } + + /// + /// Gets the attributes assigned to the bounds. + /// + public DebugInfoAttributes Attributes + { + get; + } + + /// + public bool Equals(DebugInfoBounds other) + { + return NativeOffset == other.NativeOffset + && ILOffset == other.ILOffset + && Attributes == other.Attributes; + } + + /// + public override bool Equals(object? obj) + { + return obj is DebugInfoBounds other && Equals(other); + } + + /// + public override int GetHashCode() + { + unchecked + { + int hashCode = (int) NativeOffset; + hashCode = (hashCode * 397) ^ (int) ILOffset; + hashCode = (hashCode * 397) ^ (int) Attributes; + return hashCode; + } + } + + /// + public override string ToString() + { + return $"{nameof(NativeOffset)}: {NativeOffset:X4}, {nameof(ILOffset)}: IL_{ILOffset:X4} ({Attributes})"; + } + } + +} diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/DebugInfoSection.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/DebugInfoSection.cs new file mode 100644 index 000000000..d27a2050b --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/DebugInfoSection.cs @@ -0,0 +1,58 @@ +using System; +using System.Threading; +using AsmResolver.IO; + +namespace AsmResolver.PE.DotNet.ReadyToRun +{ + /// + /// Represents the ReadyToRun section containing additional debugging information for precompiled methods. + /// + public class DebugInfoSection : SegmentBase, IReadyToRunSection + { + private NativeArray? _entries; + + /// + public ReadyToRunSectionType Type => ReadyToRunSectionType.DebugInfo; + + /// + public virtual bool CanRead => false; + + /// + /// Gets an ordered collection of debug info entries stored in the section. + /// + public NativeArray Entries + { + get + { + if (_entries is null) + Interlocked.CompareExchange(ref _entries, GetEntries(), null); + return _entries; + } + } + + /// + /// Obtains the entries stored in the section. + /// + /// The entries. + /// + /// This method is called upon initialization of the property. + /// + protected virtual NativeArray GetEntries() => new(); + + /// + public virtual BinaryStreamReader CreateReader() => throw new InvalidOperationException(); + + /// + public override void UpdateOffsets(in RelocationParameters parameters) + { + base.UpdateOffsets(in parameters); + Entries.UpdateOffsets(in parameters); + } + + /// + public override uint GetPhysicalSize() => Entries.GetPhysicalSize(); + + /// + public override void Write(IBinaryStreamWriter writer) => Entries.Write(writer); + } +} diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/DebugInfoVariable.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/DebugInfoVariable.cs new file mode 100644 index 000000000..b23449dfa --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/DebugInfoVariable.cs @@ -0,0 +1,128 @@ +using System; +using AsmResolver.PE.File.Headers; + +namespace AsmResolver.PE.DotNet.ReadyToRun +{ + /// + /// Provides debugging information about a single native variable in a precompiled method. + /// + public readonly struct DebugInfoVariable : IEquatable + { + /// + /// The special VARARGS handle variable index. + /// + public const uint VarArgsHandle = 0xFFFFFFFF; + + /// + /// The special return buffer variable index. + /// + public const uint ReturnBuffer = 0xFFFFFFFE; + + /// + /// The special type context variable index. + /// + public const uint TypeContext = 0xFFFFFFFD; + + internal const uint Unknown = 0xFFFFFFFC; + + /// + /// Creates new debugging information for the specified native variable. + /// + /// The start offset the variable is live at. + /// The (exclusive) end offset the variable is live at. + /// The index of the variable. + /// The location of the variable. + public DebugInfoVariable(uint startOffset, uint endOffset, uint index, DebugInfoVariableLocation location) + { + StartOffset = startOffset; + EndOffset = endOffset; + Index = index; + Location = location; + } + + /// + /// Gets the start offset the variable is live at. + /// + public uint StartOffset + { + get; + } + + /// + /// Gets the (exclusive) end offset the variable is live at. + /// + public uint EndOffset + { + get; + } + + /// + /// Gets the index of the variable. + /// + public uint Index + { + get; + } + + /// + /// Gets the location of the variable. + /// + public DebugInfoVariableLocation Location + { + get; + } + + internal static DebugInfoVariable FromReader(PEReaderContext context, ref NibbleReader reader) + { + uint start = reader.Read3BitEncodedUInt(); + uint end = start + reader.Read3BitEncodedUInt(); + uint index = reader.Read3BitEncodedUInt() + Unknown; + var location = DebugInfoVariableLocation.FromReader(context, ref reader); + + return new DebugInfoVariable(start, end, index, location); + } + + internal void Write(MachineType machineType, ref NibbleWriter writer) + { + writer.Write3BitEncodedUInt(StartOffset); + writer.Write3BitEncodedUInt(EndOffset - StartOffset); + writer.Write3BitEncodedUInt(Index - Unknown); + Location.Write(machineType, ref writer); + } + + /// + public bool Equals(DebugInfoVariable other) + { + return StartOffset == other.StartOffset + && EndOffset == other.EndOffset + && Index == other.Index + && Location.Equals(other.Location); + } + + /// + public override bool Equals(object? obj) + { + return obj is DebugInfoVariable other && Equals(other); + } + + /// + public override int GetHashCode() + { + unchecked + { + int hashCode = (int) StartOffset; + hashCode = (hashCode * 397) ^ (int) EndOffset; + hashCode = (hashCode * 397) ^ (int) Index; + hashCode = (hashCode * 397) ^ Location.GetHashCode(); + return hashCode; + } + } + + /// + public override string ToString() + { + return $"[{StartOffset:X4}, {EndOffset:X4}), {nameof(Index)}: {Index}, {nameof(Location)}: {Location}"; + } + } + +} diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/DebugInfoVariableLocation.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/DebugInfoVariableLocation.cs new file mode 100644 index 000000000..f86ec73b5 --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/DebugInfoVariableLocation.cs @@ -0,0 +1,255 @@ +using System; +using AsmResolver.PE.File.Headers; + +namespace AsmResolver.PE.DotNet.ReadyToRun +{ + /// + /// Describes the location of a native variable in a precompiled method. + /// + public readonly struct DebugInfoVariableLocation : IEquatable + { + /// + /// Creates a new location description for a variable. + /// + /// The type of location. + /// The first parameter further specifying the location. + public DebugInfoVariableLocation(DebugInfoVariableLocationType type, uint data1) + { + Type = type; + Data1 = data1; + Data2 = 0; + Data3 = 0; + } + + /// + /// Creates a new location description for a variable. + /// + /// The type of location. + /// The first parameter further specifying the location. + /// The second parameter further specifying the location. + public DebugInfoVariableLocation(DebugInfoVariableLocationType type, uint data1, uint data2) + { + Type = type; + Data1 = data1; + Data2 = data2; + Data3 = 0; + } + + /// + /// Creates a new location description for a variable. + /// + /// The type of location. + /// The first parameter further specifying the location. + /// The second parameter further specifying the location. + /// The third parameter further specifying the location. + public DebugInfoVariableLocation(DebugInfoVariableLocationType type, uint data1, uint data2, uint data3) + { + Type = type; + Data1 = data1; + Data2 = data2; + Data3 = data3; + } + + /// + /// Gets the type of location. + /// + public DebugInfoVariableLocationType Type + { + get; + } + + /// + /// Gets the first parameter further specifying the location. + /// + public uint Data1 + { + get; + } + + /// + /// Gets the second parameter further specifying the location. + /// + public uint Data2 + { + get; + } + + /// + /// Gets the third parameter further specifying the location. + /// + public uint Data3 + { + get; + } + + internal static DebugInfoVariableLocation FromReader(PEReaderContext context, ref NibbleReader reader) + { + var type = (DebugInfoVariableLocationType) reader.Read3BitEncodedUInt(); + + uint data1 = 0, data2 = 0, data3 = 0; + + switch (type) + { + case DebugInfoVariableLocationType.Register: + case DebugInfoVariableLocationType.RegisterFP: + case DebugInfoVariableLocationType.RegisterByReference: + case DebugInfoVariableLocationType.FPStack: + case DebugInfoVariableLocationType.FixedVA: + data1 = reader.Read3BitEncodedUInt(); + break; + + case DebugInfoVariableLocationType.Stack: + case DebugInfoVariableLocationType.StackByReference: + data1 = reader.Read3BitEncodedUInt(); + data2 = ToStackOffset(reader.Read3BitEncodedUInt()); + break; + + case DebugInfoVariableLocationType.RegisterRegister: + data1 = reader.Read3BitEncodedUInt(); + data2 = reader.Read3BitEncodedUInt(); + break; + + case DebugInfoVariableLocationType.RegisterStack: + data1 = reader.Read3BitEncodedUInt(); + data2 = reader.Read3BitEncodedUInt(); + data3 = ToStackOffset(reader.Read3BitEncodedUInt()); + break; + + case DebugInfoVariableLocationType.StackRegister: + data1 = ToStackOffset(reader.Read3BitEncodedUInt()); + data2 = reader.Read3BitEncodedUInt(); + data3 = reader.Read3BitEncodedUInt(); + break; + + case DebugInfoVariableLocationType.Stack2: + data1 = reader.Read3BitEncodedUInt(); + data2 = ToStackOffset(reader.Read3BitEncodedUInt()); + break; + + default: + throw new ArgumentOutOfRangeException(); + } + + return new DebugInfoVariableLocation(type, data1, data2, data3); + + uint ToStackOffset(uint value) + { + if (context.File.FileHeader.Machine == MachineType.I386) + return value * sizeof(uint); + + return value; + } + } + + internal void Write(MachineType machineType, ref NibbleWriter writer) + { + writer.Write3BitEncodedUInt((uint) Type); + + switch (Type) + { + case DebugInfoVariableLocationType.Register: + case DebugInfoVariableLocationType.RegisterFP: + case DebugInfoVariableLocationType.RegisterByReference: + case DebugInfoVariableLocationType.FPStack: + case DebugInfoVariableLocationType.FixedVA: + writer.Write3BitEncodedUInt(Data1); + break; + + case DebugInfoVariableLocationType.Stack: + case DebugInfoVariableLocationType.StackByReference: + writer.Write3BitEncodedUInt(Data1); + writer.Write3BitEncodedUInt(EncodeStackOffset(Data2)); + break; + + case DebugInfoVariableLocationType.RegisterRegister: + writer.Write3BitEncodedUInt(Data1); + writer.Write3BitEncodedUInt(Data2); + break; + + case DebugInfoVariableLocationType.RegisterStack: + writer.Write3BitEncodedUInt(Data1); + writer.Write3BitEncodedUInt(Data2); + writer.Write3BitEncodedUInt(EncodeStackOffset(Data3)); + break; + + case DebugInfoVariableLocationType.StackRegister: + writer.Write3BitEncodedUInt(EncodeStackOffset(Data1)); + writer.Write3BitEncodedUInt(Data2); + writer.Write3BitEncodedUInt(Data3); + break; + + case DebugInfoVariableLocationType.Stack2: + writer.Write3BitEncodedUInt(Data1); + writer.Write3BitEncodedUInt(EncodeStackOffset(Data2)); + break; + + default: + throw new ArgumentOutOfRangeException(); + } + + return; + + uint EncodeStackOffset(uint value) + { + if (machineType == MachineType.I386) + return value / sizeof(uint); + return value; + } + } + + /// + public bool Equals(DebugInfoVariableLocation other) + { + return Type == other.Type + && Data1 == other.Data1 + && Data2 == other.Data2 + && Data3 == other.Data3; + } + + /// + public override bool Equals(object? obj) + { + return obj is DebugInfoVariableLocation other && Equals(other); + } + + /// + public override int GetHashCode() + { + unchecked + { + int hashCode = (int) Type; + hashCode = (hashCode * 397) ^ (int) Data1; + hashCode = (hashCode * 397) ^ (int) Data2; + hashCode = (hashCode * 397) ^ (int) Data3; + return hashCode; + } + } + + /// + public override string ToString() + { + switch (Type) + { + case DebugInfoVariableLocationType.Register: + case DebugInfoVariableLocationType.RegisterFP: + case DebugInfoVariableLocationType.RegisterByReference: + case DebugInfoVariableLocationType.FPStack: + case DebugInfoVariableLocationType.FixedVA: + return $"{Type} {Data1:X}"; + + case DebugInfoVariableLocationType.Stack: + case DebugInfoVariableLocationType.StackByReference: + case DebugInfoVariableLocationType.RegisterRegister: + case DebugInfoVariableLocationType.Stack2: + return $"{Type} {Data1:X}, {Data2:X}"; + + case DebugInfoVariableLocationType.RegisterStack: + case DebugInfoVariableLocationType.StackRegister: + return $"{Type} {Data1:X}, {Data2:X}, {Data3:X}"; + + default: + throw new ArgumentOutOfRangeException(); + } + } + } +} diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/DebugInfoVariableLocationType.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/DebugInfoVariableLocationType.cs new file mode 100644 index 000000000..65e76b6cf --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/DebugInfoVariableLocationType.cs @@ -0,0 +1,63 @@ +namespace AsmResolver.PE.DotNet.ReadyToRun +{ + /// + /// Provides members defining all possible locations a native variable in a precompiled method can be placed at. + /// + public enum DebugInfoVariableLocationType + { + /// + /// variable is in a register. + /// + Register, + + /// + /// address of the variable is in a register. + /// + RegisterByReference, + + /// + /// variable is in an fp register. + /// + RegisterFP, + + /// + /// variable is on the stack (memory addressed relative to the frame-pointer). + /// + Stack, + + /// + /// address of the variable is on the stack (memory addressed relative to the frame-pointer). + /// + StackByReference, + + /// + /// variable lives in two registers. + /// + RegisterRegister, + + /// + /// variable lives partly in a register and partly on the stack. + /// + RegisterStack, + + /// + /// reverse of VLT_REG_STK. + /// + StackRegister, + + /// + /// variable lives in two slots on the stack. + /// + Stack2, + + /// + /// variable lives on the floating-point stack. + /// + FPStack, + + /// + /// variable is a fixed argument in a varargs function (relative to VARARGS_HANDLE). + /// + FixedVA, + } +} diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/DefaultReadyToRunSectionReader.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/DefaultReadyToRunSectionReader.cs new file mode 100644 index 000000000..f1e95f821 --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/DefaultReadyToRunSectionReader.cs @@ -0,0 +1,26 @@ +using AsmResolver.IO; +using AsmResolver.PE.File.Headers; +using static AsmResolver.PE.DotNet.ReadyToRun.ReadyToRunSectionType; + +namespace AsmResolver.PE.DotNet.ReadyToRun +{ + /// + /// Provides a default implementation for the interface. + /// + public class DefaultReadyToRunSectionReader : IReadyToRunSectionReader + { + /// + public IReadyToRunSection ReadSection(PEReaderContext context, ReadyToRunSectionType type, ref BinaryStreamReader reader) + { + return type switch + { + CompilerIdentifier => new CompilerIdentifierSection(reader.ReadAsciiString()), + ImportSections => new SerializedImportSectionsSection(context, ref reader), + RuntimeFunctions when context.File.FileHeader.Machine == MachineType.Amd64 => new SerializedX64RuntimeFunctionsSection(context, ref reader), + MethodDefEntryPoints => new SerializedMethodEntryPointsSection(ref reader), + ReadyToRunSectionType.DebugInfo => new SerializedDebugInfoSection(context, reader), + _ => new CustomReadyToRunSection(type, reader.ReadSegment(reader.Length)) + }; + } + } +} diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/IReadyToRunSection.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/IReadyToRunSection.cs new file mode 100644 index 000000000..3694aa6c5 --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/IReadyToRunSection.cs @@ -0,0 +1,34 @@ +using System; +using AsmResolver.IO; + +namespace AsmResolver.PE.DotNet.ReadyToRun +{ + /// + /// Represents a single section within a ReadyToRun directory of a .NET module. + /// + public interface IReadyToRunSection : ISegment + { + /// + /// Gets the type of the ReadyToRun section. + /// + ReadyToRunSectionType Type + { + get; + } + + /// + /// Indicates whether the raw contents of the section can be read using a . + /// + public bool CanRead + { + get; + } + + /// + /// Creates a binary reader that reads the raw contents of the ReadyToRun section. + /// + /// The reader. + /// Occurs when is false. + BinaryStreamReader CreateReader(); + } +} diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/IReadyToRunSectionReader.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/IReadyToRunSectionReader.cs new file mode 100644 index 000000000..26d0ef231 --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/IReadyToRunSectionReader.cs @@ -0,0 +1,19 @@ +using AsmResolver.IO; + +namespace AsmResolver.PE.DotNet.ReadyToRun +{ + /// + /// Provides methods for parsing individual sections in a ReadyToRun directory of a .NET module. + /// + public interface IReadyToRunSectionReader + { + /// + /// Parses a single ReadyToRun section. + /// + /// The context in which the reader is situated in. + /// The type of section to read. + /// The input stream containing the raw binary contents of the section. + /// The parsed section. + IReadyToRunSection ReadSection(PEReaderContext context, ReadyToRunSectionType type, ref BinaryStreamReader reader); + } +} diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/ImportSection.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/ImportSection.cs new file mode 100644 index 000000000..ee3a99b79 --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/ImportSection.cs @@ -0,0 +1,125 @@ +using System.Diagnostics; +using System.Threading; +using AsmResolver.Collections; +using AsmResolver.IO; +using AsmResolver.PE.File.Headers; + +namespace AsmResolver.PE.DotNet.ReadyToRun +{ + /// + /// Represents a single import section in a ReadyToRun ImportSections section. + /// + [DebuggerDisplay("{Type}, Slots = {Slots.Count} ({Attributes})")] + public class ImportSection : IWritable + { + internal const uint ImportSectionSize = + DataDirectory.DataDirectorySize // Section + + sizeof(ImportSectionAttributes) // Flags + + sizeof(ImportSectionType) // Type + + sizeof(uint) // Signatures + + sizeof(uint) // AuxiliaryData + ; + + private ReferenceTable? _slots; + private ReferenceTable? _signatures; + + /// + /// Gets a collection of slots stored in the import section. + /// + public ReferenceTable Slots + { + get + { + if (_slots is null) + Interlocked.CompareExchange(ref _slots, GetSlots(), null); + return _slots; + } + } + + /// + /// Gets or sets attributes associated to the section. + /// + public ImportSectionAttributes Attributes + { + get; + set; + } + + /// + /// Gets or sets the type of slots stored in . + /// + public ImportSectionType Type + { + get; + set; + } + + /// + /// Gets or sets the size in bytes of a single slot in . + /// + /// + /// Valid values are either 4 or 8. + /// + public byte EntrySize + { + get; + set; + } + + /// + /// Gets a collection of signatures associated with each slot in stored in the import + /// section (if available). + /// + public ReferenceTable Signatures + { + get + { + if (_signatures is null) + Interlocked.CompareExchange(ref _signatures, GetSignatures(), null); + return _signatures; + } + } + + /// + /// Gets a pointer to auxiliary data attached to this section, if available. + /// + public ISegmentReference AuxiliaryData + { + get; + set; + } = SegmentReference.Null; + + /// + /// Obtains the slots stored in this section. + /// + /// The slots. + /// + /// This method is called upon initialization of the property. + /// + protected virtual ReferenceTable GetSlots() => new(ReferenceTableAttributes.Va | ReferenceTableAttributes.Adaptive); + + /// + /// Obtains the signatures stored in this section. + /// + /// The signatures. + /// + /// This method is called upon initialization of the property. + /// + protected virtual ReferenceTable GetSignatures() => new(ReferenceTableAttributes.Rva | ReferenceTableAttributes.Force32Bit); + + /// + public uint GetPhysicalSize() => ImportSectionSize; + + /// + public void Write(IBinaryStreamWriter writer) + { + writer.WriteUInt32(Slots.Rva); + writer.WriteUInt32(Slots.GetPhysicalSize()); + writer.WriteUInt16((ushort) Attributes); + writer.WriteByte((byte) Type); + writer.WriteByte(EntrySize); + writer.WriteUInt32(Signatures.Count == 0 ? 0 : Signatures.Rva); + writer.WriteUInt32(AuxiliaryData.Rva); + } + } +} diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/ImportSectionAttributes.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/ImportSectionAttributes.cs new file mode 100644 index 000000000..fc1b7defb --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/ImportSectionAttributes.cs @@ -0,0 +1,26 @@ +using System; + +namespace AsmResolver.PE.DotNet.ReadyToRun +{ + /// + /// Provides members describing all possible attributes that can be attached to a . + /// + [Flags] + public enum ImportSectionAttributes : ushort + { + /// + /// Indicates no special attributes were assigned. + /// + None = 0, + + /// + /// Indicates the slots in the section have to be initialized at image load time. + /// + Eager = 1, + + /// + /// Indicates the slots contain pointers to code. + /// + PCode = 4 + } +} diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/ImportSectionType.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/ImportSectionType.cs new file mode 100644 index 000000000..e9264632b --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/ImportSectionType.cs @@ -0,0 +1,28 @@ +namespace AsmResolver.PE.DotNet.ReadyToRun +{ + /// + /// Provides members describing all possible import section types. + /// + public enum ImportSectionType : byte + { + /// + /// Indicates the type of the section was unspecified. + /// + Unknown = 0, + + /// + /// Indicates the section contains dispatcher stubs. + /// + StubDispatch = 2, + + /// + /// Indicates the section contains string handles. + /// + StringHandle = 3, + + /// + /// Indicates the section contains IL body fixups. + /// + ILBodyFixups = 7 + } +} diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/ImportSectionsSection.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/ImportSectionsSection.cs new file mode 100644 index 000000000..1d90f7415 --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/ImportSectionsSection.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using AsmResolver.IO; + +namespace AsmResolver.PE.DotNet.ReadyToRun +{ + /// + /// Represents a section in the ReadyToRun directory of a .NET module that contains import sections. + /// + public class ImportSectionsSection : SegmentBase, IReadyToRunSection + { + private IList? _sections; + + /// + public ReadyToRunSectionType Type => ReadyToRunSectionType.ImportSections; + + /// + /// Gets the import sub-sections stored in the section. + /// + public IList Sections + { + get + { + if (_sections is null) + Interlocked.CompareExchange(ref _sections, GetSections(), null); + return _sections; + } + } + + /// + /// Obtains the sub sections stored in the section. + /// + /// The sections. + /// + /// This method is called upon initialization of the property. + /// + protected virtual IList GetSections() => new List(); + + /// + public virtual bool CanRead => false; + + /// + public virtual BinaryStreamReader CreateReader() => throw new InvalidOperationException(); + + /// + public override uint GetPhysicalSize() => (uint) Sections.Count * ImportSection.ImportSectionSize; + + /// + public override void Write(IBinaryStreamWriter writer) + { + for (int i = 0; i < Sections.Count; i++) + Sections[i].Write(writer); + } + } +} diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/MethodEntryPoint.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/MethodEntryPoint.cs new file mode 100644 index 000000000..e0a087d07 --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/MethodEntryPoint.cs @@ -0,0 +1,174 @@ +using System.Collections.Generic; +using System.IO; +using AsmResolver.IO; + +namespace AsmResolver.PE.DotNet.ReadyToRun +{ + /// + /// Provides information about a native entry point for a managed method that was compiled ahead-of-time. + /// + public class MethodEntryPoint : SegmentBase + { + private byte[]? _serialized; + + /// + /// Constructs a new entry point for a method. + /// + /// The index of the RUNTIME_FUNCTION this method starts at. + public MethodEntryPoint(uint functionIndex) + { + RuntimeFunctionIndex = functionIndex; + } + + /// + /// Gets or sets the index to the RUNTIME_FUNCTION the method starts at. + /// + public uint RuntimeFunctionIndex + { + get; + set; + } + + /// + /// Gets a collection of fixups that need to be applied before the method can be executed natively. + /// + public IList Fixups + { + get; + } = new List(); + + /// + /// Reads a single method entry point metadata segment from the provided input stream. + /// + /// The input stream. + /// The read entry point metadata + public static MethodEntryPoint FromReader(ref BinaryStreamReader reader) + { + ulong offset = reader.Offset; + uint rva = reader.Rva; + + uint header = NativeFormat.DecodeUnsigned(ref reader); + bool hasFixups = (header & 1) != 0; + + MethodEntryPoint result; + if (!hasFixups) + { + result = new MethodEntryPoint(header >> 1); + } + else + { + result = new MethodEntryPoint(header >> 2); + ReadFixups(result, reader); + } + + result.Offset = offset; + result.Rva = rva; + + return result; + } + + private static void ReadFixups(MethodEntryPoint entryPoint, BinaryStreamReader reader) + { + var nibbleReader = new NibbleReader(reader); + + uint importIndex = nibbleReader.Read3BitEncodedUInt(); + while (true) + { + uint slotIndex = nibbleReader.Read3BitEncodedUInt(); + while (true) + { + entryPoint.Fixups.Add(new MethodFixup(importIndex, slotIndex)); + + uint slotDelta = nibbleReader.Read3BitEncodedUInt(); + if (slotDelta == 0) + break; + + slotIndex += slotDelta; + } + + uint importDelta = nibbleReader.Read3BitEncodedUInt(); + if (importDelta == 0) + break; + + importIndex += importDelta; + } + } + + private uint GetHeader() => Fixups.Count > 0 + ? RuntimeFunctionIndex << 2 | 1 + : RuntimeFunctionIndex << 1; + + /// + public override void UpdateOffsets(in RelocationParameters parameters) + { + base.UpdateOffsets(in parameters); + _serialized = Serialize(); + } + + /// + public override uint GetPhysicalSize() + { + _serialized ??= Serialize(); + return (uint) _serialized.Length; + } + + /// + public override void Write(IBinaryStreamWriter writer) + { + _serialized ??= Serialize(); + writer.WriteBytes(_serialized); + } + + private byte[] Serialize() + { + using var stream = new MemoryStream(); + var writer = new BinaryStreamWriter(stream); + + // Write header. + NativeFormat.EncodeUnsigned(writer, GetHeader()); + + if (Fixups.Count == 0) + return stream.ToArray(); + + var nibbleWriter = new NibbleWriter(writer); + + // Write fixups. + uint lastImportIndex = 0; + uint lastSlotIndex = 0; + for (int i = 0; i < Fixups.Count; i++) + { + var fixup = Fixups[i]; + + uint importDelta = fixup.ImportIndex - lastImportIndex; + if (importDelta != 0 || i == 0) + { + // We're entering a new chunk of fixups with a different import index. + // Close of previous import chunk with a 0 slot delta. + if (i > 0) + nibbleWriter.Write3BitEncodedUInt(0); + + // Start new import chunk. + nibbleWriter.Write3BitEncodedUInt(importDelta); + lastSlotIndex = 0; + } + + // Write current slot. + uint slotDelta = fixup.SlotIndex - lastSlotIndex; + nibbleWriter.Write3BitEncodedUInt(slotDelta); + + lastImportIndex = fixup.ImportIndex; + lastSlotIndex = fixup.SlotIndex; + } + + // Close off last slot list. + nibbleWriter.Write3BitEncodedUInt(0); + + // Close off last import list. + nibbleWriter.Write3BitEncodedUInt(0); + + nibbleWriter.Flush(); + + return stream.ToArray(); + } + } +} diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/MethodEntryPointsSection.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/MethodEntryPointsSection.cs new file mode 100644 index 000000000..7da2c46e3 --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/MethodEntryPointsSection.cs @@ -0,0 +1,60 @@ +using System; +using System.Threading; +using AsmResolver.IO; + +namespace AsmResolver.PE.DotNet.ReadyToRun +{ + /// + /// Represents a section in a ReadyToRun directory of a .NET module containing the native entry points and fixups + /// of all the ahead-of-time compiled managed methods. + /// + public class MethodEntryPointsSection : SegmentBase, IReadyToRunSection + { + private NativeArray? _entryPoints; + + /// + public ReadyToRunSectionType Type => ReadyToRunSectionType.MethodDefEntryPoints; + + /// + public virtual bool CanRead => false; + + /// + /// Gets a collection of entry points stored in the section. The index of the element corresponds to the method + /// as specified in the method definition table in the tables stream of the .NET module. + /// + public NativeArray EntryPoints + { + get + { + if (_entryPoints is null) + Interlocked.CompareExchange(ref _entryPoints, GetEntryPoints(), null); + return _entryPoints; + } + } + + /// + /// Obtains the entry points stored in the section. + /// + /// The entry points. + /// + /// This method is called upon initialization of the property. + /// + protected virtual NativeArray GetEntryPoints() => new(); + + /// + public virtual BinaryStreamReader CreateReader() => throw new InvalidOperationException(); + + /// + public override void UpdateOffsets(in RelocationParameters parameters) + { + base.UpdateOffsets(in parameters); + EntryPoints.UpdateOffsets(in parameters); + } + + /// + public override uint GetPhysicalSize() => EntryPoints.GetPhysicalSize(); + + /// + public override void Write(IBinaryStreamWriter writer) => EntryPoints.Write(writer); + } +} diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/MethodFixup.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/MethodFixup.cs new file mode 100644 index 000000000..5136ec107 --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/MethodFixup.cs @@ -0,0 +1,63 @@ +using System; +using System.Diagnostics; + +namespace AsmResolver.PE.DotNet.ReadyToRun +{ + /// + /// Describes a fixup that needs to be applied to a native method body before it can be executed. + /// + [DebuggerDisplay("ImportIndex = {ImportIndex}, SlotIndex = {SlotIndex}")] + public readonly struct MethodFixup : IEquatable + { + /// + /// Constructs a new method fixup. + /// + /// The index to the import section containing the fixup. + /// The index of the fixup slot within the import section to apply. + public MethodFixup(uint importIndex, uint slotIndex) + { + ImportIndex = importIndex; + SlotIndex = slotIndex; + } + + /// + /// Gets the index to the import section containing the fixup. + /// + public uint ImportIndex + { + get; + } + + /// + /// Gets the index of the fixup slot within the import section to apply. + /// + public uint SlotIndex + { + get; + } + + /// + public bool Equals(MethodFixup other) + { + return ImportIndex == other.ImportIndex && SlotIndex == other.SlotIndex; + } + + /// + public override bool Equals(object? obj) + { + return obj is MethodFixup other && Equals(other); + } + + /// + public override int GetHashCode() + { + unchecked + { + return ((int) ImportIndex * 397) ^ (int) SlotIndex; + } + } + + /// + public override string ToString() => $"ImportIndex = {ImportIndex}, SlotIndex = {SlotIndex}"; + } +} diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/NativeArray.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/NativeArray.cs new file mode 100644 index 000000000..3592367fc --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/NativeArray.cs @@ -0,0 +1,283 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using AsmResolver.IO; + +namespace AsmResolver.PE.DotNet.ReadyToRun +{ + /// + /// Represents a sparse list of elements stored in the native file format as specified by the ReadyToRun file format. + /// + /// The type of elements to store in the array. + public class NativeArray : Collection, ISegment + where T : IWritable + { + private readonly List _roots = new(); + private uint _entryIndexSize = 2; + private uint _totalSize; + + /// + public bool CanUpdateOffsets => true; + + /// + public ulong Offset + { + get; + private set; + } + + /// + public uint Rva + { + get; + private set; + } + + private uint Header => (uint) (Items.Count << 2) | _entryIndexSize; + + /// + /// Reads a sparse array in the native file format from the provided input stream. + /// + /// The input stream. + /// The function to use for reading individual elements. + /// The read array. + public static NativeArray FromReader(BinaryStreamReader reader, Func readElement) + { + var result = new NativeArray(); + + uint header = NativeFormat.DecodeUnsigned(ref reader); + int count = (int) (header >> 2); + result._entryIndexSize = (byte) (header & 3); + reader = reader.ForkAbsolute(reader.Offset); + + for (int i = 0; i < count; i++) + { + result.Add(NativeFormat.TryGetArrayElement(reader, result._entryIndexSize, i, out var elementReader) + ? readElement(elementReader) + : default); + } + + return result; + } + + private void RebuildTree() + { + _roots.Clear(); + + for (int i = 0; i < Items.Count; i++) + { + var item = Items[i]; + if (item is not null) + InsertNode(i, item); + } + + // TODO: optimize for entry size. + _entryIndexSize = 2; + } + + private void UpdateTreeOffsets(in RelocationParameters parameters) + { + Offset = parameters.Offset; + Rva = parameters.Rva; + + var current = parameters; + + current.Advance(NativeFormat.GetEncodedUnsignedSize(Header)); + current.Advance((uint) _roots.Count * (1u << (int) _entryIndexSize)); + + foreach (var root in _roots) + { + root.UpdateOffsets(current); + current.Advance(root.GetPhysicalSize()); + } + + _totalSize = (uint) (current.Offset - parameters.Offset); + } + + private void InsertNode(int index, T? value) + { + int rootIndex = index / NativeFormat.ArrayBlockSize; + while (rootIndex >= _roots.Count) + _roots.Add(new Node(NativeFormat.ArrayBlockSize >> 1)); + + // TODO: truncate trees. + + var current = _roots[rootIndex]; + + uint bit = NativeFormat.ArrayBlockSize >> 1; + while (bit > 0) + { + if ((index & bit) != 0) + { + current.Right ??= new Node(current.Depth >> 1); + current = current.Right; + } + else + { + current.Left ??= new Node(current.Depth >> 1); + current = current.Left; + } + + bit >>= 1; + } + + current.Index = (uint) index; + current.Value = value; + } + + /// + public void UpdateOffsets(in RelocationParameters parameters) + { + RebuildTree(); + UpdateTreeOffsets(parameters); + } + + /// + public uint GetPhysicalSize() => _totalSize; + + /// + public uint GetVirtualSize() => GetPhysicalSize(); + + /// + public void Write(IBinaryStreamWriter writer) + { + uint header = Header; + uint headerSize = NativeFormat.GetEncodedUnsignedSize(header); + NativeFormat.EncodeUnsigned(writer, header); + + foreach (var root in _roots) + WriteRootNodeHeader(writer, root, headerSize); + + foreach (var root in _roots) + root.Write(writer); + } + + private void WriteRootNodeHeader(IBinaryStreamWriter writer, Node root, uint headerSize) + { + uint offset = (uint) (root.Offset - headerSize - Offset); + switch (_entryIndexSize) + { + case 0: + writer.WriteByte((byte) offset); + break; + + case 1: + writer.WriteUInt16((ushort) offset); + break; + + case 2: + writer.WriteUInt32(offset); + break; + + default: + throw new ArgumentOutOfRangeException(nameof(_entryIndexSize)); + } + } + + private sealed class Node : SegmentBase + { + public uint Index; + public T? Value; + public Node? Left; + public Node? Right; + public readonly int Depth; + + private uint _size; + + public Node(int depth, uint index = 0, T? value = default) + { + Depth = depth; + Index = index; + Value = value; + } + + public uint Header + { + get + { + uint tag = 0; + if (Left is not null) + tag |= 0b01; + if (Right is not null) + tag |= 0b10; + + uint value; + if (Right is not null) + value = (uint) (Right.Offset - Offset); + else if (Left is not null) + value = 0; + else + value = Index; + + return tag | (value << 2); + } + } + + [MemberNotNullWhen(true, nameof(Value))] + public bool IsLeaf => Left is null && Right is null; + + public override void UpdateOffsets(in RelocationParameters parameters) + { + base.UpdateOffsets(in parameters); + + var current = parameters; + + if (Depth > 0) + { + // TODO: optimize header for size. + // current.Advance(NativeArrayView.GetEncodedUnsignedSize(Header)); + current.Advance(5); + } + + if (IsLeaf) + { + if (Value is ISegment segment) + segment.UpdateOffsets(current); + current.Advance(Value.GetPhysicalSize()); + } + else + { + if (Left is not null) + { + Left.UpdateOffsets(current); + current.Advance(Left.GetPhysicalSize()); + } + + if (Right is not null) + { + Right.UpdateOffsets(current); + current.Advance(Right.GetPhysicalSize()); + } + } + + _size = (uint) (current.Offset - parameters.Offset); + } + + public override uint GetPhysicalSize() => _size; + + public override void Write(IBinaryStreamWriter writer) + { + System.Diagnostics.Debug.Assert(writer.Offset == Offset); + + if (Depth > 0) + { + // TODO: optimize header for size. + // NativeArrayView.EncodeUnsigned(writer, Header); + writer.WriteByte(0b00001111); + writer.WriteUInt32(Header); + } + + if (IsLeaf) + { + Value.Write(writer); + } + else + { + Left?.Write(writer); + Right?.Write(writer); + } + } + } + } +} diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/NativeFormat.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/NativeFormat.cs new file mode 100644 index 000000000..926603a67 --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/NativeFormat.cs @@ -0,0 +1,156 @@ +using System; +using AsmResolver.IO; + +namespace AsmResolver.PE.DotNet.ReadyToRun; + +internal static class NativeFormat +{ + internal const int ArrayBlockSize = 16; + + public static uint GetEncodedUnsignedSize(uint value) => value switch + { + < 0b01111111 => 1, + < 0b00111111_111111111 => 2, + < 0b00011111_111111111_11111111 => 3, + < 0b00001111_111111111_11111111_11111111 => 4, + _ => 5 + }; + + public static uint DecodeUnsigned(ref BinaryStreamReader reader) + { + // Reference: https://github.com/dotnet/runtime/blob/163bc5800b469bcb86d59b77da21965916a0f4ce/src/coreclr/tools/Common/Internal/NativeFormat/NativeFormatReader.Primitives.cs#L66 + + uint value = 0; + + uint val = reader.ReadByte(); + if ((val & 1) == 0) + { + value = val >> 1; + } + else if ((val & 2) == 0) + { + value = (val >> 2) | ((uint) reader.ReadByte() << 6); + } + else if ((val & 4) == 0) + { + value = (val >> 3) + | ((uint) reader.ReadByte() << 5) + | ((uint) reader.ReadByte() << 13); + } + else if ((val & 8) == 0) + { + value = (val >> 4) + | ((uint) reader.ReadByte() << 4) + | ((uint) reader.ReadByte() << 12) + | ((uint) reader.ReadByte() << 20); + } + else if ((val & 16) == 0) + { + value = reader.ReadUInt32(); + } + else + { + throw new BadImageFormatException("Invalid encoding of unsigned integer."); + } + + return value; + } + + public static void EncodeUnsigned(IBinaryStreamWriter writer, uint value) + { + switch (GetEncodedUnsignedSize(value)) + { + case 1: + writer.WriteByte((byte) ((value & 0b01111111) << 1)); + break; + case 2: + writer.WriteByte((byte) ((value & 0b00111111) << 2 | 0b01)); + writer.WriteByte((byte) (value >> 6)); + break; + case 3: + writer.WriteByte((byte) ((value & 0b00011111) << 3 | 0b011)); + writer.WriteByte((byte) ((value >> 5) & 0xFF)); + writer.WriteByte((byte) ((value >> 13) & 0xFF)); + break; + case 4: + writer.WriteByte((byte) ((value & 0b00001111) << 4 | 0b0011)); + writer.WriteByte((byte) ((value >> 4) & 0xFF)); + writer.WriteByte((byte) ((value >> 12) & 0xFF)); + writer.WriteByte((byte) ((value >> 20) & 0xFF)); + break; + case 5: + writer.WriteByte(0b00001111); + writer.WriteUInt32(value); + break; + default: + throw new ArgumentOutOfRangeException(nameof(value)); + } + } + + public static bool TryGetArrayElement( + BinaryStreamReader reader, + uint entryIndexSize, + int index, + out BinaryStreamReader contents) + { + // Reference: https://github.com/dotnet/runtime/blob/5de6ca4ddd04b6fd25b93bf76f115aa209ab12ff/src/coreclr/vm/nativeformatreader.h#L370 + + switch (entryIndexSize) + { + case 0: + reader.RelativeOffset = (uint) index / ArrayBlockSize; + reader.RelativeOffset = reader.ReadByte(); + break; + + case 1: + reader.RelativeOffset = sizeof(ushort) * ((uint) index / ArrayBlockSize); + reader.RelativeOffset = reader.ReadUInt16(); + break; + + default: + reader.RelativeOffset = sizeof(uint) * ((uint) index / ArrayBlockSize); + reader.RelativeOffset = reader.ReadUInt32(); + break; + } + + for (uint bit = ArrayBlockSize >> 1; bit > 0; bit >>= 1) + { + var lookahead = reader.Fork(); + + uint value = DecodeUnsigned(ref lookahead); + if ((index & bit) != 0) + { + if ((value & 2) != 0) + { + reader.RelativeOffset += value >> 2; + continue; + } + } + else + { + if ((value & 1) != 0) + { + reader.RelativeOffset = lookahead.RelativeOffset; + continue; + } + } + + // Not found + if ((value & 3) == 0) + { + // Matching special leaf node? + if ((value >> 2) == (index & (ArrayBlockSize - 1))) + { + reader.RelativeOffset = lookahead.RelativeOffset; + break; + } + } + + contents = default; + return false; + } + + contents = reader; + return true; + } +} diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/NibbleReader.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/NibbleReader.cs new file mode 100644 index 000000000..44d70be55 --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/NibbleReader.cs @@ -0,0 +1,46 @@ +using AsmResolver.IO; + +namespace AsmResolver.PE.DotNet.ReadyToRun +{ + internal struct NibbleReader + { + private BinaryStreamReader _reader; + private byte? _buffer; + + public NibbleReader(BinaryStreamReader reader) + { + _reader = reader; + _buffer = null; + } + + public BinaryStreamReader BaseReader => _reader; + + public byte ReadNibble() + { + if (!_buffer.HasValue) + { + _buffer = _reader.ReadByte(); + return (byte) (_buffer & 0xF); + } + + byte value = (byte) ((_buffer >> 4) & 0xF); + _buffer = null; + return value; + } + + public uint Read3BitEncodedUInt() + { + uint result = 0; + byte nibble; + + do + { + nibble = ReadNibble(); + result <<= 3; + result |= (uint) (nibble & 0b0111); + } while ((nibble & 0b1000) != 0); + + return result; + } + } +} diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/NibbleWriter.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/NibbleWriter.cs new file mode 100644 index 000000000..51bd0e3d6 --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/NibbleWriter.cs @@ -0,0 +1,55 @@ +using AsmResolver.IO; + +namespace AsmResolver.PE.DotNet.ReadyToRun +{ + internal struct NibbleWriter + { + private readonly IBinaryStreamWriter _writer; + private byte? _buffer; + + public NibbleWriter(IBinaryStreamWriter writer) + { + _writer = writer; + _buffer = null; + } + + public void WriteNibble(byte nibble) + { + nibble &= 0xF; + + if (_buffer.HasValue) + { + _writer.WriteByte((byte) (_buffer.Value | nibble << 4)); + _buffer = null; + } + else + { + _buffer = nibble; + } + } + + public void Write3BitEncodedUInt(uint value) + { + int i = 0; + while ((value >> i) > 7) + i += 3; + + while (i > 0) + { + WriteNibble((byte)(((value >> i) & 0x7) | 0x8)); + i -= 3; + } + + WriteNibble((byte)(value & 0x7)); + } + + public void Flush() + { + if (_buffer is not null) + { + _writer.WriteByte(_buffer.Value); + _buffer = null; + } + } + } +} diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/ReadyToRunAttributes.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/ReadyToRunAttributes.cs new file mode 100644 index 000000000..50bec7928 --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/ReadyToRunAttributes.cs @@ -0,0 +1,54 @@ +using System; + +namespace AsmResolver.PE.DotNet.ReadyToRun +{ + /// + /// Provides members describing all possible flags that can be associated to a single ReadyToRun header. + /// + [Flags] + public enum ReadyToRunAttributes + { + /// + /// Indicates the original IL image was platform neutral. The platform neutrality is part of assembly name. + /// This flag can be used to reconstruct the full original assembly name. + /// + PlatformNeutralSource = 0x00000001, + + /// + /// Indicates the image represents a composite R2R file resulting from a combined compilation of a larger number + /// of input MSIL assemblies. + /// + Composite = 0x00000002, + + /// + /// + /// + Partial = 0x00000004, + + /// + /// Indicates PInvoke stubs compiled into image are non-shareable (no secret parameter). + /// + NonSharedPInvokeStubs = 0x00000008, + + /// + /// Indicates the input MSIL is embedded in the R2R image. + /// + EmbeddedMsil = 0x00000010, + + /// + /// Indicates this is a component assembly of a composite R2R image. + /// + Component = 0x00000020, + + /// + /// Indicates this R2R module has multiple modules within its version bubble (For versions before version 6.3, + /// all modules are assumed to possibly have this characteristic). + /// + MultiModuleVersionBubble = 0x00000040, + + /// + /// Indicates this R2R module has code in it that would not be naturally encoded into this module. + /// + UnrelatedR2RCode = 0x00000080, + } +} diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/ReadyToRunDirectory.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/ReadyToRunDirectory.cs new file mode 100644 index 000000000..d6d1c7a76 --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/ReadyToRunDirectory.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using AsmResolver.IO; +using AsmResolver.PE.File.Headers; + +namespace AsmResolver.PE.DotNet.ReadyToRun +{ + /// + /// Represents a managed native header of a .NET portable executable file that is in the ReadyToRun format. + /// + public class ReadyToRunDirectory : SegmentBase, IManagedNativeHeader + { + private IList? _sections; + + /// + public ManagedNativeHeaderSignature Signature => ManagedNativeHeaderSignature.Rtr; + + /// + /// Gets the major version of the file format that is used. + /// + public ushort MajorVersion + { + get; + set; + } = 5; + + /// + /// Gets the minor version of the file format that is used. + /// + public ushort MinorVersion + { + get; + set; + } = 4; + + /// + /// Gets or sets the flags associated with the ReadyToRun module. + /// + public ReadyToRunAttributes Attributes + { + get; + set; + } + + /// + /// Gets the individual sections referenced by the ReadyToRun directory. + /// + public IList Sections + { + get + { + if (_sections is null) + Interlocked.CompareExchange(ref _sections, GetSections(), null); + return _sections; + } + } + + /// + /// Obtains the sections referenced by the directory. + /// + /// The sections. + /// + /// This method is called upon initialization of the property. + /// + protected virtual IList GetSections() => new List(); + + /// + /// Gets a section by its section type. + /// + /// The type of section. + /// The section. + /// + /// Occurs when there is no section of the provided type present in the directory. + /// + public IReadyToRunSection GetSection(ReadyToRunSectionType type) + { + return TryGetSection(type, out var section) + ? section + : throw new ArgumentException($"Directory does not contain a section of type {type}."); + } + + /// + /// Gets a section by its section type. + /// + /// The type of section. + /// The section. + /// + /// Occurs when there is no section of the provided type present in the directory. + /// + public TSection GetSection() + where TSection : class, IReadyToRunSection + { + return TryGetSection(out var section) + ? section + : throw new ArgumentException($"Directory does not contain a section of type {typeof(TSection).Name}."); + } + + /// + /// Attempts to get a section by its section type. + /// + /// The type of the section. + /// The section, or null if none was found. + /// true if the section was found, false otherwise. + public bool TryGetSection(ReadyToRunSectionType type, [NotNullWhen(true)] out IReadyToRunSection? section) + { + for (int i = 0; i < Sections.Count; i++) + { + if (Sections[i].Type == type) + { + section = Sections[i]; + return true; + } + } + + section = null; + return false; + } + + /// + /// Attempts to get a section by its section type. + /// + /// The type of section. + /// The section, or null if none was found. + /// true if the section was found, false otherwise. + public bool TryGetSection([NotNullWhen(true)] out TSection? section) + where TSection : class, IReadyToRunSection + { + for (int i = 0; i < Sections.Count; i++) + { + if (Sections[i] is TSection s) + { + section = s; + return true; + } + } + + section = null; + return false; + } + + /// + public override uint GetPhysicalSize() + { + return sizeof(ManagedNativeHeaderSignature) // Signature + + sizeof(ushort) // MajorVersion + + sizeof(ushort) // MinorVersion + + sizeof(ReadyToRunAttributes) // Flags + + sizeof(uint) // NumberOfSections + + (uint) Sections.Count * (sizeof(ReadyToRunSectionType) + DataDirectory.DataDirectorySize) //Sections + ; + } + + /// + public override void Write(IBinaryStreamWriter writer) + { + writer.WriteUInt32((uint) Signature); + writer.WriteUInt16(MajorVersion); + writer.WriteUInt16(MinorVersion); + writer.WriteUInt32((uint) Attributes); + writer.WriteInt32(Sections.Count); + + foreach (var section in Sections) + { + writer.WriteUInt32((uint) section.Type); + writer.WriteUInt32(section.Rva); + writer.WriteUInt32(section.GetPhysicalSize()); + } + } + } +} diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/ReadyToRunFixupKind.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/ReadyToRunFixupKind.cs new file mode 100644 index 000000000..fb7162a87 --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/ReadyToRunFixupKind.cs @@ -0,0 +1,239 @@ +namespace AsmResolver.PE.DotNet.ReadyToRun +{ + /// + /// Provides members describing all possible types of imported fixups in a ReadyToRun assembly. + /// + public enum ReadyToRunFixupKind : byte + { + /// + /// Generic lookup using this; followed by the type signature and by the method signature. + /// + ThisObjDictionaryLookup = 0x07, + + /// + /// Type-based generic lookup for methods on instantiated types; followed by the TypeSpec signature. + /// + TypeDictionaryLookup = 0x08, + + /// + /// Generic method lookup; followed by the method spec signature. + /// + MethodDictionaryLookup = 0x09, + + /// + /// Pointer uniquely identifying the type to the runtime, followed by TypeSpec signature (see ECMA-335). + /// + TypeHandle = 0x10, + + /// + /// Pointer uniquely identifying the method to the runtime, followed by method signature (see below). + /// + MethodHandle = 0x11, + + /// + /// Pointer uniquely identifying the field to the runtime, followed by field signature (see below). + /// + FieldHandle = 0x12, + + /// + /// Method entrypoint or call, followed by method signature. + /// + MethodEntry = 0x13, + + /// + /// Method entrypoint or call, followed by MethodDef token (shortcut). + /// + MethodEntryDefToken = 0x14, + + /// + /// Method entrypoint or call, followed by MethodRef token (shortcut). + /// + MethodEntryRefToken = 0x15, + + /// + /// Virtual method entrypoint or call, followed by method signature. + /// + VirtualEntry = 0x16, + + /// + /// Virtual method entrypoint or call, followed by MethodDef token (shortcut) + /// + VirtualEntryDefToken = 0x17, + + /// + /// Virtual method entrypoint or call, followed by MethodRef token (shortcut) + /// + VirtualEntryRefToken = 0x18, + + /// + /// Virtual method entrypoint or call, followed by TypeSpec signature and slot + /// + VirtualEntrySlot = 0x19, + + /// + /// Helper call, followed by helper call id (see chapter 4 Helper calls) + /// + Helper = 0x1A, + + /// + /// String handle, followed by metadata string token + /// + StringHandle = 0x1B, + + /// + /// New object helper, followed by TypeSpec signature + /// + NewObject = 0x1C, + + /// + /// New array helper, followed by TypeSpec signature + /// + NewArray = 0x1D, + + /// + /// IsInst helper, followed by TypeSpec signature + /// + IsInstanceOf = 0x1E, + + /// + /// ChkCast helper, followed by TypeSpec signature + /// + ChkCast = 0x1F, + + /// + /// Field address, followed by field signature + /// + FieldAddress = 0x20, + + /// + /// Static constructor trigger, followed by TypeSpec signature + /// + CctorTrigger = 0x21, + + /// + /// Non-GC static base, followed by TypeSpec signature + /// + StaticBaseNonGC = 0x22, + + /// + /// GC static base, followed by TypeSpec signature + /// + StaticBaseGC = 0x23, + + /// + /// Non-GC thread-local static base, followed by TypeSpec signature + /// + ThreadStaticBaseNonGC = 0x24, + + /// + /// GC thread-local static base, followed by TypeSpec signature + /// + ThreadStaticBaseGC = 0x25, + + /// + /// Starting offset of fields for given type, followed by TypeSpec signature. Used to address base class + /// fragility. + /// + FieldBaseOffset = 0x26, + + /// + /// Field offset, followed by field signature + /// + FieldOffset = 0x27, + + /// + /// Hidden dictionary argument for generic code, followed by TypeSpec signature + /// + TypeDictionary = 0x28, + + /// + /// Hidden dictionary argument for generic code, followed by method signature + /// + MethodDictionary = 0x29, + + /// + /// Verification of type layout, followed by TypeSpec and expected type layout descriptor + /// + CheckTypeLayout = 0x2A, + + /// + /// Verification of field offset, followed by field signature and expected field layout descriptor + /// + CheckFieldOffset = 0x2B, + + /// + /// Delegate constructor, followed by method signature + /// + DelegateCtor = 0x2C, + + /// + /// Dictionary lookup for method declaring type. Followed by the type signature. + /// + DeclaringTypeHandle = 0x2D, + + /// + /// Target (indirect) of an inlined PInvoke. Followed by method signature. + /// + IndirectPInvokeTarget = 0x2E, + + /// + /// Target of an inlined PInvoke. Followed by method signature. + /// + PInvokeTarget = 0x2F, + + /// + /// Specify the instruction sets that must be supported/unsupported to use the R2R code associated with the + /// fixup. + /// + CheckInstructionSetSupport = 0x30, + + /// + /// Generate a runtime check to ensure that the field offset matches between compile and runtime. + /// Unlike CheckFieldOffset, this will generate a runtime exception on failure instead of silently dropping + /// the method + /// + VerifyFieldOffset = 0x31, + + /// + /// Generate a runtime check to ensure that the field offset matches between compile and runtime. + /// Unlike CheckFieldOffset, this will generate a runtime exception on failure instead of silently dropping + /// the method + /// + VerifyTypeLayout = 0x32, + + /// + /// Generate a runtime check to ensure that virtual function resolution has equivalent behavior at runtime as + /// at compile time. If not equivalent, code will not be used. See Virtual override signatures for details of + /// the signature used. + /// + CheckVirtualFunctionOverride = 0x33, + + /// + /// Generate a runtime check to ensure that virtual function resolution has equivalent behavior at runtime as + /// at compile time. If not equivalent, generate runtime failure. See Virtual override signatures for details + /// of the signature used. + /// + VerifyVirtualFunctionOverride = 0x34, + + /// + /// Check to see if an IL method is defined the same at runtime as at compile time. A failed match will cause + /// code not to be used. SeeIL Body signatures for details. + /// + CheckILBody = 0x35, + + /// + /// Verify an IL body is defined the same at compile time and runtime. A failed match will cause a hard runtime + /// failure. SeeIL Body signatures for details. + /// + VerifyILBody = 0x36, + + /// + /// When or-ed to the fixup ID, the fixup byte in the signature is followed by an encoded uint with AssemblyRef + /// index, either within the MSIL metadata of the master context module for the signature or within the manifest + /// metadata R2R header table (used in cases inlining brings in references to assemblies not seen in the input + /// MSIL). + /// + ModuleOverride = 0x80, + + } +} diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/ReadyToRunHelper.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/ReadyToRunHelper.cs new file mode 100644 index 000000000..573d1fbe1 --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/ReadyToRunHelper.cs @@ -0,0 +1,165 @@ +namespace AsmResolver.PE.DotNet.ReadyToRun +{ + /// + /// Provides members describing all available helper calls for a single fixup. + /// + public enum ReadyToRunHelper : uint + { + // Reference: https://github.com/dotnet/runtime/blob/aac9923eb6b94d24232cf87d37c8aaccef5fff93/src/coreclr/inc/readytorun.h#L284 +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + Invalid = 0x00, + + Module = 0x01, + GSCookie = 0x02, + IndirectTrapThreads = 0x03, + + // + // Delay load helpers + // + + // All delay load helpers use custom calling convention: + // - scratch register - address of indirection cell. 0 = address is inferred from callsite. + // - stack - section index, module handle + DelayLoadMethodCall = 0x08, + + DelayLoadHelper = 0x10, + DelayLoadHelperObj = 0x11, + DelayLoadHelperObjObj = 0x12, + + // JIT helpers + + // Exception handling helpers + Throw = 0x20, + Rethrow = 0x21, + Overflow = 0x22, + RngChkFail = 0x23, + FailFast = 0x24, + ThrowNullRef = 0x25, + ThrowDivZero = 0x26, + + // Write barriers + WriteBarrier = 0x30, + CheckedWriteBarrier = 0x31, + ByRefWriteBarrier = 0x32, + + // Array helpers + StelemRef = 0x38, + LdelemaRef = 0x39, + + MemSet = 0x40, + MemCpy = 0x41, + + // PInvoke helpers + PInvokeBegin = 0x42, + PInvokeEnd = 0x43, + GCPoll = 0x44, + ReversePInvokeEnter = 0x45, + ReversePInvokeExit = 0x46, + + // Get string handle lazily + GetString = 0x50, + + // Used by /Tuning for Profile optimizations + LogMethodEnter = 0x51, + + // Reflection helpers + GetRuntimeTypeHandle = 0x54, + GetRuntimeMethodHandle = 0x55, + GetRuntimeFieldHandle = 0x56, + + Box = 0x58, + BoxNullable = 0x59, + Unbox = 0x5A, + UnboxNullable = 0x5B, + NewMultiDimArr = 0x5C, + + // Helpers used with generic handle lookup cases + NewObject = 0x60, + NewArray = 0x61, + CheckCastAny = 0x62, + CheckInstanceAny = 0x63, + GenericGcStaticBase = 0x64, + GenericNonGcStaticBase = 0x65, + GenericGcTlsBase = 0x66, + GenericNonGcTlsBase = 0x67, + VirtualFuncPtr = 0x68, + IsInstanceOfException = 0x69, + NewMaybeFrozenArray = 0x6A, + NewMaybeFrozenObject = 0x6B, + + // Long mul/div/shift ops + LMul = 0xC0, + LMulOfv = 0xC1, + ULMulOvf = 0xC2, + LDiv = 0xC3, + LMod = 0xC4, + ULDiv = 0xC5, + ULMod = 0xC6, + LLsh = 0xC7, + LRsh = 0xC8, + LRsz = 0xC9, + Lng2Dbl = 0xCA, + ULng2Dbl = 0xCB, + + // 32-bit division helpers + Div = 0xCC, + Mod = 0xCD, + UDiv = 0xCE, + UMod = 0xCF, + + // Floating point conversions + Dbl2Int = 0xD0, + Dbl2IntOvf = 0xD1, + Dbl2Lng = 0xD2, + Dbl2LngOvf = 0xD3, + Dbl2UInt = 0xD4, + Dbl2UIntOvf = 0xD5, + Dbl2ULng = 0xD6, + Dbl2ULngOvf = 0xD7, + + // Floating point ops + DblRem = 0xE0, + FltRem = 0xE1, + DblRound = 0xE2, + FltRound = 0xE3, + + // Personality routines + PersonalityRoutine = 0xF0, + PersonalityRoutineFilterFunclet = 0xF1, + + // Synchronized methods + MonitorEnter = 0xF8, + MonitorExit = 0xF9, + + // + // Deprecated/legacy + // + + // JIT32 x86-specific write barriers + WriteBarrierEax = 0x100, + WriteBarrierEbx = 0x101, + WriteBarrierEcx = 0x102, + WriteBarrierEsi = 0x103, + WriteBarrierEdi = 0x104, + WriteBarrierEbp = 0x105, + CheckedWriteBarrierEax = 0x106, + CheckedWriteBarrierEbx = 0x107, + CheckedWriteBarrierEcx = 0x108, + CheckedWriteBarrierEsi = 0x109, + CheckedWriteBarrierEdi = 0x10A, + CheckedWriteBarrierEbp = 0x10B, + + // JIT32 x86-specific exception handling + EndCatch = 0x110, + + // Stack probing helper + StackProbe = 0x111, + + GetCurrentManagedThreadId = 0x112, + + // Array helpers for use with native ints + StelemRefI = 0x113, + LdelemaRefI = 0x114, +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + } +} diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/ReadyToRunSectionType.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/ReadyToRunSectionType.cs new file mode 100644 index 000000000..3083e1f13 --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/ReadyToRunSectionType.cs @@ -0,0 +1,35 @@ +namespace AsmResolver.PE.DotNet.ReadyToRun +{ + /// + /// Provides all possible ReadyToRun section types. + /// + public enum ReadyToRunSectionType + { +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + CompilerIdentifier = 100, + ImportSections = 101, + RuntimeFunctions = 102, + MethodDefEntryPoints = 103, + ExceptionInfo = 104, + DebugInfo = 105, + DelayLoadMethodCallThunks = 106, + AvailableTypesOld = 107, + AvailableTypes = 108, + InstanceMethodEntryPoints = 109, + InliningInfo = 110, + ProfileDataInfo = 111, + ManifestMetadata = 112, + AttributePresence = 113, + InliningInfo2 = 114, + ComponentAssemblies = 115, + OwnerCompositeExecutable = 116, + PgoInstrumentationData = 117, + ManifestAssemblyMvids = 118, + CrossModuleInlineInfo = 119, + HotColdMap = 120, + MethodIsGenericMap = 121, + EnclosingTypeMap = 122, + TypeGenericInfoMap = 123, +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + } +} diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/RuntimeFunctionsSection.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/RuntimeFunctionsSection.cs new file mode 100644 index 000000000..1866d8f2f --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/RuntimeFunctionsSection.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using AsmResolver.IO; +using AsmResolver.PE.Exceptions; + +namespace AsmResolver.PE.DotNet.ReadyToRun +{ + /// + /// Represents a platform-agnostic ReadyToRun section containing RUNTIME_FUNCTION structures describing all code + /// blocks in the image with pointers to their unwind info. + /// + public abstract class RuntimeFunctionsSection : SegmentBase, IReadyToRunSection + { + /// + public ReadyToRunSectionType Type => ReadyToRunSectionType.RuntimeFunctions; + + /// + public virtual bool CanRead => false; + + /// + public virtual BinaryStreamReader CreateReader() => throw new InvalidOperationException(); + + /// + /// Obtains the collection of functions stored in the section. + /// + /// The functions. + public abstract IEnumerable GetFunctions(); + } + + /// + /// Represents a platform-specific ReadyToRun section containing RUNTIME_FUNCTION structures describing all code + /// blocks in the image with pointers to their unwind info. + /// + /// The type of function to store in the section. + public abstract class RuntimeFunctionsSection : RuntimeFunctionsSection + where TFunction : IRuntimeFunction, IWritable + { + private IList? _functions; + + /// + /// Gets a collection of functions stored in the section. + /// + public IList Functions + { + get + { + if (_functions is null) + Interlocked.CompareExchange(ref _functions, GetEntries(), null); + return _functions; + } + } + + /// + /// Obtains the entries stored in the section. + /// + /// The entries. + /// + /// This method is called upon initialization of the property. + /// + protected virtual IList GetEntries() => new List(); + + /// + public sealed override IEnumerable GetFunctions() => (IEnumerable) Functions; + + /// + public override uint GetPhysicalSize() => Functions.Count > 0 + ? (uint) Functions.Count * Functions[0].GetPhysicalSize() + : 0; + + /// + public override void Write(IBinaryStreamWriter writer) + { + for (int i = 0; i < Functions.Count; i++) + Functions[i].Write(writer); + } + } +} diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/SerializedDebugInfo.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/SerializedDebugInfo.cs new file mode 100644 index 000000000..509776808 --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/SerializedDebugInfo.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using AsmResolver.IO; + +namespace AsmResolver.PE.DotNet.ReadyToRun +{ + /// + /// Provides a lazy-initialized implementation of a that is read from a file. + /// + public class SerializedDebugInfo : DebugInfo + { + private readonly PEReaderContext _context; + private readonly BinaryStreamReader _boundsReader; + private readonly BinaryStreamReader _variablesReader; + + /// + /// Reads debug information from the provided input stream. + /// + /// The context the reader is situated in. + /// The input stream. + public SerializedDebugInfo(PEReaderContext context, BinaryStreamReader reader) + { + _context = context; + + var original = reader; + uint lookBack = NativeFormat.DecodeUnsigned(ref reader); + if (lookBack != 0) + reader.Offset = original.Offset - lookBack; + + var nibbleReader = new NibbleReader(reader); + uint boundsByteCount = nibbleReader.Read3BitEncodedUInt(); + uint variablesByteCount = nibbleReader.Read3BitEncodedUInt(); + + _boundsReader = reader.ForkRelative(nibbleReader.BaseReader.RelativeOffset, boundsByteCount); + _variablesReader = reader.ForkRelative(nibbleReader.BaseReader.RelativeOffset + boundsByteCount, variablesByteCount); + } + + /// + protected override IList GetBounds() + { + var result = base.GetBounds(); + if (_boundsReader.Length == 0) + return result; + + var reader = new NibbleReader(_boundsReader); + uint count = reader.Read3BitEncodedUInt(); + + uint nativeOffset = 0; + for (int i = 0; i < count; i++) + { + nativeOffset += reader.Read3BitEncodedUInt(); + + result.Add(new DebugInfoBounds( + nativeOffset, + reader.Read3BitEncodedUInt() + DebugInfoBounds.EpilogOffset, + (DebugInfoAttributes) reader.Read3BitEncodedUInt() + )); + } + + return result; + } + + /// + protected override IList GetVariables() + { + var result = base.GetVariables(); + if (_variablesReader.Length == 0) + return result; + + var reader = new NibbleReader(_variablesReader); + uint count = reader.Read3BitEncodedUInt(); + + for (int i = 0; i < count; i++) + result.Add(DebugInfoVariable.FromReader(_context, ref reader)); + + return result; + } + } +} diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/SerializedDebugInfoSection.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/SerializedDebugInfoSection.cs new file mode 100644 index 000000000..11f9beebe --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/SerializedDebugInfoSection.cs @@ -0,0 +1,36 @@ +using AsmResolver.IO; + +namespace AsmResolver.PE.DotNet.ReadyToRun +{ + /// + /// Provides a lazy-initialized implementation of the that is read from a file. + /// + public class SerializedDebugInfoSection : DebugInfoSection + { + private readonly PEReaderContext _context; + private readonly BinaryStreamReader _reader; + + /// + /// Reads a debug information section from the provided input stream. + /// + /// The context the reader is situated in. + /// The input stream. + public SerializedDebugInfoSection(PEReaderContext context, BinaryStreamReader reader) + { + _context = context; + _reader = reader; + } + + /// + public override bool CanRead => true; + + /// + public override BinaryStreamReader CreateReader() => _reader.Fork(); + + /// + protected override NativeArray GetEntries() + { + return NativeArray.FromReader(_reader, r => new SerializedDebugInfo(_context, r)); + } + } +} diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/SerializedImportSection.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/SerializedImportSection.cs new file mode 100644 index 000000000..83d19c549 --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/SerializedImportSection.cs @@ -0,0 +1,87 @@ +using AsmResolver.Collections; +using AsmResolver.IO; +using AsmResolver.PE.File.Headers; + +namespace AsmResolver.PE.DotNet.ReadyToRun +{ + /// + /// Provides a lazy-initialized implementation of the that is read from a file. + /// + public class SerializedImportSection : ImportSection + { + private readonly PEReaderContext _context; + private readonly DataDirectory _section; + private readonly byte _originalEntrySize; + private readonly uint _signatures; + + /// + /// Reads a single import section from the provided input stream. + /// + /// The context in which the reader is situated in. + /// The input stream. + public SerializedImportSection(PEReaderContext context, ref BinaryStreamReader reader) + { + _context = context; + _section = DataDirectory.FromReader(ref reader); + + Attributes = (ImportSectionAttributes) reader.ReadUInt16(); + Type = (ImportSectionType) reader.ReadByte(); + EntrySize = _originalEntrySize = reader.ReadByte(); + _signatures = reader.ReadUInt32(); + AuxiliaryData = context.File.GetReferenceToRva(reader.ReadUInt32()); + } + + /// + protected override ReferenceTable GetSlots() + { + var result = base.GetSlots(); + + if (!_section.IsPresentInPE) + return result; + + if (!_context.File.TryCreateDataDirectoryReader(_section, out var slotsReader)) + { + _context.BadImage("Invalid RVA and/or size for import section."); + return result; + } + + if (_originalEntrySize is not (4 or 8)) + { + _context.BadImage($"Invalid entry size of {_originalEntrySize} for import section."); + return result; + } + + while (slotsReader.CanRead(_originalEntrySize)) + { + ulong address = slotsReader.ReadNativeInt(_originalEntrySize == 4); + var reference = address != 0 + ? _context.File.GetReferenceToRva((uint) (address - _context.File.OptionalHeader.ImageBase)) + : SegmentReference.Null; + + result.Add(reference); + } + + return result; + } + + /// + protected override ReferenceTable GetSignatures() + { + var result = base.GetSignatures(); + + if (_signatures == 0) + return result; + + if (!_context.File.TryCreateReaderAtRva(_signatures, out var reader)) + { + _context.BadImage($"Invalid signatures RVA {_signatures:X8} for import section."); + return result; + } + + for (int i = 0; i < _section.Size / _originalEntrySize; i++) + result.Add(_context.File.GetReferenceToRva(reader.ReadUInt32())); + + return result; + } + } +} diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/SerializedImportSectionsSection.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/SerializedImportSectionsSection.cs new file mode 100644 index 000000000..2c521b5ca --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/SerializedImportSectionsSection.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using AsmResolver.IO; + +namespace AsmResolver.PE.DotNet.ReadyToRun +{ + /// + /// Provides a lazy-initialized implementation of the that is read from a file. + /// + public sealed class SerializedImportSectionsSection : ImportSectionsSection + { + private readonly PEReaderContext _context; + private readonly BinaryStreamReader _reader; + + /// + /// Reads a single import sections section from the provided input stream. + /// + /// The context in which the reader is situated in. + /// The input stream. + public SerializedImportSectionsSection(PEReaderContext context, ref BinaryStreamReader reader) + { + Offset = reader.Offset; + Rva = reader.Rva; + + _context = context; + _reader = reader; + } + + /// + public override bool CanRead => true; + + /// + public override BinaryStreamReader CreateReader() => _reader.Fork(); + + /// + protected override IList GetSections() + { + var result = new List(); + + var reader = _reader.Fork(); + while (reader.CanRead(ImportSection.ImportSectionSize)) + result.Add(new SerializedImportSection(_context, ref reader)); + + return result; + } + } +} diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/SerializedMethodEntryPointsSection.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/SerializedMethodEntryPointsSection.cs new file mode 100644 index 000000000..59bd0246f --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/SerializedMethodEntryPointsSection.cs @@ -0,0 +1,41 @@ +using AsmResolver.IO; + +namespace AsmResolver.PE.DotNet.ReadyToRun +{ + /// + /// Provides a lazy-initialized implementation of that is read from an + /// input .NET executable file. + /// + public class SerializedMethodEntryPointsSection : MethodEntryPointsSection + { + private readonly BinaryStreamReader _reader; + + /// + /// Reads a method entry points section from the provided input stream. + /// + /// The input stream. + public SerializedMethodEntryPointsSection(ref BinaryStreamReader reader) + { + Offset = reader.Offset; + Rva = reader.Rva; + + _reader = reader; + } + + /// + public override bool CanRead => true; + + /// + public override BinaryStreamReader CreateReader() => _reader.Fork(); + + /// + protected override NativeArray GetEntryPoints() + { + return NativeArray.FromReader( + _reader, + reader => MethodEntryPoint.FromReader(ref reader) + ); + } + + } +} diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/SerializedReadyToRunDirectory.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/SerializedReadyToRunDirectory.cs new file mode 100644 index 000000000..1a01e37ff --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/SerializedReadyToRunDirectory.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using AsmResolver.IO; +using AsmResolver.PE.File.Headers; + +namespace AsmResolver.PE.DotNet.ReadyToRun +{ + /// + /// Provides an implementation of a .NET ReadyToRun directory that was stored in a PE file. + /// + public class SerializedReadyToRunDirectory : ReadyToRunDirectory + { + private readonly PEReaderContext _context; + private readonly uint _numberOfSections = 0; + private readonly BinaryStreamReader _sectionReader; + + /// + /// Reads a .NET ReadyToRun directory from an input stream. + /// + /// The reader context. + /// The input stream. + public SerializedReadyToRunDirectory(PEReaderContext context, ref BinaryStreamReader reader) + { + if (reader.ReadUInt32() != (uint) ManagedNativeHeaderSignature.Rtr) + throw new BadImageFormatException("Input stream does not point to a valid R2R header."); + + Offset = reader.Offset; + Rva = reader.Rva; + + _context = context; + MajorVersion = reader.ReadUInt16(); + MinorVersion = reader.ReadUInt16(); + Attributes = (ReadyToRunAttributes) reader.ReadUInt32(); + _numberOfSections = reader.ReadUInt32(); + _sectionReader = reader.Fork(); + } + + /// + protected override IList GetSections() + { + var result = new List(); + + var reader = _sectionReader.Fork(); + for (int i = 0; i < _numberOfSections; i++) + { + // Read header. + if (!reader.CanRead(sizeof(uint) + DataDirectory.DataDirectorySize)) + break; + + var type = (ReadyToRunSectionType) reader.ReadUInt32(); + var header = DataDirectory.FromReader(ref reader); + + // Get reader to raw contents. + if (!_context.File.TryCreateDataDirectoryReader(header, out var contentsReader)) + { + _context.BadImage($"Invalid RVA and/or size for ReadyToRun section {i}."); + continue; + } + + // Parse. + result.Add(_context.Parameters.ReadyToRunSectionReader.ReadSection(_context, type, ref contentsReader)); + } + + return result; + } + } +} diff --git a/src/AsmResolver.PE/DotNet/ReadyToRun/SerializedX64RuntimeFunctionsSection.cs b/src/AsmResolver.PE/DotNet/ReadyToRun/SerializedX64RuntimeFunctionsSection.cs new file mode 100644 index 000000000..64332d854 --- /dev/null +++ b/src/AsmResolver.PE/DotNet/ReadyToRun/SerializedX64RuntimeFunctionsSection.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using AsmResolver.IO; +using AsmResolver.PE.Exceptions.X64; + +namespace AsmResolver.PE.DotNet.ReadyToRun +{ + /// + /// Provides a lazy-initialized implementation of the class that reads + /// entries from an x86 64-bit PE file. + /// + public class SerializedX64RuntimeFunctionsSection : RuntimeFunctionsSection + { + private readonly PEReaderContext _context; + private readonly BinaryStreamReader _reader; + + /// + /// Reads a runtime function section from the provided input stream. + /// + /// The context in which the reader is situated in. + /// The input stream. + public SerializedX64RuntimeFunctionsSection(PEReaderContext context, ref BinaryStreamReader reader) + { + Offset = reader.Offset; + Rva = reader.Rva; + + _context = context; + _reader = reader; + } + + /// + public override bool CanRead => true; + + /// + public override BinaryStreamReader CreateReader() => _reader.Fork(); + + /// + protected override IList GetEntries() + { + var result = base.GetEntries(); + + var reader = _reader.Fork(); + while (reader.CanRead(X64RuntimeFunction.EntrySize)) + result.Add(X64RuntimeFunction.FromReader(_context, ref reader)); + + return result; + } + } +} diff --git a/src/AsmResolver.PE/DotNet/Resources/SerializedDotNetResourcesDirectory.cs b/src/AsmResolver.PE/DotNet/Resources/SerializedDotNetResourcesDirectory.cs index c94c072be..dfa63b4ce 100644 --- a/src/AsmResolver.PE/DotNet/Resources/SerializedDotNetResourcesDirectory.cs +++ b/src/AsmResolver.PE/DotNet/Resources/SerializedDotNetResourcesDirectory.cs @@ -62,6 +62,13 @@ public override bool TryCreateManifestResourceReader(uint offset, out BinaryStre reader = _reader.ForkRelative(offset); uint length = reader.ReadUInt32(); + + if (!reader.CanRead(length)) + { + reader = default; + return false; + } + reader = reader.ForkAbsolute(reader.Offset, length); return true; } diff --git a/src/AsmResolver.PE/DotNet/SerializedDotNetDirectory.cs b/src/AsmResolver.PE/DotNet/SerializedDotNetDirectory.cs index ffb0d51d6..ea27a7508 100644 --- a/src/AsmResolver.PE/DotNet/SerializedDotNetDirectory.cs +++ b/src/AsmResolver.PE/DotNet/SerializedDotNetDirectory.cs @@ -2,6 +2,7 @@ using AsmResolver.IO; using AsmResolver.PE.DotNet.Metadata; using AsmResolver.PE.DotNet.Metadata.Tables; +using AsmResolver.PE.DotNet.ReadyToRun; using AsmResolver.PE.DotNet.Resources; using AsmResolver.PE.DotNet.VTableFixups; using AsmResolver.PE.File.Headers; @@ -160,7 +161,7 @@ public SerializedDotNetDirectory(PEReaderContext context, ref BinaryStreamReader } /// - protected override IReadableSegment? GetManagedNativeHeader() + protected override IManagedNativeHeader? GetManagedNativeHeader() { if (!_nativeHeaderDirectory.IsPresentInPE) return null; @@ -171,10 +172,12 @@ public SerializedDotNetDirectory(PEReaderContext context, ref BinaryStreamReader return null; } - // TODO: interpretation instead of raw contents. - return DataSegment.FromReader(ref directoryReader); - + var signature = (ManagedNativeHeaderSignature) directoryReader.Fork().ReadUInt32(); + return signature switch + { + ManagedNativeHeaderSignature.Rtr => new SerializedReadyToRunDirectory(_context, ref directoryReader), + _ => new CustomManagedNativeHeader(signature, directoryReader.ReadSegment(directoryReader.RemainingLength)) + }; } - } } diff --git a/src/AsmResolver.PE/PEReaderParameters.cs b/src/AsmResolver.PE/PEReaderParameters.cs index cb8ee0573..32a48c4ad 100644 --- a/src/AsmResolver.PE/PEReaderParameters.cs +++ b/src/AsmResolver.PE/PEReaderParameters.cs @@ -3,6 +3,7 @@ using AsmResolver.PE.Certificates; using AsmResolver.PE.Debug; using AsmResolver.PE.DotNet.Metadata; +using AsmResolver.PE.DotNet.ReadyToRun; namespace AsmResolver.PE { @@ -28,6 +29,7 @@ public PEReaderParameters(IErrorListener errorListener) MetadataStreamReader = new DefaultMetadataStreamReader(); DebugDataReader = new DefaultDebugDataReader(); CertificateReader = new DefaultCertificateReader(); + ReadyToRunSectionReader = new DefaultReadyToRunSectionReader(); ErrorListener = errorListener ?? throw new ArgumentNullException(nameof(errorListener)); } @@ -76,5 +78,15 @@ public IFileService FileService get; set; } = UncachedFileService.Instance; + + /// + /// Gets or sets the object to use for reading ReadyToRun metadata sections from the disk while reading the + /// portable executable. + /// + public IReadyToRunSectionReader ReadyToRunSectionReader + { + get; + set; + } } } diff --git a/src/AsmResolver.PE/Tls/ITlsDirectory.cs b/src/AsmResolver.PE/Tls/ITlsDirectory.cs index 9149c0314..9c48ea052 100644 --- a/src/AsmResolver.PE/Tls/ITlsDirectory.cs +++ b/src/AsmResolver.PE/Tls/ITlsDirectory.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using AsmResolver.Collections; using AsmResolver.PE.Relocations; namespace AsmResolver.PE.Tls @@ -30,7 +31,7 @@ ISegmentReference Index /// /// Gets a table of function callbacks that need to be called upon every thread creation. /// - TlsCallbackCollection CallbackFunctions + ReferenceTable CallbackFunctions { get; } diff --git a/src/AsmResolver.PE/Tls/SerializedTlsDirectory.cs b/src/AsmResolver.PE/Tls/SerializedTlsDirectory.cs index 539c08cc4..b2efff26a 100644 --- a/src/AsmResolver.PE/Tls/SerializedTlsDirectory.cs +++ b/src/AsmResolver.PE/Tls/SerializedTlsDirectory.cs @@ -1,3 +1,4 @@ +using AsmResolver.Collections; using AsmResolver.IO; using AsmResolver.PE.File.Headers; @@ -61,9 +62,9 @@ public SerializedTlsDirectory(PEReaderContext context, ref BinaryStreamReader re } /// - protected override TlsCallbackCollection GetCallbackFunctions() + protected override ReferenceTable GetCallbackFunctions() { - var result = new TlsCallbackCollection(this); + var result = base.GetCallbackFunctions(); var file = _context.File; var optionalHeader = file.OptionalHeader; diff --git a/src/AsmResolver.PE/Tls/TlsCallbackCollection.cs b/src/AsmResolver.PE/Tls/TlsCallbackCollection.cs deleted file mode 100644 index dc29af2aa..000000000 --- a/src/AsmResolver.PE/Tls/TlsCallbackCollection.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System.Collections.ObjectModel; -using AsmResolver.IO; - -namespace AsmResolver.PE.Tls -{ - /// - /// Represents a collection of Thread-Local Storage (TLS) callback function addresses. - /// - public class TlsCallbackCollection : Collection, ISegment - { - private readonly ITlsDirectory _owner; - private ulong _imageBase = 0x00400000; - private bool _is32Bit = true; - - internal TlsCallbackCollection(ITlsDirectory owner) - { - _owner = owner; - } - - /// - public ulong Offset - { - get; - private set; - } - - /// - public uint Rva - { - get; - private set; - } - - /// - public bool CanUpdateOffsets => true; - - /// - public void UpdateOffsets(in RelocationParameters parameters) - { - Offset = parameters.Offset; - Rva = parameters.Rva; - _imageBase = parameters.ImageBase; - _is32Bit = parameters.Is32Bit; - } - - /// - public uint GetPhysicalSize() - { - uint pointerSize = (uint) (_is32Bit ? sizeof(uint) : sizeof(ulong)); - return (uint) (pointerSize * (Count + 1)); - } - - /// - public uint GetVirtualSize() => GetPhysicalSize(); - - /// - public void Write(IBinaryStreamWriter writer) - { - ulong imageBase = _imageBase; - bool is32Bit = _is32Bit; - - for (int i = 0; i < Items.Count; i++) - writer.WriteNativeInt(imageBase + Items[i].Rva, is32Bit); - - writer.WriteNativeInt(0, is32Bit); - } - } -} diff --git a/src/AsmResolver.PE/Tls/TlsDirectory.cs b/src/AsmResolver.PE/Tls/TlsDirectory.cs index 6f231fd23..90ba70a56 100644 --- a/src/AsmResolver.PE/Tls/TlsDirectory.cs +++ b/src/AsmResolver.PE/Tls/TlsDirectory.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Threading; +using AsmResolver.Collections; using AsmResolver.IO; using AsmResolver.PE.Relocations; @@ -11,7 +12,7 @@ namespace AsmResolver.PE.Tls public class TlsDirectory : SegmentBase, ITlsDirectory { private readonly LazyVariable _templateData; - private TlsCallbackCollection? _callbackFunctions; + private ReferenceTable? _callbackFunctions; private ulong _imageBase = 0x00400000; private bool _is32Bit = true; @@ -39,7 +40,7 @@ public ISegmentReference Index } /// - public TlsCallbackCollection CallbackFunctions + public ReferenceTable CallbackFunctions { get { @@ -87,7 +88,7 @@ public override void UpdateOffsets(in RelocationParameters parameters) /// /// This method is called upon initialization of the property. /// - protected virtual TlsCallbackCollection GetCallbackFunctions() => new(this); + protected virtual ReferenceTable GetCallbackFunctions() => new(ReferenceTableAttributes.Va | ReferenceTableAttributes.Adaptive | ReferenceTableAttributes.ZeroTerminated); /// public IEnumerable GetRequiredBaseRelocations() diff --git a/src/AsmResolver.Symbols.Pdb/AsmResolver.Symbols.Pdb.csproj b/src/AsmResolver.Symbols.Pdb/AsmResolver.Symbols.Pdb.csproj index d597389f5..1b0c09029 100644 --- a/src/AsmResolver.Symbols.Pdb/AsmResolver.Symbols.Pdb.csproj +++ b/src/AsmResolver.Symbols.Pdb/AsmResolver.Symbols.Pdb.csproj @@ -5,7 +5,7 @@ Windows PDB models for the AsmResolver executable file inspection toolsuite. windows pdb symbols enable - net6.0;netcoreapp3.1;netstandard2.0 + net6.0;netcoreapp3.1;netstandard2.0;netstandard2.1 true true diff --git a/src/AsmResolver/AsmResolver.csproj b/src/AsmResolver/AsmResolver.csproj index ea9a1dcc6..d3591d07c 100644 --- a/src/AsmResolver/AsmResolver.csproj +++ b/src/AsmResolver/AsmResolver.csproj @@ -7,7 +7,7 @@ true 1701;1702;NU5105 enable - net6.0;netcoreapp3.1;netstandard2.0 + net6.0;netcoreapp3.1;netstandard2.0;netstandard2.1 true diff --git a/src/AsmResolver/Collections/BitList.cs b/src/AsmResolver/Collections/BitList.cs index c4224cd04..7c552dd64 100644 --- a/src/AsmResolver/Collections/BitList.cs +++ b/src/AsmResolver/Collections/BitList.cs @@ -46,7 +46,7 @@ public bool this[int index] { get { - if (index >= Count) + if (index < 0 || index >= Count) throw new IndexOutOfRangeException(); (int wordIndex, int bitIndex) = SplitWordBitIndex(index); @@ -54,7 +54,7 @@ public bool this[int index] } set { - if (index >= Count) + if (index < 0 || index >= Count) throw new IndexOutOfRangeException(); (int wordIndex, int bitIndex) = SplitWordBitIndex(index); @@ -119,7 +119,7 @@ public int IndexOf(bool item) /// public void Insert(int index, bool item) { - if (index > Count) + if (index < 0 || index > Count) throw new IndexOutOfRangeException(); EnsureCapacity(Count++); diff --git a/src/AsmResolver/Collections/ReferenceTable.cs b/src/AsmResolver/Collections/ReferenceTable.cs new file mode 100644 index 000000000..c8f87968b --- /dev/null +++ b/src/AsmResolver/Collections/ReferenceTable.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.ObjectModel; +using AsmResolver.IO; + +namespace AsmResolver.Collections +{ + /// + /// Represents a table of references that can be written as one block of contiguous bytes. + /// + public class ReferenceTable : Collection, ISegment + { + private ulong _imageBase; + private bool _is32Bit; + + /// + /// Creates a new reference table. + /// + /// The attributes describing the shape of the table. + public ReferenceTable(ReferenceTableAttributes attributes) + { + Attributes = attributes; + } + + /// + /// Gets the attributes describing the shape of the table. + /// + public ReferenceTableAttributes Attributes + { + get; + } + + /// + /// Gets the type of reference that is stored in the table. + /// + public ReferenceTableAttributes ReferenceType => Attributes & ReferenceTableAttributes.ReferenceTypeMask; + + /// + /// Gets a value indicating whether the table contains offsets. + /// + public bool IsOffsetTable => ReferenceType == ReferenceTableAttributes.Offset; + + /// + /// Gets a value indicating whether the table contains RVAs. + /// + public bool IsRvaTable => ReferenceType == ReferenceTableAttributes.Rva; + + /// + /// Gets a value indicating whether the table contains VAs. + /// + public bool IsVaTable => ReferenceType == ReferenceTableAttributes.Va; + + /// + /// Gets the size in bytes that each reference occupies in the table. + /// + public ReferenceTableAttributes ReferenceSize => Attributes & ReferenceTableAttributes.SizeMask; + + /// + /// Gets a value indicating whether the size of a single reference changes depending on whether + /// this table is put in a 32 or 64 bit application. + /// + public bool IsAdaptive => ReferenceSize == ReferenceTableAttributes.Adaptive; + + /// + /// Gets a value indicating whether the size of a single reference in the table is 32 bits. + /// + public bool Is32BitTable => ReferenceSize == ReferenceTableAttributes.Force32Bit + || IsAdaptive && _is32Bit; + + /// + /// Gets a value indicating whether the size of a single reference in the table is 64 bits. + /// + public bool Is64BitTable => ReferenceSize == ReferenceTableAttributes.Force64Bit + || IsAdaptive && !_is32Bit; + + /// + /// Gets a value indicating whether the table ends with a zero entry. + /// + public bool IsZeroTerminated => (Attributes & ReferenceTableAttributes.ZeroTerminated) != 0; + + /// + public ulong Offset + { + get; + private set; + } + + /// + public uint Rva + { + get; + private set; + } + + /// + public bool CanUpdateOffsets => true; + + /// + public void UpdateOffsets(in RelocationParameters parameters) + { + _imageBase = parameters.ImageBase; + _is32Bit = parameters.Is32Bit; + Offset = parameters.Offset; + Rva = parameters.Rva; + } + + /// + public uint GetPhysicalSize() + { + int actualCount = Count + (IsZeroTerminated ? 1 : 0); + return (uint) (actualCount * (_is32Bit ? sizeof(uint) : sizeof(ulong))); + } + + /// + public uint GetVirtualSize() => GetPhysicalSize(); + + /// + public void Write(IBinaryStreamWriter writer) + { + for (int i = 0; i < Items.Count; i++) + { + var item = Items[i]; + + ulong value = ReferenceType switch + { + ReferenceTableAttributes.Offset => item.Offset, + ReferenceTableAttributes.Rva => item.Rva, + ReferenceTableAttributes.Va => item.Rva + _imageBase, + _ => throw new ArgumentOutOfRangeException() + }; + + writer.WriteNativeInt(value, Is32BitTable); + } + + if (IsZeroTerminated) + writer.WriteNativeInt(0, Is32BitTable); + } + } +} diff --git a/src/AsmResolver/Collections/ReferenceTableAttributes.cs b/src/AsmResolver/Collections/ReferenceTableAttributes.cs new file mode 100644 index 000000000..abfa45497 --- /dev/null +++ b/src/AsmResolver/Collections/ReferenceTableAttributes.cs @@ -0,0 +1,56 @@ +using System; + +namespace AsmResolver.Collections +{ + /// + /// Provides members describing the shape of a reference table. + /// + [Flags] + public enum ReferenceTableAttributes + { + /// + /// Indicates the table contains offsets. + /// + Offset = 0b00, + + /// + /// Indicates the table contains RVAs. + /// + Rva = 0b01, + + /// + /// Indicates the table contains VAs. + /// + Va = 0b10, + + /// + /// Provides a bit-mask for obtaining the type of references stored in the table. + /// + ReferenceTypeMask = 0b11, + + /// + /// Indicates the table changes in size depending on whether it is put in a 32-bit or 64-bit application. + /// + Adaptive = 0b0000, + + /// + /// Indicates the table always uses 32 bits for every entry. + /// + Force32Bit = 0b0100, + + /// + /// Indicates the table always uses 64 bits for every entry. + /// + Force64Bit = 0b1000, + + /// + /// Provides a bit-mask for obtaining the size of each reference stored in the table. + /// + SizeMask = 0b1100, + + /// + /// Indicates the table ends with an extra zero item. + /// + ZeroTerminated = 0b10000 + } +} diff --git a/src/AsmResolver/IO/BinaryStreamReader.cs b/src/AsmResolver/IO/BinaryStreamReader.cs index a5d5ae3b8..532b54616 100644 --- a/src/AsmResolver/IO/BinaryStreamReader.cs +++ b/src/AsmResolver/IO/BinaryStreamReader.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Text; @@ -8,6 +8,7 @@ namespace AsmResolver.IO /// /// Provides methods for reading binary data from a data source. /// + [DebuggerDisplay("[{StartOffset}..{EndOffset}) at {Offset} ({RelativeOffset})")] public struct BinaryStreamReader { [ThreadStatic] @@ -125,6 +126,11 @@ public uint RelativeOffset set => Offset = value + StartOffset; } + /// + /// Gets the remaining number of bytes that can be read from the stream. + /// + public uint RemainingLength => Length - RelativeOffset; + /// /// Gets or sets the current virtual address (relative to the image base) to read from. /// @@ -311,6 +317,20 @@ public decimal ReadDecimal() return new decimal(_buffer); } +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + /// + /// Attempts to read the provided amount of bytes from the input stream. + /// + /// The buffer that receives the read bytes. + /// The number of bytes that were read. + public int ReadBytes(Span buffer) + { + int actualLength = DataSource.ReadBytes(Offset, buffer); + Offset += (uint) actualLength; + return actualLength; + } +#endif + /// /// Attempts to read the provided amount of bytes from the input stream. /// @@ -344,7 +364,7 @@ public IReadableSegment ReadSegment(uint count) /// The remaining bytes. public byte[] ReadToEnd() { - byte[] buffer = new byte[Length - RelativeOffset]; + byte[] buffer = new byte[RemainingLength]; ReadBytes(buffer, 0, buffer.Length); return buffer; } diff --git a/src/AsmResolver/IO/BinaryStreamWriter.cs b/src/AsmResolver/IO/BinaryStreamWriter.cs index e55c7ed25..fb8c0aa93 100644 --- a/src/AsmResolver/IO/BinaryStreamWriter.cs +++ b/src/AsmResolver/IO/BinaryStreamWriter.cs @@ -7,6 +7,9 @@ namespace AsmResolver.IO /// Provides a default implementation of a binary writer that writes the data to an output stream. /// public class BinaryStreamWriter : IBinaryStreamWriter +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + , ISpanBinaryStreamWriter +#endif { /// /// Creates a new binary stream writer using the provided output stream. @@ -47,6 +50,14 @@ public void WriteBytes(byte[] buffer, int startIndex, int count) BaseStream.Write(buffer, startIndex, count); } +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + /// + public void WriteBytes(ReadOnlySpan buffer) + { + BaseStream.Write(buffer); + } +#endif + /// public void WriteByte(byte value) { diff --git a/src/AsmResolver/IO/ByteArrayDataSource.cs b/src/AsmResolver/IO/ByteArrayDataSource.cs index c9dc3d8dc..680c53ea8 100644 --- a/src/AsmResolver/IO/ByteArrayDataSource.cs +++ b/src/AsmResolver/IO/ByteArrayDataSource.cs @@ -6,6 +6,9 @@ namespace AsmResolver.IO /// Provides a wrapper around a raw byte array. /// public sealed class ByteArrayDataSource : IDataSource +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + , ISpanDataSource +#endif { private readonly byte[] _data; @@ -60,5 +63,16 @@ public int ReadBytes(ulong address, byte[] buffer, int index, int count) Buffer.BlockCopy(_data, relativeIndex, buffer, index, actualLength); return actualLength; } + +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + /// + public int ReadBytes(ulong address, Span buffer) + { + int relativeIndex = (int) (address - BaseAddress); + int actualLength = Math.Min(buffer.Length, _data.Length - relativeIndex); + _data.AsSpan(relativeIndex, actualLength).CopyTo(buffer); + return actualLength; + } +#endif } } diff --git a/src/AsmResolver/IO/ByteArrayFileService.cs b/src/AsmResolver/IO/ByteArrayFileService.cs index 141394113..02816d600 100644 --- a/src/AsmResolver/IO/ByteArrayFileService.cs +++ b/src/AsmResolver/IO/ByteArrayFileService.cs @@ -1,6 +1,6 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; -using System.IO; namespace AsmResolver.IO { @@ -11,7 +11,7 @@ namespace AsmResolver.IO /// public class ByteArrayFileService : IFileService { - private readonly Dictionary _files = new(); + private readonly ConcurrentDictionary _files = new(); /// public IEnumerable GetOpenedFiles() => _files.Keys; @@ -19,17 +19,11 @@ public class ByteArrayFileService : IFileService /// public IInputFile OpenFile(string filePath) { - if (!_files.TryGetValue(filePath, out var file)) - { - file = new ByteArrayInputFile(filePath); - _files.Add(filePath, file); - } - - return file; + return _files.GetOrAdd(filePath, x => new ByteArrayInputFile(x)); } /// - public void InvalidateFile(string filePath) => _files.Remove(filePath); + public void InvalidateFile(string filePath) => _files.TryRemove(filePath, out _); /// void IDisposable.Dispose() => _files.Clear(); diff --git a/src/AsmResolver/IO/DataSourceExtensions.cs b/src/AsmResolver/IO/DataSourceExtensions.cs new file mode 100644 index 000000000..fed2113b8 --- /dev/null +++ b/src/AsmResolver/IO/DataSourceExtensions.cs @@ -0,0 +1,34 @@ +using System; +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP1_0_OR_GREATER +using System.Buffers; +#endif + +namespace AsmResolver.IO +{ + /// + /// Provides extension methods for . + /// + public static class DataSourceExtensions + { +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + /// + /// Reads a block of data from the data source. + /// + /// The to read from. + /// The starting address to read from. + /// The buffer that receives the read bytes. + /// The number of bytes that were read. + public static int ReadBytes(this IDataSource dataSource, ulong address, Span buffer) + { + if (dataSource is ISpanDataSource spanDataSource) + return spanDataSource.ReadBytes(address, buffer); + + byte[] array = ArrayPool.Shared.Rent(buffer.Length); + int bytesRead = dataSource.ReadBytes(address, array, 0, buffer.Length); + new ReadOnlySpan(array, 0, bytesRead).CopyTo(buffer); + ArrayPool.Shared.Return(array); + return bytesRead; + } +#endif + } +} diff --git a/src/AsmResolver/IO/DataSourceSlice.cs b/src/AsmResolver/IO/DataSourceSlice.cs index f107316a0..65e9408b2 100644 --- a/src/AsmResolver/IO/DataSourceSlice.cs +++ b/src/AsmResolver/IO/DataSourceSlice.cs @@ -6,6 +6,9 @@ namespace AsmResolver.IO /// Represents a data source that only exposes a part (slice) of another data source. /// public class DataSourceSlice : IDataSource +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + , ISpanDataSource +#endif { private readonly IDataSource _source; @@ -64,5 +67,14 @@ public int ReadBytes(ulong address, byte[] buffer, int index, int count) int maxCount = Math.Max(0, (int) (Length - (address - BaseAddress))); return _source.ReadBytes(address, buffer, index, Math.Min(maxCount, count)); } + +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + /// + public int ReadBytes(ulong address, Span buffer) + { + int maxCount = Math.Max(0, (int) (Length - (address - BaseAddress))); + return _source.ReadBytes(address, buffer[..Math.Min(maxCount, buffer.Length)]); + } +#endif } } diff --git a/src/AsmResolver/IO/DisplacedDataSource.cs b/src/AsmResolver/IO/DisplacedDataSource.cs index 6d946b9c6..77757e0f5 100644 --- a/src/AsmResolver/IO/DisplacedDataSource.cs +++ b/src/AsmResolver/IO/DisplacedDataSource.cs @@ -6,6 +6,9 @@ namespace AsmResolver.IO /// Represents a data source that was moved in memory to a different address. /// public class DisplacedDataSource : IDataSource +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + , ISpanDataSource +#endif { private readonly IDataSource _dataSource; private readonly long _displacement; @@ -39,5 +42,13 @@ public int ReadBytes(ulong address, byte[] buffer, int index, int count) { return _dataSource.ReadBytes(address - (ulong) _displacement, buffer, index, count); } + +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + /// + public int ReadBytes(ulong address, Span buffer) + { + return _dataSource.ReadBytes(address - (ulong) _displacement, buffer); + } +#endif } } diff --git a/src/AsmResolver/IO/IBinaryStreamWriter.cs b/src/AsmResolver/IO/IBinaryStreamWriter.cs index 7d76eaa29..fe772a3f8 100644 --- a/src/AsmResolver/IO/IBinaryStreamWriter.cs +++ b/src/AsmResolver/IO/IBinaryStreamWriter.cs @@ -1,5 +1,8 @@ using System; using System.Text; +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP1_0_OR_GREATER +using System.Buffers; +#endif namespace AsmResolver.IO { @@ -130,6 +133,27 @@ public static void WriteBytes(this IBinaryStreamWriter writer, byte[] buffer) writer.WriteBytes(buffer, 0, buffer.Length); } +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + /// + /// Writes a buffer of data to the stream. + /// + /// The writer to use. + /// The data to write. + public static void WriteBytes(this IBinaryStreamWriter writer, ReadOnlySpan buffer) + { + if (writer is ISpanBinaryStreamWriter spanWriter) + { + spanWriter.WriteBytes(buffer); + return; + } + + byte[] array = ArrayPool.Shared.Rent(buffer.Length); + buffer.CopyTo(array); + writer.WriteBytes(array, 0, buffer.Length); + ArrayPool.Shared.Return(array); + } +#endif + /// /// Writes a specified amount of zero bytes to the stream. /// diff --git a/src/AsmResolver/IO/ISpanBinaryStreamWriter.cs b/src/AsmResolver/IO/ISpanBinaryStreamWriter.cs new file mode 100644 index 000000000..493404953 --- /dev/null +++ b/src/AsmResolver/IO/ISpanBinaryStreamWriter.cs @@ -0,0 +1,18 @@ +using System; + +namespace AsmResolver.IO +{ +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + /// + /// Provides span-based methods for writing data to a binary stream. + /// + public interface ISpanBinaryStreamWriter : IBinaryStreamWriter + { + /// + /// Writes a buffer of data to the stream. + /// + /// The buffer to write to the stream. + void WriteBytes(ReadOnlySpan buffer); + } +#endif +} diff --git a/src/AsmResolver/IO/ISpanDataSource.cs b/src/AsmResolver/IO/ISpanDataSource.cs new file mode 100644 index 000000000..30e3737e3 --- /dev/null +++ b/src/AsmResolver/IO/ISpanDataSource.cs @@ -0,0 +1,20 @@ +using System; + +namespace AsmResolver.IO +{ +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + /// + /// Provides span-based members for reading data from a data source. + /// + public interface ISpanDataSource : IDataSource + { + /// + /// Reads a block of data from the data source. + /// + /// The starting address to read from. + /// The buffer that receives the read bytes. + /// The number of bytes that were read. + int ReadBytes(ulong address, Span buffer); + } +#endif +} diff --git a/src/AsmResolver/IO/MemoryMappedDataSource.cs b/src/AsmResolver/IO/MemoryMappedDataSource.cs index 70e130372..5788c0ac2 100644 --- a/src/AsmResolver/IO/MemoryMappedDataSource.cs +++ b/src/AsmResolver/IO/MemoryMappedDataSource.cs @@ -7,6 +7,9 @@ namespace AsmResolver.IO /// Represents a data source that obtains its data from a memory mapped file. /// public sealed class MemoryMappedDataSource : IDataSource, IDisposable +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + , ISpanDataSource +#endif { private readonly MemoryMappedViewAccessor _accessor; @@ -40,6 +43,38 @@ public ulong Length public int ReadBytes(ulong address, byte[] buffer, int index, int count) => _accessor.ReadArray((long) address, buffer, index, count); +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + /// + public unsafe int ReadBytes(ulong address, Span buffer) + { + if (!IsValidAddress(address)) + return 0; + + var handle = _accessor.SafeMemoryMappedViewHandle; + int actualLength = (int) Math.Min(Length - address, (uint) buffer.Length); + +#if NET6_0_OR_GREATER + handle.ReadSpan(address, buffer[..actualLength]); +#else + byte* pointer = null; + + try + { + handle.AcquirePointer(ref pointer); + new ReadOnlySpan(pointer, actualLength).CopyTo(buffer); + } + finally + { + if (pointer != null) + { + handle.ReleasePointer(); + } + } +#endif + return actualLength; + } +#endif + /// public void Dispose() => _accessor?.Dispose(); } diff --git a/src/AsmResolver/IO/MemoryMappedFileService.cs b/src/AsmResolver/IO/MemoryMappedFileService.cs index ea5892886..ab372ebe9 100644 --- a/src/AsmResolver/IO/MemoryMappedFileService.cs +++ b/src/AsmResolver/IO/MemoryMappedFileService.cs @@ -1,4 +1,6 @@ +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; namespace AsmResolver.IO { @@ -8,7 +10,7 @@ namespace AsmResolver.IO /// public class MemoryMappedFileService : IFileService { - private readonly Dictionary _files = new(); + private readonly ConcurrentDictionary _files = new(); /// public IEnumerable GetOpenedFiles() => _files.Keys; @@ -16,23 +18,14 @@ public class MemoryMappedFileService : IFileService /// public IInputFile OpenFile(string filePath) { - if (!_files.TryGetValue(filePath, out var file)) - { - file = new MemoryMappedInputFile(filePath); - _files.Add(filePath, file); - } - - return file; + return _files.GetOrAdd(filePath, x => new MemoryMappedInputFile(x)); } /// public void InvalidateFile(string filePath) { - if (_files.TryGetValue(filePath, out var file)) - { + if (_files.TryRemove(filePath, out var file)) file.Dispose(); - _files.Remove(filePath); - } } /// diff --git a/src/AsmResolver/IO/MemoryMappedInputFile.cs b/src/AsmResolver/IO/MemoryMappedInputFile.cs index 6b04e968f..7890c1d33 100644 --- a/src/AsmResolver/IO/MemoryMappedInputFile.cs +++ b/src/AsmResolver/IO/MemoryMappedInputFile.cs @@ -40,8 +40,8 @@ public BinaryStreamReader CreateReader(ulong address, uint rva, uint length) => /// public void Dispose() { - _file?.Dispose(); - _dataSource?.Dispose(); + _file.Dispose(); + _dataSource.Dispose(); } } } diff --git a/src/AsmResolver/IO/UncachedFileService.cs b/src/AsmResolver/IO/UncachedFileService.cs index f2c593a7a..6a195c28d 100644 --- a/src/AsmResolver/IO/UncachedFileService.cs +++ b/src/AsmResolver/IO/UncachedFileService.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; namespace AsmResolver.IO diff --git a/src/AsmResolver/IO/UnmanagedDataSource.cs b/src/AsmResolver/IO/UnmanagedDataSource.cs index b11a5f57d..1350eb747 100644 --- a/src/AsmResolver/IO/UnmanagedDataSource.cs +++ b/src/AsmResolver/IO/UnmanagedDataSource.cs @@ -7,6 +7,9 @@ namespace AsmResolver.IO /// Represents a data source that obtains its data from a block of unmanaged memory. /// public sealed unsafe class UnmanagedDataSource : IDataSource +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + , ISpanDataSource +#endif { private readonly void* _basePointer; @@ -66,5 +69,19 @@ public int ReadBytes(ulong address, byte[] buffer, int index, int count) Marshal.Copy((IntPtr) address, buffer, index, actualLength); return actualLength; } + +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + /// + public int ReadBytes(ulong address, Span buffer) + { + if (!IsValidAddress(address)) + return 0; + + ulong relativeIndex = address - (ulong) _basePointer; + int actualLength = (int) Math.Min((uint) buffer.Length, Length - relativeIndex); + new ReadOnlySpan((byte*) address, actualLength).CopyTo(buffer); + return actualLength; + } +#endif } } diff --git a/src/AsmResolver/IO/ZeroesDataSource.cs b/src/AsmResolver/IO/ZeroesDataSource.cs index 1e15c006e..7582becec 100644 --- a/src/AsmResolver/IO/ZeroesDataSource.cs +++ b/src/AsmResolver/IO/ZeroesDataSource.cs @@ -6,6 +6,9 @@ namespace AsmResolver.IO /// Implements a data source that reads zero bytes. /// public sealed class ZeroesDataSource : IDataSource +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + , ISpanDataSource +#endif { /// /// Creates a new zeroes data source. @@ -51,8 +54,18 @@ public ulong Length public int ReadBytes(ulong address, byte[] buffer, int index, int count) { int actualLength = (int) Math.Min(Length, (ulong) count); - Array.Clear(buffer, index, count); + Array.Clear(buffer, index, actualLength); return actualLength; } + +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + /// + public int ReadBytes(ulong address, Span buffer) + { + int actualLength = (int) Math.Min(Length, (uint) buffer.Length); + buffer[..actualLength].Clear(); + return actualLength; + } +#endif } } diff --git a/src/AsmResolver/ISegment.cs b/src/AsmResolver/ISegment.cs index 849245fd0..021f1f47c 100644 --- a/src/AsmResolver/ISegment.cs +++ b/src/AsmResolver/ISegment.cs @@ -36,8 +36,6 @@ bool CanUpdateOffsets public static partial class Extensions { - private const string ReservedStringCharacters = "\\\"\t\r\n\b"; - [ThreadStatic] private static StringBuilder? _buffer; @@ -125,9 +123,22 @@ public static string CreateEscapedString(this string literal) _buffer.Append('"'); foreach (char currentChar in literal) { - if (ReservedStringCharacters.Contains(currentChar)) - _buffer.Append('\\'); - _buffer.Append(currentChar); + string? replacement = currentChar switch + { + '\0' => "\\0", + '\\' => "\\\\", + '\"' => "\\\"", + '\t' => "\\t", + '\r' => "\\r", + '\n' => "\\n", + '\b' => "\\b", + _ => null + }; + + if (replacement is not null) + _buffer.Append(replacement); + else + _buffer.Append(currentChar); } _buffer.Append('"'); diff --git a/src/AsmResolver/Utf8String.cs b/src/AsmResolver/Utf8String.cs index f3c43f540..5e3a6fb74 100644 --- a/src/AsmResolver/Utf8String.cs +++ b/src/AsmResolver/Utf8String.cs @@ -35,6 +35,27 @@ public Utf8String(byte[] data) { } +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + /// + /// Creates a new UTF-8 string from the provided raw data. + /// + /// The raw UTF-8 data. + public Utf8String(ReadOnlySpan data) + { + _data = data.ToArray(); + } + + /// + /// Creates a new UTF-8 string from the provided . + /// + /// The string value to encode as UTF-8. + public Utf8String(ReadOnlySpan value) + { + _data = new byte[Encoding.UTF8.GetByteCount(value)]; + Encoding.UTF8.GetBytes(value, _data.AsSpan()); + } +#endif + /// /// Creates a new UTF-8 string from the provided raw data. /// @@ -82,6 +103,38 @@ public Utf8String(string value) /// The character index. public char this[int index] => Value[index]; +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + /// + /// Creates a new read-only span over the string. + /// + /// The read-only span representation of the string. + public ReadOnlySpan AsSpan() + { + return _data.AsSpan(); + } + + /// + /// Creates a new read-only span over the string. + /// + /// The index at which to begin this slice. + /// The read-only span representation of the string. + public ReadOnlySpan AsSpan(int start) + { + return _data.AsSpan(start); + } + + /// + /// Creates a new read-only span over the string. + /// + /// The index at which to begin this slice. + /// The desired length for the slice. + /// The read-only span representation of the string. + public ReadOnlySpan AsSpan(int start, int length) + { + return _data.AsSpan(start, length); + } +#endif + /// /// Gets the raw UTF-8 bytes of the string. /// @@ -337,6 +390,60 @@ public int CompareTo(Utf8String? other) => other is not null return new Utf8String(value); } +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + /// + /// Converts a raw sequence of bytes into an . + /// + /// The raw data to convert. + /// The new UTF-8 encoded string. + public static implicit operator Utf8String(ReadOnlySpan data) + { + if (data.IsEmpty) + return Empty; + + return new Utf8String(data); + } + + /// + /// Converts a into an . + /// + /// The string value to convert. + /// The new UTF-8 encoded string. + public static implicit operator Utf8String(ReadOnlySpan data) + { + if (data.IsEmpty) + return Empty; + + return new Utf8String(data); + } + + /// + /// Converts a into a . + /// + /// The UTF-8 string value to convert. + /// The span. + public static implicit operator ReadOnlySpan(Utf8String? value) + { + if (value is null) + return ReadOnlySpan.Empty; + + return value._data; + } + + /// + /// Converts a into a . + /// + /// The UTF-8 string value to convert. + /// The span. + public static implicit operator ReadOnlySpan(Utf8String? value) + { + if (value is null) + return ReadOnlySpan.Empty; + + return value.Value; + } +#endif + /// /// Converts a raw sequence of bytes into an . /// diff --git a/src/AsmResolver/VirtualAddressFactory.cs b/src/AsmResolver/VirtualAddressFactory.cs index b5af70799..4defa956b 100644 --- a/src/AsmResolver/VirtualAddressFactory.cs +++ b/src/AsmResolver/VirtualAddressFactory.cs @@ -14,6 +14,8 @@ public static VirtualAddressFactory Instance } = new(); /// - public ISegmentReference GetReferenceToRva(uint rva) => new VirtualAddress(rva); + public ISegmentReference GetReferenceToRva(uint rva) => rva != 0 + ? new VirtualAddress(rva) + : SegmentReference.Null; } } diff --git a/test/AsmResolver.Benchmarks/AsmResolver.Benchmarks.csproj b/test/AsmResolver.Benchmarks/AsmResolver.Benchmarks.csproj index 9357b967b..864745258 100644 --- a/test/AsmResolver.Benchmarks/AsmResolver.Benchmarks.csproj +++ b/test/AsmResolver.Benchmarks/AsmResolver.Benchmarks.csproj @@ -7,7 +7,7 @@ - + diff --git a/test/AsmResolver.DotNet.Dynamic.Tests/AsmResolver.DotNet.Dynamic.Tests.csproj b/test/AsmResolver.DotNet.Dynamic.Tests/AsmResolver.DotNet.Dynamic.Tests.csproj index 816b9b8fa..aa91f3a42 100644 --- a/test/AsmResolver.DotNet.Dynamic.Tests/AsmResolver.DotNet.Dynamic.Tests.csproj +++ b/test/AsmResolver.DotNet.Dynamic.Tests/AsmResolver.DotNet.Dynamic.Tests.csproj @@ -8,10 +8,10 @@ - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/AsmResolver.DotNet.Dynamic.Tests/DynamicMethodDefinitionTest.cs b/test/AsmResolver.DotNet.Dynamic.Tests/DynamicMethodDefinitionTest.cs index 229a27713..c649f621d 100644 --- a/test/AsmResolver.DotNet.Dynamic.Tests/DynamicMethodDefinitionTest.cs +++ b/test/AsmResolver.DotNet.Dynamic.Tests/DynamicMethodDefinitionTest.cs @@ -197,9 +197,11 @@ public void ReadDynamicMethodInitializedByDynamicILInfoWithTokens() Assert.Equal("WriteLine", reference.Name); } - [Fact] + [SkippableFact] public void ReadDynamicMethodInitializedByDynamicILInfoWithLocals() { + Skip.IfNot(DynamicTypeSignatureResolver.IsSupported, "Current platform does not support dynamic type resolution."); + // Create new dynamic method. var method = new DynamicMethod("Test", typeof(void), Type.EmptyTypes); var info = method.GetDynamicILInfo(); diff --git a/test/AsmResolver.DotNet.Tests/AsmResolver.DotNet.Tests.csproj b/test/AsmResolver.DotNet.Tests/AsmResolver.DotNet.Tests.csproj index 1f798a757..c9a4eb306 100644 --- a/test/AsmResolver.DotNet.Tests/AsmResolver.DotNet.Tests.csproj +++ b/test/AsmResolver.DotNet.Tests/AsmResolver.DotNet.Tests.csproj @@ -6,13 +6,15 @@ false disable + + 11 - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/AsmResolver.DotNet.Tests/Builder/TokenPreservation/TypeRefTokenPreservationTest.cs b/test/AsmResolver.DotNet.Tests/Builder/TokenPreservation/TypeRefTokenPreservationTest.cs index 45eabbd0d..824b3d8b1 100644 --- a/test/AsmResolver.DotNet.Tests/Builder/TokenPreservation/TypeRefTokenPreservationTest.cs +++ b/test/AsmResolver.DotNet.Tests/Builder/TokenPreservation/TypeRefTokenPreservationTest.cs @@ -100,6 +100,36 @@ public void PreserveDuplicatedTypeRefs() newObjectReferences.Select(r => r.MetadataToken).ToHashSet()); } + [Fact] + public void PreserveDuplicatedTypeRefsInBaseType() + { + // Prepare temp module with two references to System.Object + var module = new ModuleDefinition("Test"); + var assembly = new AssemblyDefinition("Test", new Version(1, 0, 0, 0)); + assembly.Modules.Add(module); + + var ref1 = (TypeReference) module.CorLibTypeFactory.CorLibScope + .CreateTypeReference("System", "Object") + .ImportWith(module.DefaultImporter); + var ref2 = (TypeReference) module.CorLibTypeFactory.CorLibScope + .CreateTypeReference("System", "Object") + .ImportWith(module.DefaultImporter); + + // Force assign new tokens to instruct builder that both type references need to be added. + module.TokenAllocator.AssignNextAvailableToken(ref1); + module.TokenAllocator.AssignNextAvailableToken(ref2); + + module.TopLevelTypes.Add(new TypeDefinition(null, "A", TypeAttributes.Public, ref1)); + module.TopLevelTypes.Add(new TypeDefinition(null, "B", TypeAttributes.Public, ref2)); + + // Rebuild. + var image = module.ToPEImage(new ManagedPEImageBuilder(MetadataBuilderFlags.PreserveTypeReferenceIndices)); + + // Verify that both object references are still there. + var newModule = ModuleDefinition.FromImage(image); + Assert.Equal(2, newModule.GetImportedTypeReferences().Count(t => t.Name == "Object")); + } + [Fact] public void PreserveNestedTypeRefOrdering() { @@ -113,5 +143,6 @@ public void PreserveNestedTypeRefOrdering() Assert.Equal(originalTypeRefs, newTypeRefs.Take(originalTypeRefs.Count), Comparer); } + } } diff --git a/test/AsmResolver.DotNet.Tests/Bundles/BundleManifestTest.cs b/test/AsmResolver.DotNet.Tests/Bundles/BundleManifestTest.cs index a4f8b60cd..6cf4bc39e 100644 --- a/test/AsmResolver.DotNet.Tests/Bundles/BundleManifestTest.cs +++ b/test/AsmResolver.DotNet.Tests/Bundles/BundleManifestTest.cs @@ -69,7 +69,7 @@ public void WriteBundleManifestV1Windows() BundleManifest.FromBytes(Properties.Resources.HelloWorld_SingleFile_V1), "3.1", "HelloWorld.dll", - $"Hello, World!{Environment.NewLine}"); + "Hello, World!\n"); } [SkippableFact] @@ -80,7 +80,7 @@ public void WriteBundleManifestV2Windows() BundleManifest.FromBytes(Properties.Resources.HelloWorld_SingleFile_V2), "5.0", "HelloWorld.dll", - $"Hello, World!{Environment.NewLine}"); + "Hello, World!\n"); } [SkippableFact] @@ -91,7 +91,7 @@ public void WriteBundleManifestV6Windows() BundleManifest.FromBytes(Properties.Resources.HelloWorld_SingleFile_V6), "6.0", "HelloWorld.dll", - $"Hello, World!{Environment.NewLine}"); + "Hello, World!\n"); } [SkippableFact] @@ -153,7 +153,7 @@ public void WriteWithWin32Resources() .GetRunner() .RunAndCaptureOutput("HelloWorld.exe", stream.ToArray()); - Assert.Equal($"Hello, World!{Environment.NewLine}", output); + Assert.Equal("Hello, World!\n", output); // Verify that resources were added properly. var newImage = PEImage.FromBytes(stream.ToArray()); @@ -267,7 +267,7 @@ private void AssertPatchAndRepackageChangesOutput( className, methodName); - Assert.Equal($"Hello, Mars!{Environment.NewLine}", output); + Assert.Equal("Hello, Mars!\n", output); } private void AssertWriteManifestWindowsPreservesOutput( @@ -297,7 +297,7 @@ private void AssertWriteManifestWindowsPreservesOutput( className, methodName); - Assert.Equal(expectedOutput, output); + Assert.Equal(expectedOutput.Replace("\r\n", "\n"), output); } private static void DeleteTempExtractionDirectory(BundleManifest manifest, string fileName) { diff --git a/test/AsmResolver.Tests/Listeners/CustomMemberClonerListener.cs b/test/AsmResolver.DotNet.Tests/Cloning/CustomMemberClonerListener.cs similarity index 100% rename from test/AsmResolver.Tests/Listeners/CustomMemberClonerListener.cs rename to test/AsmResolver.DotNet.Tests/Cloning/CustomMemberClonerListener.cs diff --git a/test/AsmResolver.DotNet.Tests/Cloning/MetadataClonerTest.cs b/test/AsmResolver.DotNet.Tests/Cloning/MetadataClonerTest.cs index 88b68064c..aa9d30ad8 100644 --- a/test/AsmResolver.DotNet.Tests/Cloning/MetadataClonerTest.cs +++ b/test/AsmResolver.DotNet.Tests/Cloning/MetadataClonerTest.cs @@ -4,6 +4,8 @@ using System.Reflection; using AsmResolver.DotNet.Cloning; using AsmResolver.DotNet.Signatures; +using AsmResolver.DotNet.Signatures.Types; +using AsmResolver.DotNet.TestCases.CustomAttributes; using AsmResolver.DotNet.TestCases.Events; using AsmResolver.DotNet.TestCases.Fields; using AsmResolver.DotNet.TestCases.Generics; @@ -126,7 +128,7 @@ public void CloneHelloWorldProgramType() targetModule.ManagedEntryPointMethod = (MethodDefinition) result.ClonedMembers.First(m => m.Name == "Main"); _fixture .GetRunner() - .RebuildAndRun(targetModule, "HelloWorld.exe", "Hello World!" + Environment.NewLine); + .RebuildAndRun(targetModule, "HelloWorld.exe", "Hello World!\n"); } [Fact] @@ -408,5 +410,31 @@ public void CloneAndInjectAndAssignToken() Assert.All(result.ClonedTopLevelTypes, t => Assert.Contains(t, targetModule.TopLevelTypes)); Assert.All(result.ClonedMembers, m => Assert.NotEqual(0u, ((IMetadataMember) m).MetadataToken.Rid)); } + + [Fact] + public void CloneIncludedTypeArgument() + { + // https://github.com/Washi1337/AsmResolver/issues/482 + + var sourceModule = ModuleDefinition.FromFile(typeof(CustomAttributesTestClass).Assembly.Location); + var targetModule = PrepareTempModule(); + + var type = sourceModule.LookupMember(typeof(TestEnum).MetadataToken); + var method = sourceModule.LookupMember(typeof(CustomAttributesTestClass) + .GetMethod(nameof(CustomAttributesTestClass.FIxedLocalTypeArgument))! + .MetadataToken); + + var result = new MemberCloner(targetModule) + .Include(type) + .Include(method) + .AddListener(new InjectTypeClonerListener(targetModule)) + .Clone(); + + var newType = result.GetClonedMember(type); + var newMethod = result.GetClonedMember(method); + + var newArgument = Assert.IsAssignableFrom(newMethod.CustomAttributes[0].Signature!.FixedArguments[0].Element); + Assert.Equal(newType, newArgument, _signatureComparer); + } } } diff --git a/test/AsmResolver.DotNet.Tests/Code/Native/NativeMethodBodyTest.cs b/test/AsmResolver.DotNet.Tests/Code/Native/NativeMethodBodyTest.cs index 5a12c217c..7342b7e07 100644 --- a/test/AsmResolver.DotNet.Tests/Code/Native/NativeMethodBodyTest.cs +++ b/test/AsmResolver.DotNet.Tests/Code/Native/NativeMethodBodyTest.cs @@ -299,7 +299,7 @@ public void NativeBodyWithLocalSymbols(bool is32Bit, byte[] movInstruction, uint // Verify. _fixture .GetRunner() - .RebuildAndRun(body.Owner.Module!, "StringPointer.exe", $"Hello, world!{Environment.NewLine}"); + .RebuildAndRun(body.Owner.Module!, "StringPointer.exe", "Hello, world!\n"); } [SkippableTheory] @@ -341,7 +341,7 @@ public void NativeBodyWithGlobalSymbol(bool is32Bit, byte[] movInstruction, uint // Verify. _fixture .GetRunner() - .RebuildAndRun(file, "StringPointer.exe", $"Hello, world!{Environment.NewLine}"); + .RebuildAndRun(file, "StringPointer.exe", "Hello, world!\n"); } private static void InjectCallToNativeBody(NativeMethodBody body, ISymbol messageSymbol, uint fixupOffset, AddressFixupType fixupType) diff --git a/test/AsmResolver.DotNet.Tests/CustomAttributeTest.cs b/test/AsmResolver.DotNet.Tests/CustomAttributeTest.cs index 352ddb998..79e4faa9c 100644 --- a/test/AsmResolver.DotNet.Tests/CustomAttributeTest.cs +++ b/test/AsmResolver.DotNet.Tests/CustomAttributeTest.cs @@ -85,7 +85,7 @@ private static CustomAttribute GetCustomAttributeTestCase( attributeName += "`1"; var attribute = method.CustomAttributes - .First(c => c.Constructor!.DeclaringType!.Name.Value.StartsWith(attributeName)); + .First(c => c.Constructor!.DeclaringType!.Name!.Value.StartsWith(attributeName)); if (access) { @@ -197,10 +197,13 @@ public void FixedComplexTypeArgument(bool rebuild, bool access) var argument = attribute.Signature.FixedArguments[0]; var factory = attribute.Constructor!.Module!.CorLibTypeFactory; - var listRef = new TypeReference(factory.CorLibScope, "System.Collections.Generic", "KeyValuePair`2"); - var instance = new GenericInstanceTypeSignature(listRef, false, - new SzArrayTypeSignature(factory.String), - new SzArrayTypeSignature(factory.Int32)); + var instance = factory.CorLibScope + .CreateTypeReference("System.Collections.Generic", "KeyValuePair`2") + .MakeGenericInstanceType( + false, + factory.String.MakeSzArrayType(), + factory.Int32.MakeSzArrayType() + ); Assert.Equal(instance, argument.Element as TypeSignature, _comparer); } @@ -293,7 +296,7 @@ public void GenericTypeArgument(bool rebuild, bool access) var module = attribute.Constructor!.Module!; var nestedClass = (TypeDefinition) module.LookupMember(typeof(TestGenericType<>).MetadataToken); - var expected = new GenericInstanceTypeSignature(nestedClass, false, module.CorLibTypeFactory.Object); + var expected = nestedClass.MakeGenericInstanceType(false, module.CorLibTypeFactory.Object); var element = Assert.IsAssignableFrom(argument.Element); Assert.Equal(expected, element, _comparer); @@ -312,9 +315,9 @@ public void ArrayGenericTypeArgument(bool rebuild, bool access) var module = attribute.Constructor!.Module!; var nestedClass = (TypeDefinition) module.LookupMember(typeof(TestGenericType<>).MetadataToken); - var expected = new SzArrayTypeSignature( - new GenericInstanceTypeSignature(nestedClass, false, module.CorLibTypeFactory.Object) - ); + var expected = nestedClass + .MakeGenericInstanceType(false, module.CorLibTypeFactory.Object) + .MakeSzArrayType(); var element = Assert.IsAssignableFrom(argument.Element); Assert.Equal(expected, element, _comparer); @@ -413,7 +416,7 @@ public void FixedInt32EmptyArrayAsObject(bool rebuild, bool access) var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.FixedInt32ArrayAsObjectEmptyArgument),rebuild, access); var argument = attribute.Signature!.FixedArguments[0]; - var boxedArgument =Assert.IsAssignableFrom(argument.Element); + var boxedArgument = Assert.IsAssignableFrom(argument.Element); Assert.Equal(Array.Empty(), boxedArgument.Value); } diff --git a/test/AsmResolver.DotNet.Tests/ManifestResourceTest.cs b/test/AsmResolver.DotNet.Tests/ManifestResourceTest.cs index 074e4f4b8..addfc14db 100644 --- a/test/AsmResolver.DotNet.Tests/ManifestResourceTest.cs +++ b/test/AsmResolver.DotNet.Tests/ManifestResourceTest.cs @@ -1,14 +1,25 @@ using System.IO; using System.Linq; using System.Text; +using AsmResolver.DotNet.Builder; +using AsmResolver.PE.DotNet.Builder; +using AsmResolver.PE.DotNet.Metadata.Tables; using AsmResolver.PE.DotNet.Metadata.Tables.Rows; +using AsmResolver.Tests.Runners; using Xunit; using TestCaseResources = AsmResolver.DotNet.TestCases.Resources.Resources; namespace AsmResolver.DotNet.Tests { - public class ManifestResourceTest + public class ManifestResourceTest : IClassFixture { + private readonly TemporaryDirectoryFixture _fixture; + + public ManifestResourceTest(TemporaryDirectoryFixture fixture) + { + _fixture = fixture; + } + [Fact] public void ReadEmbeddedResource1Data() { @@ -69,5 +80,96 @@ public void PersistentDataReader() Assert.True(resource.TryGetReader(out var reader)); Assert.Equal(contents, reader.ReadToEnd()); } + + [Fact] + public void PersistentUniqueResourceData() + { + var module = ModuleDefinition.FromBytes(Properties.Resources.DupResource); + + // Create three unique resources. + module.Resources.Add(new ManifestResource( + "resource1", + ManifestResourceAttributes.Public, + new DataSegment(new byte[] {1, 2, 3, 4})) + ); + module.Resources.Add(new ManifestResource( + "resource2", + ManifestResourceAttributes.Public, + new DataSegment(new byte[] {5, 6, 7, 8})) + ); + module.Resources.Add(new ManifestResource( + "resource3", + ManifestResourceAttributes.Public, + new DataSegment(new byte[] {9, 10, 11, 12})) + ); + + // Verify program returns correct data. + _fixture + .GetRunner() + .RebuildAndRun(module, "DupResource.dll", + """ + resource1: 01020304 + resource2: 05060708 + resource3: 090A0B0C + + """); + } + + [Theory] + [InlineData(MetadataBuilderFlags.None)] + [InlineData(MetadataBuilderFlags.NoResourceDataDeduplication)] + public void PersistentIdenticalResourceData(MetadataBuilderFlags flags) + { + var module = ModuleDefinition.FromBytes(Properties.Resources.DupResource); + + // Create two identical resources and one unique resource. + module.Resources.Add(new ManifestResource( + "resource1", + ManifestResourceAttributes.Public, + new DataSegment(new byte[] {1, 2, 3, 4})) + ); + module.Resources.Add(new ManifestResource( + "resource2", + ManifestResourceAttributes.Public, + new DataSegment(new byte[] {1, 2, 3, 4})) + ); + module.Resources.Add(new ManifestResource( + "resource3", + ManifestResourceAttributes.Public, + new DataSegment(new byte[] {9, 10, 11, 12})) + ); + + // Build image. + var image = module.ToPEImage(new ManagedPEImageBuilder(flags)); + + var table = image.DotNetDirectory!.Metadata! + .GetStream() + .GetTable(); + + if ((flags & MetadataBuilderFlags.NoResourceDataDeduplication) != 0) + { + // Without deduplication, all offsets should be different. + Assert.NotEqual(table[0].Offset, table[1].Offset); + Assert.NotEqual(table[0].Offset, table[2].Offset); + } + else + { + // With deduplication, resources with same data should share data offset. + Assert.Equal(table[0].Offset, table[1].Offset); + Assert.NotEqual(table[0].Offset, table[2].Offset); + } + + // Verify program returns correct data. + var file = new ManagedPEFileBuilder().CreateFile(image); + _fixture + .GetRunner() + .RebuildAndRun(file, $"DupResource_{flags}.dll", + """ + resource1: 01020304 + resource2: 01020304 + resource3: 090A0B0C + + """); + } } } diff --git a/test/AsmResolver.DotNet.Tests/MethodDefinitionTest.cs b/test/AsmResolver.DotNet.Tests/MethodDefinitionTest.cs index ad21af54f..be30f21fc 100644 --- a/test/AsmResolver.DotNet.Tests/MethodDefinitionTest.cs +++ b/test/AsmResolver.DotNet.Tests/MethodDefinitionTest.cs @@ -566,5 +566,28 @@ public void MethodFullNameTests(string methodName, string expectedFullName) Assert.Equal(expectedFullName, method.FullName); } + + [Fact] + public void CreateParameterlessConstructor() + { + var module = ModuleDefinition.FromFile(typeof(Constructors).Assembly.Location); + var ctor = MethodDefinition.CreateConstructor(module); + + Assert.True(ctor.IsConstructor); + Assert.Empty(ctor.Parameters); + Assert.NotNull(ctor.CilMethodBody); + Assert.Equal(CilOpCodes.Ret, Assert.Single(ctor.CilMethodBody.Instructions).OpCode); + } + + [Fact] + public void CreateConstructor() + { + var module = ModuleDefinition.FromFile(typeof(Constructors).Assembly.Location); + var factory = module.CorLibTypeFactory; + var ctor = MethodDefinition.CreateConstructor(module, factory.Int32, factory.Double); + + Assert.True(ctor.IsConstructor); + Assert.Equal(new[] {factory.Int32, factory.Double}, ctor.Parameters.Select(x => x.ParameterType)); + } } } diff --git a/test/AsmResolver.DotNet.Tests/Properties/Resources.Designer.cs b/test/AsmResolver.DotNet.Tests/Properties/Resources.Designer.cs index 5f2f0400f..e36534bf9 100644 --- a/test/AsmResolver.DotNet.Tests/Properties/Resources.Designer.cs +++ b/test/AsmResolver.DotNet.Tests/Properties/Resources.Designer.cs @@ -408,5 +408,12 @@ public static byte[] TypeRefNullScope_ExportedType { return ((byte[])(obj)); } } + + public static byte[] DupResource { + get { + object obj = ResourceManager.GetObject("DupResource", resourceCulture); + return ((byte[])(obj)); + } + } } } diff --git a/test/AsmResolver.DotNet.Tests/Properties/Resources.resx b/test/AsmResolver.DotNet.Tests/Properties/Resources.resx index 5edb05e41..d0c0c7189 100644 --- a/test/AsmResolver.DotNet.Tests/Properties/Resources.resx +++ b/test/AsmResolver.DotNet.Tests/Properties/Resources.resx @@ -174,4 +174,7 @@ ..\Resources\TypeRefNullScope_ExportedType.exe;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + ..\Resources\DupResource.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + diff --git a/test/AsmResolver.DotNet.Tests/ReferenceImporterTest.cs b/test/AsmResolver.DotNet.Tests/ReferenceImporterTest.cs index 2bbd52f5b..4f745efc9 100644 --- a/test/AsmResolver.DotNet.Tests/ReferenceImporterTest.cs +++ b/test/AsmResolver.DotNet.Tests/ReferenceImporterTest.cs @@ -421,9 +421,10 @@ public void ImportCustomModifierTypeWithNonImportedModifierTypeShouldResultInNew [Fact] public void ImportFullyImportedCustomModifierTypeShouldResultInSameInstance() { - var signature = new TypeReference(_module, _dummyAssembly, "SomeNamespace", "SomeType") + var assembly = _importer.ImportScope(_dummyAssembly); + var signature = new TypeReference(_module, assembly, "SomeNamespace", "SomeType") .ToTypeSignature() - .MakeModifierType(new TypeReference(_module, _dummyAssembly, "SomeNamespace", "SomeModifierType"), true); + .MakeModifierType(new TypeReference(_module, assembly, "SomeNamespace", "SomeModifierType"), true); var imported = _importer.ImportTypeSignature(signature); @@ -470,10 +471,11 @@ public void ImportFunctionPointerTypeWithNonImportedReturnTypeShouldResultInNewI [Fact] public void ImportFullyImportedFunctionPointerTypeShouldResultInSameInstance() { + var assembly = _importer.ImportScope(_dummyAssembly); var signature = MethodSignature .CreateStatic( _module.CorLibTypeFactory.Void, - new TypeReference(_module, _dummyAssembly, "SomeNamespace", "SomeType").ToTypeSignature()) + new TypeReference(_module, assembly, "SomeNamespace", "SomeType").ToTypeSignature()) .MakeFunctionPointerType(); var imported = _importer.ImportTypeSignature(signature); diff --git a/test/AsmResolver.DotNet.Tests/Resources/DupResource.dll b/test/AsmResolver.DotNet.Tests/Resources/DupResource.dll new file mode 100644 index 000000000..eb32ddd9f Binary files /dev/null and b/test/AsmResolver.DotNet.Tests/Resources/DupResource.dll differ diff --git a/test/AsmResolver.DotNet.Tests/Signatures/TypeNameParserTest.cs b/test/AsmResolver.DotNet.Tests/Signatures/TypeNameParserTest.cs index c79e85572..25f841ad8 100644 --- a/test/AsmResolver.DotNet.Tests/Signatures/TypeNameParserTest.cs +++ b/test/AsmResolver.DotNet.Tests/Signatures/TypeNameParserTest.cs @@ -82,9 +82,7 @@ public void TypeWithAssemblyName() var assemblyRef = new AssemblyReference("MyAssembly", new Version(1, 2, 3, 4)); var expected = new TypeReference(assemblyRef, ns, name).ToTypeSignature(); - - var actual = TypeNameParser.Parse(_module, - $"{ns}.{name}, {assemblyRef.FullName}"); + var actual = TypeNameParser.Parse(_module, $"{ns}.{name}, {assemblyRef.FullName}"); Assert.Equal(expected, actual, _comparer); } @@ -141,7 +139,7 @@ public void GenericTypeSingleBrackets() var elementType = new TypeReference(_module, ns, name); var argumentType = _module.CorLibTypeFactory.Object; - var expected = new GenericInstanceTypeSignature(elementType, false, argumentType); + var expected = elementType.MakeGenericInstanceType(false, argumentType); var actual = TypeNameParser.Parse(_module, $"{ns}.{name}[{argumentType.Namespace}.{argumentType.Name}]"); Assert.Equal(expected, actual, _comparer); @@ -299,5 +297,17 @@ public void ReadTypeShouldUseNewScopeInstanceIfNotImportedYet() "SomeNamespace.SomeType, SomeAssembly, Version=1.2.3.4, Culture=neutral, PublicKeyToken=0123456789abcdef"); Assert.DoesNotContain(type.Scope!.GetAssembly(), _module.AssemblyReferences); } + + [Fact] + public void ReadTypeNameFromLocalModuleShouldResultInResolvableType() + { + var module = ModuleDefinition.FromFile(typeof(TypeNameParserTest).Assembly.Location); + var type = TypeNameParser + .Parse(module, typeof(TypeNameParserTest).AssemblyQualifiedName!) + .GetUnderlyingTypeDefOrRef()!; + + Assert.NotNull(type.Resolve()); + Assert.NotNull(type.ImportWith(module.DefaultImporter).Resolve()); + } } } diff --git a/test/AsmResolver.DotNet.Tests/TestUtils.cs b/test/AsmResolver.DotNet.Tests/TestUtils.cs index ab07914f1..45896d936 100644 --- a/test/AsmResolver.DotNet.Tests/TestUtils.cs +++ b/test/AsmResolver.DotNet.Tests/TestUtils.cs @@ -16,7 +16,7 @@ public static void RebuildAndRun(this PERunner runner, ModuleDefinition module, string path = runner.GetTestExecutablePath(testClass, testMethod, fileName); module.Write(path); string actualOutput = runner.RunAndCaptureOutput(path, null, timeout); - Assert.Equal(expectedOutput, actualOutput); + Assert.Equal(expectedOutput.Replace("\r\n", "\n"), actualOutput); } public static FieldDefinition FindInitializerField(this FieldDefinition field) diff --git a/test/AsmResolver.DotNet.Tests/TypeDefinitionTest.cs b/test/AsmResolver.DotNet.Tests/TypeDefinitionTest.cs index 4f321ad7b..4e2ebfa28 100644 --- a/test/AsmResolver.DotNet.Tests/TypeDefinitionTest.cs +++ b/test/AsmResolver.DotNet.Tests/TypeDefinitionTest.cs @@ -13,7 +13,6 @@ using AsmResolver.DotNet.TestCases.Properties; using AsmResolver.DotNet.TestCases.Types; using AsmResolver.DotNet.TestCases.Types.Structs; -using AsmResolver.PE.DotNet.Metadata.Strings; using AsmResolver.PE.DotNet.Metadata.Tables; using AsmResolver.PE.DotNet.Metadata.Tables.Rows; using Xunit; @@ -27,7 +26,7 @@ public class TypeDefinitionTest private TypeDefinition RebuildAndLookup(TypeDefinition type) { var stream = new MemoryStream(); - type.Module.Write(stream); + type.Module!.Write(stream); var newModule = ModuleDefinition.FromBytes(stream.ToArray()); return newModule.TopLevelTypes.FirstOrDefault(t => t.FullName == type.FullName); @@ -489,7 +488,7 @@ public void ReadInterfaces() Assert.Equal(new HashSet(new Utf8String[] { nameof(IInterface1), nameof(IInterface2), - }), new HashSet(type.Interfaces.Select(i => i.Interface.Name))); + }), new HashSet(type.Interfaces.Select(i => i.Interface?.Name))); } [Fact] @@ -501,7 +500,7 @@ public void PersistentInterfaces() Assert.Equal(new HashSet(new Utf8String[] { nameof(IInterface1), nameof(IInterface2), - }), new HashSet(newType.Interfaces.Select(i => i.Interface.Name))); + }), new HashSet(newType.Interfaces.Select(i => i.Interface?.Name))); } [Fact] @@ -550,8 +549,18 @@ public void InheritanceMultipleLevels() var module = ModuleDefinition.FromFile(typeof(DerivedDerivedClass).Assembly.Location); var type = module.TopLevelTypes.First(t => t.Name == nameof(DerivedDerivedClass)); - Assert.True(type.InheritsFrom(typeof(AbstractClass).FullName)); - Assert.False(type.InheritsFrom(typeof(Class).FullName)); + Assert.True(type.InheritsFrom(typeof(AbstractClass).FullName!)); + Assert.False(type.InheritsFrom(typeof(Class).FullName!)); + } + + [Fact] + public void InheritanceMultipleLevelsTypeOf() + { + var module = ModuleDefinition.FromFile(typeof(DerivedDerivedClass).Assembly.Location); + var type = module.TopLevelTypes.First(t => t.Name == nameof(DerivedDerivedClass)); + + Assert.True(type.InheritsFrom(typeof(AbstractClass).Namespace, nameof(AbstractClass))); + Assert.False(type.InheritsFrom(typeof(Class).Namespace, nameof(Class))); } [Fact] @@ -560,17 +569,29 @@ public void InterfaceImplementedFromInheritanceHierarchy() var module = ModuleDefinition.FromFile(typeof(DerivedInterfaceImplementations).Assembly.Location); var type = module.TopLevelTypes.First(t => t.Name == nameof(DerivedInterfaceImplementations)); - Assert.True(type.Implements(typeof(IInterface1).FullName)); - Assert.True(type.Implements(typeof(IInterface2).FullName)); - Assert.True(type.Implements(typeof(IInterface3).FullName)); - Assert.False(type.Implements(typeof(IInterface4).FullName)); + Assert.True(type.Implements(typeof(IInterface1).FullName!)); + Assert.True(type.Implements(typeof(IInterface2).FullName!)); + Assert.True(type.Implements(typeof(IInterface3).FullName!)); + Assert.False(type.Implements(typeof(IInterface4).FullName!)); + } + + [Fact] + public void InterfaceImplementedFromInheritanceHierarchyTypeOf() + { + var module = ModuleDefinition.FromFile(typeof(DerivedInterfaceImplementations).Assembly.Location); + var type = module.TopLevelTypes.First(t => t.Name == nameof(DerivedInterfaceImplementations)); + + Assert.True(type.Implements(typeof(IInterface1).Namespace, nameof(IInterface1))); + Assert.True(type.Implements(typeof(IInterface2).Namespace, nameof(IInterface2))); + Assert.True(type.Implements(typeof(IInterface3).Namespace, nameof(IInterface3))); + Assert.False(type.Implements(typeof(IInterface4).Namespace, nameof(IInterface4))); } [Fact] public void CorLibTypeDefinitionToSignatureShouldResultInCorLibTypeSignature() { var module = new ModuleDefinition("Test"); - var type = module.CorLibTypeFactory.Object.Resolve(); + var type = module.CorLibTypeFactory.Object.Resolve()!; var signature = type.ToTypeSignature(); var corlibType = Assert.IsAssignableFrom(signature); Assert.Equal(ElementType.Object, corlibType.ElementType); @@ -615,13 +636,92 @@ public void AddTypeWithCorLibBaseTypeToAssemblyWithCorLibTypeReferenceInAttribut public void ReadIsByRefLike() { var resolver = new DotNetCoreAssemblyResolver(new Version(5, 0)); - var corLib = resolver.Resolve(KnownCorLibs.SystemPrivateCoreLib_v5_0_0_0); + var corLib = resolver.Resolve(KnownCorLibs.SystemPrivateCoreLib_v5_0_0_0)!; - var intType = corLib.ManifestModule.TopLevelTypes.First(t => t.Name == "Int32"); + var intType = corLib.ManifestModule!.TopLevelTypes.First(t => t.Name == "Int32"); var spanType = corLib.ManifestModule.TopLevelTypes.First(t => t.Name == "Span`1"); Assert.False(intType.IsByRefLike); Assert.True(spanType.IsByRefLike); } + + [Fact] + public void GetStaticConstructor() + { + var module = ModuleDefinition.FromFile(typeof(Constructors).Assembly.Location); + + var type1 = module.LookupMember(typeof(Constructors).MetadataToken); + var cctor = type1.GetStaticConstructor(); + Assert.NotNull(cctor); + Assert.True(cctor.IsStatic); + Assert.True(cctor.IsConstructor); + + var type2 = module.LookupMember(typeof(NoStaticConstructor).MetadataToken); + Assert.Null(type2.GetStaticConstructor()); + } + + [Fact] + public void GetOrCreateStaticConstructor() + { + var module = ModuleDefinition.FromFile(typeof(Constructors).Assembly.Location); + var type1 = module.LookupMember(typeof(Constructors).MetadataToken); + + // If cctor already exists, we expect this to be returned. + var cctor = type1.GetStaticConstructor(); + Assert.NotNull(cctor); + Assert.Same(cctor, type1.GetOrCreateStaticConstructor()); + + var type2 = module.LookupMember(typeof(NoStaticConstructor).MetadataToken); + Assert.Null(type2.GetStaticConstructor()); + + // If cctor doesn't exist yet, it should be added. + cctor = type2.GetOrCreateStaticConstructor(); + Assert.NotNull(cctor); + Assert.Same(type2, cctor.DeclaringType); + Assert.Same(cctor, type2.GetOrCreateStaticConstructor()); + Assert.True(cctor.IsStatic); + Assert.True(cctor.IsConstructor); + } + + [Fact] + public void GetParameterlessConstructor() + { + var module = ModuleDefinition.FromFile(typeof(Constructors).Assembly.Location); + var type = module.LookupMember(typeof(Constructors).MetadataToken); + + var ctor = type.GetConstructor(); + Assert.NotNull(ctor); + Assert.False(ctor.IsStatic); + Assert.True(ctor.IsConstructor); + Assert.Empty(ctor.Parameters); + } + + [Theory] + [InlineData(new[] {ElementType.I4, ElementType.I4})] + [InlineData(new[] {ElementType.I4, ElementType.String})] + [InlineData(new[] {ElementType.I4, ElementType.String, ElementType.R8})] + public void GetParametersConstructor(ElementType[] types) + { + var module = ModuleDefinition.FromFile(typeof(Constructors).Assembly.Location); + var type = module.LookupMember(typeof(Constructors).MetadataToken); + + var signatures = types.Select(x => (TypeSignature) module.CorLibTypeFactory.FromElementType(x)).ToArray(); + var ctor = type.GetConstructor(signatures); + Assert.NotNull(ctor); + Assert.False(ctor.IsStatic); + Assert.True(ctor.IsConstructor); + Assert.Equal(signatures, ctor.Signature!.ParameterTypes); + } + + [Fact] + public void GetNonExistingConstructorShouldReturnNull() + { + var module = ModuleDefinition.FromFile(typeof(Constructors).Assembly.Location); + var type = module.LookupMember(typeof(Constructors).MetadataToken); + var factory = module.CorLibTypeFactory; + + Assert.Null(type.GetConstructor(factory.String)); + Assert.Null(type.GetConstructor(factory.String, factory.String)); + } } } diff --git a/test/AsmResolver.PE.File.Tests/AsmResolver.PE.File.Tests.csproj b/test/AsmResolver.PE.File.Tests/AsmResolver.PE.File.Tests.csproj index 5da60fb15..99e8aa174 100644 --- a/test/AsmResolver.PE.File.Tests/AsmResolver.PE.File.Tests.csproj +++ b/test/AsmResolver.PE.File.Tests/AsmResolver.PE.File.Tests.csproj @@ -9,10 +9,10 @@ - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/AsmResolver.PE.File.Tests/PEFileTest.cs b/test/AsmResolver.PE.File.Tests/PEFileTest.cs index b1410df05..5fcb3b084 100644 --- a/test/AsmResolver.PE.File.Tests/PEFileTest.cs +++ b/test/AsmResolver.PE.File.Tests/PEFileTest.cs @@ -53,7 +53,7 @@ public void RebuildNetPENoChange() var peFile = PEFile.FromBytes(Properties.Resources.HelloWorld); _fixture .GetRunner() - .RebuildAndRun(peFile, "HelloWorld", "Hello World!" + Environment.NewLine); + .RebuildAndRun(peFile, "HelloWorld", "Hello World!\n"); } [Fact] @@ -74,7 +74,7 @@ public void RebuildNetPEAddSection() // Rebuild and check if file is still runnable. _fixture .GetRunner() - .RebuildAndRun(peFile, fileName, "Hello World!" + Environment.NewLine); + .RebuildAndRun(peFile, fileName, "Hello World!\n"); // Read the new file. var newPEFile = PEFile.FromFile(_fixture diff --git a/test/AsmResolver.PE.Tests/AsmResolver.PE.Tests.csproj b/test/AsmResolver.PE.Tests/AsmResolver.PE.Tests.csproj index ba76c1c56..01de3b1e4 100644 --- a/test/AsmResolver.PE.Tests/AsmResolver.PE.Tests.csproj +++ b/test/AsmResolver.PE.Tests/AsmResolver.PE.Tests.csproj @@ -16,10 +16,10 @@ - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/AsmResolver.PE.Tests/DotNet/Builder/ManagedPEFileBuilderTest.cs b/test/AsmResolver.PE.Tests/DotNet/Builder/ManagedPEFileBuilderTest.cs index 1d2ccda32..fdd13ff56 100644 --- a/test/AsmResolver.PE.Tests/DotNet/Builder/ManagedPEFileBuilderTest.cs +++ b/test/AsmResolver.PE.Tests/DotNet/Builder/ManagedPEFileBuilderTest.cs @@ -36,7 +36,7 @@ public void HelloWorldRebuild32BitNoChange() // Verify _fixture .GetRunner() - .RebuildAndRun(peFile, "HelloWorld", "Hello World!" + Environment.NewLine); + .RebuildAndRun(peFile, "HelloWorld", "Hello World!\n"); } [Fact] @@ -52,7 +52,7 @@ public void HelloWorldRebuild64BitNoChange() // Verify _fixture .GetRunner() - .RebuildAndRun(peFile, "HelloWorld", "Hello World!" + Environment.NewLine); + .RebuildAndRun(peFile, "HelloWorld", "Hello World!\n"); } [Fact] @@ -72,7 +72,7 @@ public void HelloWorld32BitTo64Bit() // Verify _fixture .GetRunner() - .RebuildAndRun(peFile, "HelloWorld", "Hello World!" + Environment.NewLine); + .RebuildAndRun(peFile, "HelloWorld", "Hello World!\n"); } [Fact] @@ -92,7 +92,7 @@ public void HelloWorld64BitTo32Bit() // Verify _fixture .GetRunner() - .RebuildAndRun(peFile, "HelloWorld", "Hello World!" + Environment.NewLine); + .RebuildAndRun(peFile, "HelloWorld", "Hello World!\n"); } [Fact] diff --git a/test/AsmResolver.PE.Tests/DotNet/ReadyToRun/MethodEntryPointTest.cs b/test/AsmResolver.PE.Tests/DotNet/ReadyToRun/MethodEntryPointTest.cs new file mode 100644 index 000000000..a03acf933 --- /dev/null +++ b/test/AsmResolver.PE.Tests/DotNet/ReadyToRun/MethodEntryPointTest.cs @@ -0,0 +1,59 @@ +using AsmResolver.IO; +using AsmResolver.PE.DotNet.ReadyToRun; +using Xunit; + +namespace AsmResolver.PE.Tests.DotNet.ReadyToRun +{ + public class MethodEntryPointTest + { + [Fact] + public void NoFixups() + { + var entryPoint = new MethodEntryPoint(1337); + + entryPoint.UpdateOffsets(new RelocationParameters(0, 0)); + var reader = new BinaryStreamReader(entryPoint.WriteIntoArray()); + + var newEntryPoint = MethodEntryPoint.FromReader(ref reader); + Assert.Equal(1337u, newEntryPoint.RuntimeFunctionIndex); + Assert.Empty(newEntryPoint.Fixups); + } + + [Theory] + [InlineData(0, 0)] + [InlineData(1337, 0)] + [InlineData(0, 1337)] + [InlineData(1337, 1338)] + public void SingleFixup(uint importIndex, uint slotIndex) + { + var entryPoint = new MethodEntryPoint(1234); + entryPoint.Fixups.Add(new MethodFixup(importIndex, slotIndex)); + + entryPoint.UpdateOffsets(new RelocationParameters(0, 0)); + var reader = new BinaryStreamReader(entryPoint.WriteIntoArray()); + + var newEntryPoint = MethodEntryPoint.FromReader(ref reader); + Assert.Equal(1234u, newEntryPoint.RuntimeFunctionIndex); + Assert.Equal(entryPoint.Fixups, newEntryPoint.Fixups); + } + + [Fact] + public void MultipleFixups() + { + var entryPoint = new MethodEntryPoint(1234); + entryPoint.Fixups.Add(new MethodFixup(1337, 1)); + entryPoint.Fixups.Add(new MethodFixup(1337, 2)); + entryPoint.Fixups.Add(new MethodFixup(1337, 3)); + entryPoint.Fixups.Add(new MethodFixup(1339, 11)); + entryPoint.Fixups.Add(new MethodFixup(1339, 12)); + entryPoint.Fixups.Add(new MethodFixup(1339, 13)); + + entryPoint.UpdateOffsets(new RelocationParameters(0, 0)); + var reader = new BinaryStreamReader(entryPoint.WriteIntoArray()); + + var newEntryPoint = MethodEntryPoint.FromReader(ref reader); + Assert.Equal(1234u, newEntryPoint.RuntimeFunctionIndex); + Assert.Equal(entryPoint.Fixups, newEntryPoint.Fixups); + } + } +} diff --git a/test/AsmResolver.PE.Tests/DotNet/ReadyToRun/NativeArrayTest.cs b/test/AsmResolver.PE.Tests/DotNet/ReadyToRun/NativeArrayTest.cs new file mode 100644 index 000000000..e467b1fee --- /dev/null +++ b/test/AsmResolver.PE.Tests/DotNet/ReadyToRun/NativeArrayTest.cs @@ -0,0 +1,48 @@ +using System.Diagnostics; +using System.Linq; +using AsmResolver.IO; +using AsmResolver.PE.DotNet.ReadyToRun; +using Xunit; + +namespace AsmResolver.PE.Tests.DotNet.ReadyToRun; + +public class NativeArrayTest +{ + [DebuggerDisplay("{Value}")] + private readonly struct IntValue : IWritable + { + public readonly int Value; + + public IntValue(int value) => Value = value; + public uint GetPhysicalSize() => sizeof(uint); + public void Write(IBinaryStreamWriter writer) => writer.WriteInt32(Value); + public static implicit operator IntValue(int value) => new(value); + public static implicit operator int(IntValue value) => value.Value; + } + + [Theory] + [InlineData(new[] {1337})] + [InlineData(new[] {1337, 1338})] + [InlineData(new[] {1337, 1338, 1339})] + [InlineData(new[] {1337, 1338, 1339, 1340})] + [InlineData(new[] {1337, 1338, 1339, 1340, 1341})] + [InlineData(new[] { + 1337, 1338, 1339, 1340, + 1341, 1342, 1343, 1344, + 1345, 1346, 1347, 1348, + 1349, 1350, 1351, 1352, + 1353, 1354 // count > 16 -> extra root. + })] + public void AddElements(int[] elements) + { + var array = new NativeArray(); + for (int i = 0; i < elements.Length; i++) + array.Add(elements[i]); + + array.UpdateOffsets(new RelocationParameters(0, 0)); + byte[] raw = array.WriteIntoArray(); + var view = NativeArray.FromReader(new BinaryStreamReader(raw), reader => reader.ReadInt32()); + + Assert.All(Enumerable.Range(0, elements.Length), i => Assert.Equal(elements[i], view[i].Value)); + } +} diff --git a/test/AsmResolver.PE.Tests/DotNet/ReadyToRun/ReadyToRunDirectoryTest.cs b/test/AsmResolver.PE.Tests/DotNet/ReadyToRun/ReadyToRunDirectoryTest.cs new file mode 100644 index 000000000..4f0bc2327 --- /dev/null +++ b/test/AsmResolver.PE.Tests/DotNet/ReadyToRun/ReadyToRunDirectoryTest.cs @@ -0,0 +1,237 @@ +using System.Collections.Generic; +using System.Linq; +using AsmResolver.IO; +using AsmResolver.PE.DotNet.ReadyToRun; +using Xunit; + +namespace AsmResolver.PE.Tests.DotNet.ReadyToRun +{ + public class ReadyToRunDirectoryTest + { + private static T GetSection(IPEImage image, bool rebuild) + where T : class, IReadyToRunSection + { + var serializedImage = (SerializedPEImage) image; + + var directory = Assert.IsAssignableFrom(image.DotNetDirectory!.ManagedNativeHeader); + var section = directory.GetSection(); + + if (rebuild) + { + section.UpdateOffsets(new RelocationParameters(0, 0)); + + var reader = new BinaryStreamReader(section.WriteIntoArray()); + var context = serializedImage.ReaderContext; + section = (T) context.Parameters.ReadyToRunSectionReader.ReadSection(context, section.Type, ref reader); + } + + return section; + } + + [Fact] + public void ReadBasicHeader() + { + var image = PEImage.FromBytes(Properties.Resources.HelloWorld_ReadyToRun); + var header = Assert.IsAssignableFrom(image.DotNetDirectory!.ManagedNativeHeader); + + Assert.Equal(5, header.MajorVersion); + Assert.Equal(4, header.MinorVersion); + Assert.Equal(ReadyToRunAttributes.NonSharedPInvokeStubs, header.Attributes); + } + + [Fact] + public void ReadSections() + { + var image = PEImage.FromBytes(Properties.Resources.HelloWorld_ReadyToRun); + var header = Assert.IsAssignableFrom(image.DotNetDirectory!.ManagedNativeHeader); + + Assert.Equal(new[] + { + ReadyToRunSectionType.CompilerIdentifier, + ReadyToRunSectionType.ImportSections, + ReadyToRunSectionType.RuntimeFunctions, + ReadyToRunSectionType.MethodDefEntryPoints, + ReadyToRunSectionType.DebugInfo, + ReadyToRunSectionType.DelayLoadMethodCallThunks, + ReadyToRunSectionType.AvailableTypes, + ReadyToRunSectionType.InstanceMethodEntryPoints, + ReadyToRunSectionType.ManifestMetadata, + ReadyToRunSectionType.InliningInfo2, + ReadyToRunSectionType.ManifestAssemblyMvids, + }, header.Sections.Select(x => x.Type)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void CompilerIdentifierSection(bool rebuild) + { + var image = PEImage.FromBytes(Properties.Resources.HelloWorld_ReadyToRun); + var section = GetSection(image, rebuild); + + Assert.Equal("Crossgen2 6.0.2223.42425", section.Identifier); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ImportSections(bool rebuild) + { + var image = PEImage.FromBytes(Properties.Resources.HelloWorld_ReadyToRun); + var section = GetSection(image, rebuild); + + Assert.Equal(new[] + { + ImportSectionType.StubDispatch, + ImportSectionType.Unknown, + ImportSectionType.Unknown, + ImportSectionType.StubDispatch, + ImportSectionType.Unknown, + ImportSectionType.StringHandle + }, section.Sections.Select(x => x.Type)); + } + + [Theory] + [InlineData(false)] + // TODO: [InlineData(true)] + public void ImportSectionSlots(bool rebuild) + { + var image = PEImage.FromBytes(Properties.Resources.HelloWorld_ReadyToRun); + var section = GetSection(image, rebuild); + + Assert.Equal(new[] + { + (0x00000000u, 0x0000A0D4u, ReadyToRunFixupKind.CheckInstructionSetSupport), + (0x00000000u, 0x0000A0DBu, ReadyToRunFixupKind.Helper), + (0x00000000u, 0x0000A0DDu, ReadyToRunFixupKind.Helper), + (0x00000000u, 0x0000A0DFu, ReadyToRunFixupKind.Helper), + (0x00000000u, 0x0000A0E2u, ReadyToRunFixupKind.Helper), + }, ReadTestData(section.Sections[1])); + + Assert.Equal(new[] + { + (0x0009594u, 0x0000A0D9u, ReadyToRunFixupKind.MethodEntryRefToken), + }, ReadTestData(section.Sections[3])); + + Assert.Equal(new[] + { + (0x00000000u, 0x0000A0E5u, ReadyToRunFixupKind.StringHandle), + }, ReadTestData(section.Sections[5])); + + return; + + IEnumerable<(uint, uint, ReadyToRunFixupKind)> ReadTestData(ImportSection import) + { + for (int i = 0; i < import.Slots.Count; i++) + { + yield return ( + import.Slots[i].Rva, + import.Signatures[i].Rva, + (ReadyToRunFixupKind) import.Signatures[i].CreateReader().ReadByte() + ); + } + } + } + + [Fact] + public void X64RuntimeFunctions() + { + var image = PEImage.FromBytes(Properties.Resources.ReadyToRunTest); + var header = Assert.IsAssignableFrom(image.DotNetDirectory!.ManagedNativeHeader); + var section = header.GetSection(); + + Assert.Equal(new[] + { + (0x00001840u, 0x00001870u), + (0x00001870u, 0x0000188au), + (0x00001890u, 0x000018cdu), + (0x000018CDu, 0x000018f2u), + (0x00001900u, 0x0000191au), + }, section.GetFunctions().Select(x => (x.Begin.Rva, x.End.Rva))); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void MethodDefEntryPoints(bool rebuild) + { + var image = PEImage.FromBytes(Properties.Resources.ReadyToRunTest); + var section = GetSection(image, rebuild); + + Assert.Equal( + new uint?[] {0u, null, 1u, 2u, 4u}, + section.EntryPoints.Select(x => x?.RuntimeFunctionIndex) + ); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void MethodDefEntryPointFixups(bool rebuild) + { + var image = PEImage.FromBytes(Properties.Resources.ReadyToRunTest); + var section = GetSection(image, rebuild); + + Assert.Equal(new[] + { + (5u, 0u), + (5u, 4u), + }, section.EntryPoints[0]!.Fixups.Select(x => (x.ImportIndex, x.SlotIndex))); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void DebugInfoBound(bool rebuild) + { + var image = PEImage.FromBytes(Properties.Resources.ReadyToRunTest); + var section = GetSection(image, rebuild); + + Assert.NotNull(section.Entries[0]); + Assert.Equal(new DebugInfoBounds[] + { + new(0x0, DebugInfoBounds.PrologOffset, DebugInfoAttributes.StackEmpty), + new(0x4, 0x0, DebugInfoAttributes.StackEmpty), + new(0x14, 0xA, DebugInfoAttributes.StackEmpty), + new(0x1A, 0xF, DebugInfoAttributes.StackEmpty), + new(0x2A, 0x14, DebugInfoAttributes.StackEmpty), + new(0x2A, DebugInfoBounds.EpilogOffset, DebugInfoAttributes.StackEmpty), + }, section.Entries[0]!.Bounds); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void DebugInfoBoundLookBack(bool rebuild) + { + var image = PEImage.FromBytes(Properties.Resources.ReadyToRunTest); + var section = GetSection(image, rebuild); + + Assert.NotNull(section.Entries[4]); + Assert.Equal(new DebugInfoBounds[] + { + new(0x0, DebugInfoBounds.PrologOffset, DebugInfoAttributes.StackEmpty), + new(0x4, 0x0, DebugInfoAttributes.StackEmpty), + new(0x14, 0xA, DebugInfoAttributes.StackEmpty), + new(0x14, DebugInfoBounds.EpilogOffset, DebugInfoAttributes.StackEmpty), + }, section.Entries[4]!.Bounds); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void DebugInfoVariables(bool rebuild) + { + var image = PEImage.FromBytes(Properties.Resources.ReadyToRunTestLoop); + var section = GetSection(image, rebuild); + + Assert.NotNull(section.Entries[0]); + Assert.Equal(new DebugInfoVariable[] + { + new(0, 6, 0, new(DebugInfoVariableLocationType.Register, 1)), + new(0x17, 0x2B, 1, new(DebugInfoVariableLocationType.Register, 6)), + new(0x19, 0x2B, 2, new(DebugInfoVariableLocationType.Register, 7)), + }, section.Entries[0]!.Variables); + } + } +} diff --git a/test/AsmResolver.PE.Tests/Exports/ExportDirectoryTest.cs b/test/AsmResolver.PE.Tests/Exports/ExportDirectoryTest.cs index 3622d8b5f..2c95c75ab 100644 --- a/test/AsmResolver.PE.Tests/Exports/ExportDirectoryTest.cs +++ b/test/AsmResolver.PE.Tests/Exports/ExportDirectoryTest.cs @@ -282,7 +282,7 @@ public void RebuildForwarderSymbol() stream.ToArray()); string output = runner.RunAndCaptureOutput(exePath); - Assert.Equal("ActualDLL::Bar\r\nProxyDll::Baz\r\nHello World!\r\n", output); + Assert.Equal("ActualDLL::Bar\nProxyDll::Baz\nHello World!\n", output); } } } diff --git a/test/AsmResolver.PE.Tests/Properties/Resources.Designer.cs b/test/AsmResolver.PE.Tests/Properties/Resources.Designer.cs index 1ab8eda78..b006b2bea 100644 --- a/test/AsmResolver.PE.Tests/Properties/Resources.Designer.cs +++ b/test/AsmResolver.PE.Tests/Properties/Resources.Designer.cs @@ -164,6 +164,13 @@ public static byte[] HelloWorld_Signed { } } + public static byte[] HelloWorld_ReadyToRun { + get { + object obj = ResourceManager.GetObject("HelloWorld_ReadyToRun", resourceCulture); + return ((byte[])(obj)); + } + } + public static byte[] HelloWorldPortablePdb { get { object obj = ResourceManager.GetObject("HelloWorldPortablePdb", resourceCulture); @@ -275,5 +282,19 @@ public static byte[] FieldRvaTest { return ((byte[])(obj)); } } + + public static byte[] ReadyToRunTest { + get { + object obj = ResourceManager.GetObject("ReadyToRunTest", resourceCulture); + return ((byte[])(obj)); + } + } + + public static byte[] ReadyToRunTestLoop { + get { + object obj = ResourceManager.GetObject("ReadyToRunTestLoop", resourceCulture); + return ((byte[])(obj)); + } + } } } diff --git a/test/AsmResolver.PE.Tests/Properties/Resources.resx b/test/AsmResolver.PE.Tests/Properties/Resources.resx index 987d82cad..00511e896 100644 --- a/test/AsmResolver.PE.Tests/Properties/Resources.resx +++ b/test/AsmResolver.PE.Tests/Properties/Resources.resx @@ -69,6 +69,9 @@ ..\Resources\HelloWorld.signed.exe;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + ..\Resources\HelloWorld.ReadyToRun.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + ..\Resources\HelloWorld.pdb;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 @@ -117,4 +120,10 @@ ..\Resources\FieldRvaTest.exe;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + ..\Resources\ReadyToRunTest.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\Resources\ReadyToRunTestLoop.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + diff --git a/test/AsmResolver.PE.Tests/Resources/HelloWorld.ReadyToRun.dll b/test/AsmResolver.PE.Tests/Resources/HelloWorld.ReadyToRun.dll new file mode 100644 index 000000000..f78cf23e3 Binary files /dev/null and b/test/AsmResolver.PE.Tests/Resources/HelloWorld.ReadyToRun.dll differ diff --git a/test/AsmResolver.PE.Tests/Resources/ReadyToRunTest.dll b/test/AsmResolver.PE.Tests/Resources/ReadyToRunTest.dll new file mode 100644 index 000000000..6e13cd031 Binary files /dev/null and b/test/AsmResolver.PE.Tests/Resources/ReadyToRunTest.dll differ diff --git a/test/AsmResolver.PE.Tests/Resources/ReadyToRunTestLoop.dll b/test/AsmResolver.PE.Tests/Resources/ReadyToRunTestLoop.dll new file mode 100644 index 000000000..e5e6efaab Binary files /dev/null and b/test/AsmResolver.PE.Tests/Resources/ReadyToRunTestLoop.dll differ diff --git a/test/AsmResolver.PE.Win32Resources.Tests/AsmResolver.PE.Win32Resources.Tests.csproj b/test/AsmResolver.PE.Win32Resources.Tests/AsmResolver.PE.Win32Resources.Tests.csproj index 76a8733e6..1c80a65b2 100644 --- a/test/AsmResolver.PE.Win32Resources.Tests/AsmResolver.PE.Win32Resources.Tests.csproj +++ b/test/AsmResolver.PE.Win32Resources.Tests/AsmResolver.PE.Win32Resources.Tests.csproj @@ -8,10 +8,10 @@ - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/AsmResolver.Symbols.Pdb.Tests/AsmResolver.Symbols.Pdb.Tests.csproj b/test/AsmResolver.Symbols.Pdb.Tests/AsmResolver.Symbols.Pdb.Tests.csproj index 2d4002a19..364078a8a 100644 --- a/test/AsmResolver.Symbols.Pdb.Tests/AsmResolver.Symbols.Pdb.Tests.csproj +++ b/test/AsmResolver.Symbols.Pdb.Tests/AsmResolver.Symbols.Pdb.Tests.csproj @@ -8,10 +8,10 @@ - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/test/AsmResolver.Tests/AsmResolver.Tests.csproj b/test/AsmResolver.Tests/AsmResolver.Tests.csproj index 5d726da8c..5a5259bc5 100644 --- a/test/AsmResolver.Tests/AsmResolver.Tests.csproj +++ b/test/AsmResolver.Tests/AsmResolver.Tests.csproj @@ -8,17 +8,16 @@ - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/test/AsmResolver.Tests/Runners/PERunner.cs b/test/AsmResolver.Tests/Runners/PERunner.cs index ee8bc60fa..4f6761cb4 100644 --- a/test/AsmResolver.Tests/Runners/PERunner.cs +++ b/test/AsmResolver.Tests/Runners/PERunner.cs @@ -31,7 +31,7 @@ public void RebuildAndRun(PEFile peFile, string fileName, string expectedOutput, { string fullPath = Rebuild(peFile, fileName, testClass, testMethod); string actualOutput = RunAndCaptureOutput(fullPath, null, timeout); - Assert.Equal(expectedOutput, actualOutput); + Assert.Equal(expectedOutput.Replace("\r\n", "\n"), actualOutput); } public string GetTestDirectory(string testClass, string testName) @@ -104,7 +104,7 @@ public string RunAndCaptureOutput(string filePath, string[]? arguments = null, i throw new RunnerException(process.ExitCode, errorString); } - return process.StandardOutput.ReadToEnd(); + return process.StandardOutput.ReadToEnd().Replace("\r\n", "\n"); } protected abstract ProcessStartInfo GetStartInfo(string filePath, string[]? arguments); diff --git a/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.CustomAttributes/CustomAttributesTestClass.cs b/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.CustomAttributes/CustomAttributesTestClass.cs index f4444fa45..c1c3a2935 100644 --- a/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.CustomAttributes/CustomAttributesTestClass.cs +++ b/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.CustomAttributes/CustomAttributesTestClass.cs @@ -54,6 +54,11 @@ public void FixedComplexTypeArgument() { } + [TestCase(typeof(TestEnum))] + public void FIxedLocalTypeArgument() + { + } + [TestCase(2, "Fixed arg", TestEnum.Value3)] public void FixedMultipleArguments() { diff --git a/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.Methods/Constructors.cs b/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.Methods/Constructors.cs new file mode 100644 index 000000000..2d4651f00 --- /dev/null +++ b/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.Methods/Constructors.cs @@ -0,0 +1,42 @@ +using System; + +namespace AsmResolver.DotNet.TestCases.Methods; + +public class Constructors +{ + static Constructors() + { + } + + public Constructors() + { + } + + public Constructors(int a, int b) + { + } + + public Constructors(int a, string b) + { + } + + public Constructors(int a, string b, double c) + { + } + + public void NonConstructorMethod() + { + } + + public void NonConstructorMethod(int a, int b) + { + } + + public void NonConstructorMethod(int a, string b) + { + } + + public void NonConstructorMethod(int a, string b, double c) + { + } +} diff --git a/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.Methods/NoStaticConstructor.cs b/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.Methods/NoStaticConstructor.cs new file mode 100644 index 000000000..64985fff3 --- /dev/null +++ b/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.Methods/NoStaticConstructor.cs @@ -0,0 +1,5 @@ +namespace AsmResolver.DotNet.TestCases.Methods; + +public class NoStaticConstructor +{ +} diff --git a/test/TestBinaries/DotNet/HelloWorld/HelloWorld.csproj b/test/TestBinaries/DotNet/HelloWorld/HelloWorld.csproj index 53d2dbb55..b2a147cef 100644 --- a/test/TestBinaries/DotNet/HelloWorld/HelloWorld.csproj +++ b/test/TestBinaries/DotNet/HelloWorld/HelloWorld.csproj @@ -2,8 +2,9 @@ Exe - net47;netcoreapp3.1 + net47;netcoreapp3.1;net6.0 Resources\Icon.ico + true