From 6302ae16e9db2b5da7ea5bd2a0209cb33a820e53 Mon Sep 17 00:00:00 2001 From: sunnamed434 Date: Thu, 22 Dec 2022 15:10:32 +0000 Subject: [PATCH 01/35] Fix docs order of instructions (#390) * Update managed-method-bodies.rst * Revert "Update managed-method-bodies.rst" This reverts commit c5943478dc86ac00fff42edc35391aacc7d1b059. * Revert "Revert "Update managed-method-bodies.rst"" This reverts commit eed2104ff6ec8ba15e4d797c03014e889d442549. * Revert "Revert "Revert "Update managed-method-bodies.rst""" This reverts commit 0315363069af7b7f47fbf2290bb47a4c3a7319b7. * FIX: fix Cil instructions order in docs Before it could cause an error while writing an method body * Now its better cuz keeps the docs style --- docs/dotnet/managed-method-bodies.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dotnet/managed-method-bodies.rst b/docs/dotnet/managed-method-bodies.rst index 3d438d3de..c3d7ce70d 100644 --- a/docs/dotnet/managed-method-bodies.rst +++ b/docs/dotnet/managed-method-bodies.rst @@ -196,6 +196,7 @@ Below an example on how to use the ``ReferenceImporter`` to emit a call to ``Con var importer = new ReferenceImporter(targetModule); var writeLine = importer.ImportMethod(typeof(Console).GetMethod("WriteLine", new[] { typeof(string) } ); + body.Instructions.Add(new CilInstruction(CilOpCodes.Ldstr, "Hello, world!")); body.Instructions.Add(new CilInstruction(CilOpCodes.Call, writeLine)); From 37fabb921f7e3127fa93a49c8967bedf6fa02112 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Dec 2022 16:17:39 +0100 Subject: [PATCH 02/35] Bump Microsoft.NET.Test.Sdk from 17.4.0 to 17.4.1 (#388) Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 17.4.0 to 17.4.1. - [Release notes](https://github.com/microsoft/vstest/releases) - [Changelog](https://github.com/microsoft/vstest/blob/main/docs/releases.md) - [Commits](https://github.com/microsoft/vstest/compare/v17.4.0...v17.4.1) --- updated-dependencies: - dependency-name: Microsoft.NET.Test.Sdk dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../AsmResolver.DotNet.Dynamic.Tests.csproj | 2 +- test/AsmResolver.DotNet.Tests/AsmResolver.DotNet.Tests.csproj | 2 +- test/AsmResolver.PE.File.Tests/AsmResolver.PE.File.Tests.csproj | 2 +- test/AsmResolver.PE.Tests/AsmResolver.PE.Tests.csproj | 2 +- .../AsmResolver.PE.Win32Resources.Tests.csproj | 2 +- .../AsmResolver.Symbols.Pdb.Tests.csproj | 2 +- test/AsmResolver.Tests/AsmResolver.Tests.csproj | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) 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 28574f7f7..c62d0b8c7 100644 --- a/test/AsmResolver.DotNet.Dynamic.Tests/AsmResolver.DotNet.Dynamic.Tests.csproj +++ b/test/AsmResolver.DotNet.Dynamic.Tests/AsmResolver.DotNet.Dynamic.Tests.csproj @@ -8,7 +8,7 @@ - + diff --git a/test/AsmResolver.DotNet.Tests/AsmResolver.DotNet.Tests.csproj b/test/AsmResolver.DotNet.Tests/AsmResolver.DotNet.Tests.csproj index 393b05e33..a3983706c 100644 --- a/test/AsmResolver.DotNet.Tests/AsmResolver.DotNet.Tests.csproj +++ b/test/AsmResolver.DotNet.Tests/AsmResolver.DotNet.Tests.csproj @@ -9,7 +9,7 @@ - + all 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 44c298a73..80baee93b 100644 --- a/test/AsmResolver.PE.File.Tests/AsmResolver.PE.File.Tests.csproj +++ b/test/AsmResolver.PE.File.Tests/AsmResolver.PE.File.Tests.csproj @@ -9,7 +9,7 @@ - + all diff --git a/test/AsmResolver.PE.Tests/AsmResolver.PE.Tests.csproj b/test/AsmResolver.PE.Tests/AsmResolver.PE.Tests.csproj index 2c4967fcf..2a656347a 100644 --- a/test/AsmResolver.PE.Tests/AsmResolver.PE.Tests.csproj +++ b/test/AsmResolver.PE.Tests/AsmResolver.PE.Tests.csproj @@ -16,7 +16,7 @@ - + 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 66003d51f..3cdf923ca 100644 --- a/test/AsmResolver.PE.Win32Resources.Tests/AsmResolver.PE.Win32Resources.Tests.csproj +++ b/test/AsmResolver.PE.Win32Resources.Tests/AsmResolver.PE.Win32Resources.Tests.csproj @@ -8,7 +8,7 @@ - + all 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 495bbf831..942be7824 100644 --- a/test/AsmResolver.Symbols.Pdb.Tests/AsmResolver.Symbols.Pdb.Tests.csproj +++ b/test/AsmResolver.Symbols.Pdb.Tests/AsmResolver.Symbols.Pdb.Tests.csproj @@ -8,7 +8,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/AsmResolver.Tests/AsmResolver.Tests.csproj b/test/AsmResolver.Tests/AsmResolver.Tests.csproj index 2bac4b411..b3874df72 100644 --- a/test/AsmResolver.Tests/AsmResolver.Tests.csproj +++ b/test/AsmResolver.Tests/AsmResolver.Tests.csproj @@ -8,7 +8,7 @@ - + all From aa1f7132f851314e7ac63211b1a3ac8bd5771b20 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Dec 2022 14:04:58 +0000 Subject: [PATCH 03/35] Bump BenchmarkDotNet from 0.13.2 to 0.13.3 Bumps [BenchmarkDotNet](https://github.com/dotnet/BenchmarkDotNet) from 0.13.2 to 0.13.3. - [Release notes](https://github.com/dotnet/BenchmarkDotNet/releases) - [Commits](https://github.com/dotnet/BenchmarkDotNet/compare/v0.13.2...v0.13.3) --- updated-dependencies: - dependency-name: BenchmarkDotNet dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- test/AsmResolver.Benchmarks/AsmResolver.Benchmarks.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/AsmResolver.Benchmarks/AsmResolver.Benchmarks.csproj b/test/AsmResolver.Benchmarks/AsmResolver.Benchmarks.csproj index 9d08fac34..f05cc3ab8 100644 --- a/test/AsmResolver.Benchmarks/AsmResolver.Benchmarks.csproj +++ b/test/AsmResolver.Benchmarks/AsmResolver.Benchmarks.csproj @@ -7,7 +7,7 @@ - + From 7dd2df5cb5d7fa0083ab5c303af5bf2bc8123464 Mon Sep 17 00:00:00 2001 From: sunnamed434 Date: Tue, 27 Dec 2022 12:42:14 +0000 Subject: [PATCH 04/35] =?UTF-8?q?Add=20new=20property=20that=20whether=20r?= =?UTF-8?q?eturns=20a=20value=20that=20indicates=20if=20the=20T=E2=80=A6?= =?UTF-8?q?=20(#391)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add new property that whether returns a value that indicates if the TypeDefinition is type * Fix summaries grammar and structure * Fix grammar * Fix grammar --- src/AsmResolver.DotNet/TypeDefinition.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/AsmResolver.DotNet/TypeDefinition.cs b/src/AsmResolver.DotNet/TypeDefinition.cs index ffe56e169..ef51d751e 100644 --- a/src/AsmResolver.DotNet/TypeDefinition.cs +++ b/src/AsmResolver.DotNet/TypeDefinition.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; -using System.Runtime.CompilerServices; using System.Threading; using AsmResolver.Collections; using AsmResolver.DotNet.Code.Cil; @@ -492,6 +491,21 @@ public bool IsDelegate } } + /// + /// true if this is the global (aka. <Module>) type, otherwise false. + /// + /// + /// If the global (aka. <Module>) type was not added or does not exist yet in the will return false. + /// + public bool IsModuleType + { + get + { + var module = Module?.GetModuleType(); + return module != null && module == this; + } + } + /// /// Determines whether the type is marked as read-only. /// From 956caa3178567982f2b1bbecbc83ce8d4744e559 Mon Sep 17 00:00:00 2001 From: Washi Date: Tue, 27 Dec 2022 17:41:09 +0100 Subject: [PATCH 05/35] Add failing module-type test cases. --- .../ModuleDefinitionTest.cs | 58 ++++++++++++++++++ .../Properties/Resources.Designer.cs | 28 +++++++++ .../Properties/Resources.resx | 12 ++++ .../Resources/ModuleCctorAbsentNet6.dll | Bin 0 -> 2048 bytes .../Resources/ModuleCctorLookalikeNet6.dll | Bin 0 -> 2048 bytes .../Resources/ModuleCctorNet6.dll | Bin 0 -> 2048 bytes .../Resources/ModuleCctorNetFramework.exe | Bin 0 -> 2048 bytes 7 files changed, 98 insertions(+) create mode 100644 test/AsmResolver.DotNet.Tests/Resources/ModuleCctorAbsentNet6.dll create mode 100644 test/AsmResolver.DotNet.Tests/Resources/ModuleCctorLookalikeNet6.dll create mode 100644 test/AsmResolver.DotNet.Tests/Resources/ModuleCctorNet6.dll create mode 100644 test/AsmResolver.DotNet.Tests/Resources/ModuleCctorNetFramework.exe diff --git a/test/AsmResolver.DotNet.Tests/ModuleDefinitionTest.cs b/test/AsmResolver.DotNet.Tests/ModuleDefinitionTest.cs index 7e2e1ba10..e527ce48c 100644 --- a/test/AsmResolver.DotNet.Tests/ModuleDefinitionTest.cs +++ b/test/AsmResolver.DotNet.Tests/ModuleDefinitionTest.cs @@ -412,5 +412,63 @@ public void RewriteSystemPrivateXml() using var stream = new MemoryStream(); module.Write(stream); } + + [Fact] + public void GetModuleTypeNetFramework() + { + var module = ModuleDefinition.FromBytes(Properties.Resources.ModuleCctorNetFramework); + + // Module type should exist. + var type = module.GetModuleType(); + Assert.NotNull(type); + Assert.Equal("CustomModuleType", type.Name); + + // Creating module type should give us the existing type. + Assert.Same(type, module.GetOrCreateModuleType()); + } + + [Fact] + public void GetModuleTypeNet6() + { + var module = ModuleDefinition.FromBytes(Properties.Resources.ModuleCctorNet6); + + // Module type should exist. + var type = module.GetModuleType(); + Assert.NotNull(type); + Assert.Equal("", type.Name); + + // Creating module type should give us the existing type. + Assert.Same(type, module.GetOrCreateModuleType()); + } + + [Fact] + public void GetModuleTypeAbsentNet6() + { + var module = ModuleDefinition.FromBytes(Properties.Resources.ModuleCctorAbsentNet6); + + // Module type should not exist. + var type = module.GetModuleType(); + Assert.Null(type); + + // Creating should add it to the module. + type = module.GetOrCreateModuleType(); + Assert.NotNull(type); + Assert.Same(type, module.GetModuleType()); + } + + [Fact] + public void GetModuleTypeLookalikeNet6() + { + var module = ModuleDefinition.FromBytes(Properties.Resources.ModuleCctorLookalikeNet6); + + // Module type should not exist. + var type = module.GetModuleType(); + Assert.Null(type); + + // Creating should add it to the module. + type = module.GetOrCreateModuleType(); + Assert.NotNull(type); + Assert.Same(type, module.GetModuleType()); + } } } diff --git a/test/AsmResolver.DotNet.Tests/Properties/Resources.Designer.cs b/test/AsmResolver.DotNet.Tests/Properties/Resources.Designer.cs index fffb016d9..427f830ea 100644 --- a/test/AsmResolver.DotNet.Tests/Properties/Resources.Designer.cs +++ b/test/AsmResolver.DotNet.Tests/Properties/Resources.Designer.cs @@ -331,5 +331,33 @@ public static byte[] MyLibrary_X64 { return ((byte[])(obj)); } } + + public static byte[] ModuleCctorAbsentNet6 { + get { + object obj = ResourceManager.GetObject("ModuleCctorAbsentNet6", resourceCulture); + return ((byte[])(obj)); + } + } + + public static byte[] ModuleCctorLookalikeNet6 { + get { + object obj = ResourceManager.GetObject("ModuleCctorLookalikeNet6", resourceCulture); + return ((byte[])(obj)); + } + } + + public static byte[] ModuleCctorNet6 { + get { + object obj = ResourceManager.GetObject("ModuleCctorNet6", resourceCulture); + return ((byte[])(obj)); + } + } + + public static byte[] ModuleCctorNetFramework { + get { + object obj = ResourceManager.GetObject("ModuleCctorNetFramework", resourceCulture); + return ((byte[])(obj)); + } + } } } diff --git a/test/AsmResolver.DotNet.Tests/Properties/Resources.resx b/test/AsmResolver.DotNet.Tests/Properties/Resources.resx index 37af630d8..f13ac4af0 100644 --- a/test/AsmResolver.DotNet.Tests/Properties/Resources.resx +++ b/test/AsmResolver.DotNet.Tests/Properties/Resources.resx @@ -141,4 +141,16 @@ ..\Resources\MyLibrary.x64.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + ..\Resources\ModuleCctorAbsentNet6.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\Resources\ModuleCctorLookalikeNet6.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\Resources\ModuleCctorNet6.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\Resources\ModuleCctorNetFramework.exe;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + diff --git a/test/AsmResolver.DotNet.Tests/Resources/ModuleCctorAbsentNet6.dll b/test/AsmResolver.DotNet.Tests/Resources/ModuleCctorAbsentNet6.dll new file mode 100644 index 0000000000000000000000000000000000000000..7dbed50381bb5c6e3b869202a79a2ea670f86ae9 GIT binary patch literal 2048 zcmeHG%}*0S6#s4cNU73DNQgud2dfc@*`VbjG1^eRB&`rzG(i&R#|SIiU9#OGcu+2C zOg#8U_y>3}N<0`Z9x?Giyb=BmFZDOmEi`}{y^;8K=e_rP@6F76Z)Vc7uh9gcnWI_- zHYpV)H2*!@pgVf@V-)Y&wt6>>)K+hH$tsH#&t3F#B~i#Zj_ZrO6kf#Mn!NWI202Up<3S5bpA?WfrEBM zO-MWgPU(MN2Cx=_opZospdTp``#Bux|CLby;Raurc%lKDzFhN(RgJn9bpS!$^)SI| zOi$WwA;=*S#I)8UYLaID|3*>OSCYytgcd9bV3m0o8sorDH2#51k26>L$Rhow=kDpl znaN>8uUe~Rb~?XvTMa6vWTFAe1q<* zjA)_H&?pt45T*rHbe&$%zPTF0eT_QMhp2KNAcwfIr7h^z5sfx>PTSOls8c6b-J?>u z;Ci-|M`pe3%aWONow94oI=oYHe5)jJ&$E1)vK)zIrR=+Dh6uP%EQzio4HrdJc!Cl)FvQP9fuB#i z$39$QlnaOc^*|GhxQ$WvSKt9-awu``BOSj?AI?k=+x%QyW=zdvbC+JGGPXVT{KeOc zsrR41Rn4I<-_`LM;kP&EpLT3VhMHj*J>d_J=k9h7?!IgdZnx@PkJ5$G4paRV0P{)L m8(ovS6S~n-nnl}2^#VlV@WkV95w-W&A3y%3{IB5eJMa^}D4Zhz literal 0 HcmV?d00001 diff --git a/test/AsmResolver.DotNet.Tests/Resources/ModuleCctorLookalikeNet6.dll b/test/AsmResolver.DotNet.Tests/Resources/ModuleCctorLookalikeNet6.dll new file mode 100644 index 0000000000000000000000000000000000000000..559b6cf322407c9c1d783d0a294d129009583b3a GIT binary patch literal 2048 zcmeHHOHUI~6#k}AELDhv#8||50ZovY2?!gJV8cU@v_fq06_P^RD~wKO$aJdcLfN=5 zap6+ex;8G1660SWaia?tgdgBu!$SP-ofaBEP2A#{Igjt0bMHOp%p-a88EOF3k~cSj z6>3FsE&nW*8E!oBp%HKESKC&M#A;i5!77SH&z<+?3L-b>IIb_UQg|gtSdJK(Oo@V< zmu5pl^kjMKsZk(dgz)svt6b%@b%czAMmK(jfJ0^Hk9Hb@949Lws2EpuE2?PB640-x z2Jy$hVSR7wG7M7ix*eD(+k2|`b`5*Rf9DiHq$(C69=%6%Q7d{VP(ng%HMq04}l)gP6SIBs%{>rpHx_ofOe7(>uOp z52YrC483d6%iTV++wAS`?K!7548x{eBX>OH^WbWQJ5s)9IrBxDct>Ae`KD87VSRk6 z=op*6G(z1)+^X=9?PjSJsTRgK(|QFqj9wfGdZJ&CS!r&mS-t;n?)0RWCFrPMA+~MNOpy=;437sJKMtO>@=C_RWwGp zc<|;AFrL(-F~KPD4|u|xUQCpOKfrkK;03FDW?%!TiC3zptLm$&?yjot$zFeqDgf0q zH#UG(l8Vxr|D7$9yZ+d_dc3Y(YgsiiYb}%Ww$BzkG3Vt=%*wm2kgUL&S9Y20vca(& zD~TdE>*|un!>z}MfQ%8t#I0vmblN&%#y+DRKV!iD(D*$?3Zt2(sTd{)0@9RxY#=OP(^l zzD>P9H`;ILU4vR~cbV;GXM1PIIkjON4)HHETOZJU&}xNSbJDZjIiFO#A=|XFP33Tq z@}pZt>+saYL6RxL%?kHBqCirSDq)yqZ3V=HR(2!UuQ$x>h}+v53{Sm3^GXK z2ysO!LU9Ajq^k4cq{lGMl)dTDRqdM&5ONwdpab>Fy^U@Vu54)>DIHO10~)nWJ&1@J zDW6U1$%r)xe|%(*;;?7MeobxAAeF+4N(p7WjW+ zyR);SFWGu#44AS~cwc$#)KB}4ly$%w#AXUOm^i8X%Vmj~+Q)|cN)$^UxuP4ucAQv+m@eIuPK*LW%?W-1 z6}X5Xk~t=pL|2f5LK#&)1<9%}^RnUais=v0kJF4&2*|G_mSDtnR$Cyx%a|g3u6=0Y z?~}u^31W|z!WqWY7>}p)FquztFXzAX4P{^9-RC<`E^g{ykkt&!VzbxN^@ac+wJmmV vqg8)%f%y>?jx9@5yJ@17cG>f=bqXTWxMbhn-2qYicD@bmruIGme+Pa7KAVp{ literal 0 HcmV?d00001 From d80bae72858f2a66e30b45f1838693febe7c9b4a Mon Sep 17 00:00:00 2001 From: Washi Date: Tue, 27 Dec 2022 18:00:35 +0100 Subject: [PATCH 06/35] BUGFIX: Check for module type name in GetModuleType. Simplify GetOrCreateModuleType. --- src/AsmResolver.DotNet/ModuleDefinition.cs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/AsmResolver.DotNet/ModuleDefinition.cs b/src/AsmResolver.DotNet/ModuleDefinition.cs index 13a5c9669..bd9f9067a 100644 --- a/src/AsmResolver.DotNet/ModuleDefinition.cs +++ b/src/AsmResolver.DotNet/ModuleDefinition.cs @@ -915,7 +915,19 @@ public IEnumerable GetAllTypes() /// Obtains the global scope type of the .NET module. /// /// The module type. - public TypeDefinition? GetModuleType() => TopLevelTypes.Count > 0 ? TopLevelTypes[0] : null; + public TypeDefinition? GetModuleType() + { + if (TopLevelTypes.Count == 0) + return null; + + var firstType = TopLevelTypes[0]; + + // Only .NET Framework allows the module type to be renamed to something else. + if (!OriginalTargetRuntime.IsNetFramework && !firstType.IsTypeOfUtf8(null, "")) + return null; + + return firstType; + } /// /// Obtains or creates the global scope type of the .NET module. @@ -923,13 +935,15 @@ public IEnumerable GetAllTypes() /// The module type. public TypeDefinition GetOrCreateModuleType() { - if (TopLevelTypes.Count == 0 || TopLevelTypes[0].Name != "") + var moduleType = GetModuleType(); + + if (moduleType is null) { - var moduleType = new TypeDefinition(null, "", 0); + moduleType = new TypeDefinition(null, "", 0); TopLevelTypes.Insert(0, moduleType); } - return TopLevelTypes[0]; + return moduleType; } /// From a0bb1dfe282def3e6ec394fdae8170ad8ad599e4 Mon Sep 17 00:00:00 2001 From: Washi Date: Tue, 27 Dec 2022 18:00:54 +0100 Subject: [PATCH 07/35] BUGFIX: Always find latest corlib in the list of assembly references. --- .../Serialized/SerializedModuleDefinition.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/AsmResolver.DotNet/Serialized/SerializedModuleDefinition.cs b/src/AsmResolver.DotNet/Serialized/SerializedModuleDefinition.cs index 9b7866ab4..c766f2427 100644 --- a/src/AsmResolver.DotNet/Serialized/SerializedModuleDefinition.cs +++ b/src/AsmResolver.DotNet/Serialized/SerializedModuleDefinition.cs @@ -336,12 +336,11 @@ private CorLibTypeFactory CreateCorLibTypeFactory() // TODO: perhaps check public key tokens. IResolutionScope? mostRecentCorLib = null; - var mostRecentVersion = new Version(); foreach (var reference in AssemblyReferences) { if (reference.Name is not null && KnownCorLibs.KnownCorLibNames.Contains(reference.Name)) { - if (mostRecentVersion < reference.Version) + if (mostRecentCorLib is null || reference.Version > mostRecentCorLib.GetAssembly()!.Version) mostRecentCorLib = reference; } } From 1c932c5f55e42c72888229caee1e828156a529a8 Mon Sep 17 00:00:00 2001 From: Washi Date: Tue, 27 Dec 2022 18:01:12 +0100 Subject: [PATCH 08/35] Quick grammar fix in IsModuleType docs. --- src/AsmResolver.DotNet/TypeDefinition.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/AsmResolver.DotNet/TypeDefinition.cs b/src/AsmResolver.DotNet/TypeDefinition.cs index ef51d751e..cb525ff61 100644 --- a/src/AsmResolver.DotNet/TypeDefinition.cs +++ b/src/AsmResolver.DotNet/TypeDefinition.cs @@ -492,10 +492,11 @@ public bool IsDelegate } /// - /// true if this is the global (aka. <Module>) type, otherwise false. + /// true if this is the global (i.e., <Module>) type, otherwise false. /// /// - /// If the global (aka. <Module>) type was not added or does not exist yet in the will return false. + /// If the global (i.e., <Module>) type was not added or does not exist yet in the , + /// this will return false. /// public bool IsModuleType { From 8863c84c5a1c49f13bc4bcf6c7de7103bc2ecfb6 Mon Sep 17 00:00:00 2001 From: svenskithesource <40274381+Svenskithesource@users.noreply.github.com> Date: Sun, 1 Jan 2023 15:51:00 +0100 Subject: [PATCH 09/35] Use preferred notation for bits --- src/AsmResolver/IO/BinaryStreamReader.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/AsmResolver/IO/BinaryStreamReader.cs b/src/AsmResolver/IO/BinaryStreamReader.cs index 9a924cda9..c5a604bf6 100644 --- a/src/AsmResolver/IO/BinaryStreamReader.cs +++ b/src/AsmResolver/IO/BinaryStreamReader.cs @@ -171,7 +171,7 @@ public byte ReadByte() } /// - /// Reads a single unsigned 16 bit integer from the input stream, and advances the current offset by two. + /// Reads a single unsigned 16-bit integer from the input stream, and advances the current offset by two. /// /// The consumed value. public ushort ReadUInt16() @@ -184,7 +184,7 @@ public ushort ReadUInt16() } /// - /// Reads a single unsigned 32 bit integer from the input stream, and advances the current offset by four. + /// Reads a single unsigned 32-bit integer from the input stream, and advances the current offset by four. /// /// The consumed value. public uint ReadUInt32() @@ -199,7 +199,7 @@ public uint ReadUInt32() } /// - /// Reads a single unsigned 64 bit integer from the input stream, and advances the current offset by eight. + /// Reads a single unsigned 64-bit integer from the input stream, and advances the current offset by eight. /// /// The consumed value. public ulong ReadUInt64() @@ -228,7 +228,7 @@ public sbyte ReadSByte() } /// - /// Reads a single signed 16 bit integer from the input stream, and advances the current offset by two. + /// Reads a single signed 16-bit integer from the input stream, and advances the current offset by two. /// /// The consumed value. public short ReadInt16() @@ -241,7 +241,7 @@ public short ReadInt16() } /// - /// Reads a single signed 32 bit integer from the input stream, and advances the current offset by four. + /// Reads a single signed 32-bit integer from the input stream, and advances the current offset by four. /// /// The consumed value. public int ReadInt32() @@ -256,7 +256,7 @@ public int ReadInt32() } /// - /// Reads a single signed 64 bit integer from the input stream, and advances the current offset by eight. + /// Reads a single signed 64-bit integer from the input stream, and advances the current offset by eight. /// /// The consumed value. public long ReadInt64() @@ -275,7 +275,7 @@ public long ReadInt64() } /// - /// Reads a single signed 32 bit single precision floating point number from the input stream, and advances the + /// Reads a single signed 32-bit single precision floating point number from the input stream, and advances the /// current offset by four. /// /// The consumed value. @@ -286,7 +286,7 @@ public unsafe float ReadSingle() } /// - /// Reads a single signed 64 bit double precision floating point number from the input stream, and advances the + /// Reads a single signed 64-bit double precision floating point number from the input stream, and advances the /// current offset by four. /// /// The consumed value. From c6a143e9cb5e55770167dce154ecc2596b4b71cf Mon Sep 17 00:00:00 2001 From: svenskithesource <40274381+Svenskithesource@users.noreply.github.com> Date: Sun, 1 Jan 2023 16:26:44 +0100 Subject: [PATCH 10/35] delimeter -> delimiter --- src/AsmResolver/IO/BinaryStreamReader.cs | 42 ++++++++++++------------ 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/AsmResolver/IO/BinaryStreamReader.cs b/src/AsmResolver/IO/BinaryStreamReader.cs index 9a924cda9..869896f83 100644 --- a/src/AsmResolver/IO/BinaryStreamReader.cs +++ b/src/AsmResolver/IO/BinaryStreamReader.cs @@ -350,54 +350,54 @@ public byte[] ReadToEnd() } /// - /// Reads bytes from the input stream until the provided delimeter byte is reached. + /// Reads bytes from the input stream until the provided delimiter byte is reached. /// - /// The delimeter byte to stop at. - /// The read bytes, including the delimeter if it was found. - public byte[] ReadBytesUntil(byte delimeter) => ReadBytesUntil(delimeter, true); + /// The delimiter byte to stop at. + /// The read bytes, including the delimiter if it was found. + public byte[] ReadBytesUntil(byte delimiter) => ReadBytesUntil(delimiter, true); /// - /// Reads bytes from the input stream until the provided delimeter byte is reached. + /// Reads bytes from the input stream until the provided delimiter byte is reached. /// - /// The delimeter byte to stop at. - /// - /// true if the final delimeter should be included in the return value, false otherwise. + /// The delimiter byte to stop at. + /// + /// true if the final delimiter should be included in the return value, false otherwise. /// /// The read bytes. /// - /// This function always consumes the delimeter from the input stream if it is present, regardless of the value - /// of . + /// This function always consumes the delimiter from the input stream if it is present, regardless of the value + /// of . /// - public byte[] ReadBytesUntil(byte delimeter, bool includeDelimeterInReturn) + public byte[] ReadBytesUntil(byte delimiter, bool includeDelimiterInReturn) { var lookahead = Fork(); - bool hasConsumedDelimeter = lookahead.AdvanceUntil(delimeter, includeDelimeterInReturn); + bool hasConsumedDelimiter = lookahead.AdvanceUntil(delimiter, includeDelimiterInReturn); byte[] buffer = new byte[lookahead.RelativeOffset - RelativeOffset]; ReadBytes(buffer, 0, buffer.Length); - if (hasConsumedDelimeter) + if (hasConsumedDelimiter) ReadByte(); return buffer; } /// - /// Advances the reader until the provided delimeter byte is reached. + /// Advances the reader until the provided delimiter byte is reached. /// - /// The delimeter byte to stop at. - /// - /// true if the final delimeter should be consumed if available, false otherwise. + /// The delimiter byte to stop at. + /// + /// true if the final delimiter should be consumed if available, false otherwise. /// - /// true if the delimeter byte was found and consumed, false otherwise. - public bool AdvanceUntil(byte delimeter, bool consumeDelimeter) + /// true if the delimiter byte was found and consumed, false otherwise. + public bool AdvanceUntil(byte delimiter, bool consumeDelimiter) { while (RelativeOffset < Length) { byte b = ReadByte(); - if (b == delimeter) + if (b == delimiter) { - if (!consumeDelimeter) + if (!consumeDelimiter) { RelativeOffset--; return true; From 6362a9c6fc8cb45d75b654e050e3fec31ba268b4 Mon Sep 17 00:00:00 2001 From: Washi Date: Tue, 3 Jan 2023 20:30:04 +0100 Subject: [PATCH 11/35] Extract UTF8 string for to static readonly field. --- src/AsmResolver.DotNet/ModuleDefinition.cs | 8 ++++---- src/AsmResolver.DotNet/TypeDefinition.cs | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/AsmResolver.DotNet/ModuleDefinition.cs b/src/AsmResolver.DotNet/ModuleDefinition.cs index bd9f9067a..ffb018860 100644 --- a/src/AsmResolver.DotNet/ModuleDefinition.cs +++ b/src/AsmResolver.DotNet/ModuleDefinition.cs @@ -304,7 +304,7 @@ public ModuleDefinition(Utf8String? name) AssemblyReferences.Add((AssemblyReference)CorLibTypeFactory.CorLibScope); MetadataResolver = new DefaultMetadataResolver(new DotNetFrameworkAssemblyResolver()); - TopLevelTypes.Add(new TypeDefinition(null, "", 0)); + TopLevelTypes.Add(new TypeDefinition(null, TypeDefinition.ModuleTypeName, 0)); } /// @@ -325,7 +325,7 @@ public ModuleDefinition(string? name, AssemblyReference corLib) OriginalTargetRuntime = DetectTargetRuntime(); MetadataResolver = new DefaultMetadataResolver(CreateAssemblyResolver(UncachedFileService.Instance)); - TopLevelTypes.Add(new TypeDefinition(null, "", 0)); + TopLevelTypes.Add(new TypeDefinition(null, TypeDefinition.ModuleTypeName, 0)); } /// @@ -923,7 +923,7 @@ public IEnumerable GetAllTypes() var firstType = TopLevelTypes[0]; // Only .NET Framework allows the module type to be renamed to something else. - if (!OriginalTargetRuntime.IsNetFramework && !firstType.IsTypeOfUtf8(null, "")) + if (!OriginalTargetRuntime.IsNetFramework && !firstType.IsTypeOfUtf8(null, TypeDefinition.ModuleTypeName)) return null; return firstType; @@ -939,7 +939,7 @@ public TypeDefinition GetOrCreateModuleType() if (moduleType is null) { - moduleType = new TypeDefinition(null, "", 0); + moduleType = new TypeDefinition(null, TypeDefinition.ModuleTypeName, 0); TopLevelTypes.Insert(0, moduleType); } diff --git a/src/AsmResolver.DotNet/TypeDefinition.cs b/src/AsmResolver.DotNet/TypeDefinition.cs index cb525ff61..21821ad1c 100644 --- a/src/AsmResolver.DotNet/TypeDefinition.cs +++ b/src/AsmResolver.DotNet/TypeDefinition.cs @@ -25,6 +25,8 @@ public class TypeDefinition : IOwnedCollectionElement, IOwnedCollectionElement { + internal static readonly Utf8String ModuleTypeName = ""; + private readonly LazyVariable _namespace; private readonly LazyVariable _name; private readonly LazyVariable _baseType; From 30016bbcdb61c12c1a8b094241a871b04ef6b0e4 Mon Sep 17 00:00:00 2001 From: Washi Date: Thu, 5 Jan 2023 20:46:24 +0100 Subject: [PATCH 12/35] Add generic custom attribute test cases. --- .../CustomAttributeTest.cs | 202 ++++++++++++++++-- ...r.DotNet.TestCases.CustomAttributes.csproj | 1 + .../CustomAttributesTestClass.cs | 59 +++++ .../TestCaseAttribute.Generic.cs | 24 +++ 4 files changed, 270 insertions(+), 16 deletions(-) create mode 100644 test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.CustomAttributes/TestCaseAttribute.Generic.cs diff --git a/test/AsmResolver.DotNet.Tests/CustomAttributeTest.cs b/test/AsmResolver.DotNet.Tests/CustomAttributeTest.cs index b05032774..6c57703a7 100644 --- a/test/AsmResolver.DotNet.Tests/CustomAttributeTest.cs +++ b/test/AsmResolver.DotNet.Tests/CustomAttributeTest.cs @@ -70,13 +70,22 @@ public void ReadParent() Assert.Equal(parentToken, attribute.Parent.MetadataToken); } - private static CustomAttribute GetCustomAttributeTestCase(string methodName, bool rebuild = false, bool access = false) + private static CustomAttribute GetCustomAttributeTestCase( + string methodName, + bool rebuild = false, + bool access = false, + bool generic = false) { var module = ModuleDefinition.FromFile(typeof(CustomAttributesTestClass).Assembly.Location); var type = module.TopLevelTypes.First(t => t.Name == nameof(CustomAttributesTestClass)); var method = type.Methods.First(m => m.Name == methodName); + + string attributeName = nameof(TestCaseAttribute); + if (generic) + attributeName += "`1"; + var attribute = method.CustomAttributes - .First(c => c.Constructor!.DeclaringType!.Name == nameof(TestCaseAttribute)); + .First(c => c.Constructor!.DeclaringType!.Name == attributeName); if (access) { @@ -286,8 +295,8 @@ public void GenericTypeArgument(bool rebuild, bool access) var nestedClass = (TypeDefinition) module.LookupMember(typeof(TestGenericType<>).MetadataToken); var expected = new GenericInstanceTypeSignature(nestedClass, false, module.CorLibTypeFactory.Object); - Assert.IsAssignableFrom(argument.Element); - Assert.Equal(expected, (TypeSignature) argument.Element, _comparer); + var element = Assert.IsAssignableFrom(argument.Element); + Assert.Equal(expected, element, _comparer); } [Theory] @@ -307,8 +316,8 @@ public void ArrayGenericTypeArgument(bool rebuild, bool access) new GenericInstanceTypeSignature(nestedClass, false, module.CorLibTypeFactory.Object) ); - Assert.IsAssignableFrom(argument.Element); - Assert.Equal(expected, (TypeSignature) argument.Element, _comparer); + var element = Assert.IsAssignableFrom(argument.Element); + Assert.Equal(expected, element, _comparer); } [Theory] @@ -322,8 +331,8 @@ public void IntPassedOnAsObject(bool rebuild, bool access) var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.Int32PassedAsObject),rebuild, access); var argument = attribute.Signature!.FixedArguments[0]; - Assert.IsAssignableFrom(argument.Element); - Assert.Equal(123, ((BoxedArgument) argument.Element).Value); + var element = Assert.IsAssignableFrom(argument.Element); + Assert.Equal(123, element.Value); } [Theory] @@ -338,8 +347,8 @@ public void TypePassedOnAsObject(bool rebuild, bool access) var argument = attribute.Signature!.FixedArguments[0]; var module = attribute.Constructor!.Module!; - Assert.IsAssignableFrom(argument.Element); - Assert.Equal(module.CorLibTypeFactory.Int32, (ITypeDescriptor) ((BoxedArgument) argument.Element).Value, _comparer); + var element = Assert.IsAssignableFrom(argument.Element); + Assert.Equal(module.CorLibTypeFactory.Int32, (ITypeDescriptor) element.Value, _comparer); } [Theory] @@ -391,8 +400,7 @@ public void FixedInt32ArrayNullAsObject(bool rebuild, bool access) var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.FixedInt32ArrayAsObjectNullArgument),rebuild, access); var argument = attribute.Signature!.FixedArguments[0]; - Assert.IsAssignableFrom(argument.Element); - var boxedArgument = (BoxedArgument) argument.Element; + var boxedArgument = Assert.IsAssignableFrom(argument.Element); Assert.Null(boxedArgument.Value); } @@ -405,8 +413,7 @@ public void FixedInt32EmptyArrayAsObject(bool rebuild, bool access) var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.FixedInt32ArrayAsObjectEmptyArgument),rebuild, access); var argument = attribute.Signature!.FixedArguments[0]; - Assert.IsAssignableFrom(argument.Element); - var boxedArgument = (BoxedArgument) argument.Element; + var boxedArgument =Assert.IsAssignableFrom(argument.Element); Assert.Equal(Array.Empty(), boxedArgument.Value); } @@ -419,8 +426,7 @@ public void FixedInt32ArrayAsObject(bool rebuild, bool access) var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.FixedInt32ArrayAsObjectArgument),rebuild, access); var argument = attribute.Signature!.FixedArguments[0]; - Assert.IsAssignableFrom(argument.Element); - var boxedArgument = (BoxedArgument) argument.Element; + var boxedArgument = Assert.IsAssignableFrom(argument.Element); Assert.Equal(new[] { 1, 2, 3, 4 @@ -464,5 +470,169 @@ public void CreateNewWithFixedArgumentsViaProperty() var argument = Assert.Single(attribute.Signature.FixedArguments); Assert.Equal("My Message", argument.Element); } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(true, true)] + public void FixedGenericInt32Argument(bool rebuild, bool access) + { + var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.FixedGenericInt32Argument), rebuild, access); + var argument = attribute.Signature!.FixedArguments[0]; + + int value = Assert.IsAssignableFrom(argument.Element); + Assert.Equal(1, value); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(true, true)] + public void FixedGenericStringArgument(bool rebuild, bool access) + { + var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.FixedGenericStringArgument), rebuild, access); + var argument = attribute.Signature!.FixedArguments[0]; + + string value = Assert.IsAssignableFrom(argument.Element); + Assert.Equal("Fixed string generic argument", value); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(true, true)] + public void FixedGenericInt32ArrayArgument(bool rebuild, bool access) + { + var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.FixedGenericInt32ArrayArgument), rebuild, access); + var argument = attribute.Signature!.FixedArguments[0]; + + Assert.Equal(new int[] {1, 2, 3, 4}, argument.Elements.Cast()); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(true, true)] + public void FixedGenericInt32ArrayAsObjectArgument(bool rebuild, bool access) + { + var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.FixedGenericInt32ArrayAsObjectArgument), rebuild, access); + var argument = attribute.Signature!.FixedArguments[0]; + + var boxedArgument = Assert.IsAssignableFrom(argument.Element); + Assert.Equal(new[] + { + 1, 2, 3, 4 + }, boxedArgument.Value); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(true, true)] + public void FixedGenericTypeArgument(bool rebuild, bool access) + { + var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.FixedGenericTypeArgument), rebuild, access); + var argument = attribute.Signature!.FixedArguments[0]; + + var expected = attribute.Constructor!.Module!.CorLibTypeFactory.Int32; + var element = Assert.IsAssignableFrom(argument.Element); + Assert.Equal(expected, element, _comparer); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(true, true)] + public void FixedGenericTypeNullArgument(bool rebuild, bool access) + { + var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.FixedGenericTypeNullArgument), rebuild, access); + var argument = attribute.Signature!.FixedArguments[0]; + + Assert.Null(argument.Element); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(true, true)] + public void NamedGenericInt32Argument(bool rebuild, bool access) + { + var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.NamedGenericInt32Argument), rebuild, access); + var argument = attribute.Signature!.NamedArguments[0]; + + Assert.Equal("Value", argument.MemberName); + int value = Assert.IsAssignableFrom(argument.Argument.Element); + Assert.Equal(1, value); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(true, true)] + public void NamedGenericStringArgument(bool rebuild, bool access) + { + var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.NamedGenericStringArgument), rebuild, access); + var argument = attribute.Signature!.NamedArguments[0]; + + Assert.Equal("Value", argument.MemberName); + string value = Assert.IsAssignableFrom(argument.Argument.Element); + Assert.Equal("Named string generic argument", value); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(true, true)] + public void NamedGenericInt32ArrayArgument(bool rebuild, bool access) + { + var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.NamedGenericInt32ArrayArgument), rebuild, access); + var argument = attribute.Signature!.NamedArguments[0]; + + Assert.Equal("Value", argument.MemberName); + Assert.Equal(new int[] {1,2,3,4}, argument.Argument.Elements.Cast()); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(true, true)] + public void NamedGenericInt32ArrayAsObjectArgument(bool rebuild, bool access) + { + var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.NamedGenericInt32ArrayAsObjectArgument), rebuild, access); + var argument = attribute.Signature!.NamedArguments[0]; + + Assert.Equal("Value", argument.MemberName); + var boxedArgument = Assert.IsAssignableFrom(argument.Argument.Element); + Assert.Equal(new[] + { + 1, 2, 3, 4 + }, boxedArgument.Value); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(true, true)] + public void NamedGenericTypeArgument(bool rebuild, bool access) + { + var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.NamedGenericTypeArgument), rebuild, access); + var argument = attribute.Signature!.NamedArguments[0]; + + var expected = attribute.Constructor!.Module!.CorLibTypeFactory.Int32; + var element = Assert.IsAssignableFrom(argument.Argument.Element); + Assert.Equal(expected, element, _comparer); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(true, true)] + public void NamedGenericTypeNullArgument(bool rebuild, bool access) + { + var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.NamedGenericTypeNullArgument), rebuild, access); + var argument = attribute.Signature!.NamedArguments[0]; + + Assert.Null(argument.Argument.Element); + } } } diff --git a/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.CustomAttributes/AsmResolver.DotNet.TestCases.CustomAttributes.csproj b/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.CustomAttributes/AsmResolver.DotNet.TestCases.CustomAttributes.csproj index 27560206d..a2f2e3d95 100644 --- a/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.CustomAttributes/AsmResolver.DotNet.TestCases.CustomAttributes.csproj +++ b/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.CustomAttributes/AsmResolver.DotNet.TestCases.CustomAttributes.csproj @@ -2,6 +2,7 @@ netstandard2.0 + 11 diff --git a/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.CustomAttributes/CustomAttributesTestClass.cs b/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.CustomAttributes/CustomAttributesTestClass.cs index 852f2d44f..f4444fa45 100644 --- a/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.CustomAttributes/CustomAttributesTestClass.cs +++ b/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.CustomAttributes/CustomAttributesTestClass.cs @@ -134,5 +134,64 @@ public void FixedNullTypeArgument() { } + [TestCase(1)] + public void FixedGenericInt32Argument() + { + } + + [TestCase("Fixed string generic argument")] + public void FixedGenericStringArgument() + { + } + + [TestCase(new int[] {1,2,3,4})] + public void FixedGenericInt32ArrayArgument() + { + } + + [TestCase(new int[] {1,2,3,4})] + public void FixedGenericInt32ArrayAsObjectArgument() + { + } + + [TestCase(typeof(int))] + public void FixedGenericTypeArgument() + { + } + + [TestCase(null)] + public void FixedGenericTypeNullArgument() + { + } + + [TestCase(Value = 1)] + public void NamedGenericInt32Argument() + { + } + + [TestCase(Value = "Named string generic argument")] + public void NamedGenericStringArgument() + { + } + + [TestCase(Value = new int[] {1,2,3,4})] + public void NamedGenericInt32ArrayArgument() + { + } + + [TestCase(Value = new int[] {1,2,3,4})] + public void NamedGenericInt32ArrayAsObjectArgument() + { + } + + [TestCase(Value = typeof(int))] + public void NamedGenericTypeArgument() + { + } + + [TestCase(Value = null)] + public void NamedGenericTypeNullArgument() + { + } } } diff --git a/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.CustomAttributes/TestCaseAttribute.Generic.cs b/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.CustomAttributes/TestCaseAttribute.Generic.cs new file mode 100644 index 000000000..096766fa0 --- /dev/null +++ b/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.CustomAttributes/TestCaseAttribute.Generic.cs @@ -0,0 +1,24 @@ +using System; + +namespace AsmResolver.DotNet.TestCases.CustomAttributes +{ + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] + public class TestCaseAttribute : Attribute + { + public TestCaseAttribute() + { + Value = default; + } + + public TestCaseAttribute(T value) + { + Value = value; + } + + public T Value + { + get; + set; + } + } +} From 220f611c7da36021899343dd2d0e0b3306675b1b Mon Sep 17 00:00:00 2001 From: Washi Date: Thu, 5 Jan 2023 20:57:05 +0100 Subject: [PATCH 13/35] Incorporate GenericContext into custom attribute argument reader. --- .../CustomAttributeNamedArgument.cs | 4 +- .../Signatures/CustomAttributeSignature.cs | 3 +- .../Signatures/Security/SecurityAttribute.cs | 3 +- .../SerializedCustomAttributeSignature.cs | 29 +++++++++---- .../CustomAttributeTest.cs | 42 ++++++++++++------- 5 files changed, 55 insertions(+), 26 deletions(-) diff --git a/src/AsmResolver.DotNet/Signatures/CustomAttributeNamedArgument.cs b/src/AsmResolver.DotNet/Signatures/CustomAttributeNamedArgument.cs index 6d4e08f62..0790e15e5 100644 --- a/src/AsmResolver.DotNet/Signatures/CustomAttributeNamedArgument.cs +++ b/src/AsmResolver.DotNet/Signatures/CustomAttributeNamedArgument.cs @@ -67,9 +67,11 @@ public CustomAttributeArgument Argument /// Reads a single named argument from the input stream. /// /// The blob reader context. + /// /// The input stream. /// The argument. - public static CustomAttributeNamedArgument FromReader(in BlobReaderContext context, ref BinaryStreamReader reader) + public static CustomAttributeNamedArgument FromReader(in BlobReaderContext context, + GenericContext genericContext, ref BinaryStreamReader reader) { var memberType = (CustomAttributeArgumentMemberType) reader.ReadByte(); var argumentType = TypeSignature.ReadFieldOrPropType(context, ref reader); diff --git a/src/AsmResolver.DotNet/Signatures/CustomAttributeSignature.cs b/src/AsmResolver.DotNet/Signatures/CustomAttributeSignature.cs index 1a399f616..eaa877fdb 100644 --- a/src/AsmResolver.DotNet/Signatures/CustomAttributeSignature.cs +++ b/src/AsmResolver.DotNet/Signatures/CustomAttributeSignature.cs @@ -90,8 +90,9 @@ public static CustomAttributeSignature FromReader( ICustomAttributeType ctor, in BinaryStreamReader reader) { + var genericContext = GenericContext.FromMethod(ctor); var argumentTypes = ctor.Signature?.ParameterTypes ?? Array.Empty(); - return new SerializedCustomAttributeSignature(context, argumentTypes, reader); + return new SerializedCustomAttributeSignature(context, argumentTypes, genericContext, reader); } /// diff --git a/src/AsmResolver.DotNet/Signatures/Security/SecurityAttribute.cs b/src/AsmResolver.DotNet/Signatures/Security/SecurityAttribute.cs index abb55b287..68a7038d6 100644 --- a/src/AsmResolver.DotNet/Signatures/Security/SecurityAttribute.cs +++ b/src/AsmResolver.DotNet/Signatures/Security/SecurityAttribute.cs @@ -38,9 +38,10 @@ public static SecurityAttribute FromReader(in BlobReaderContext context, ref Bin return result; } + var genericContext = GenericContext.FromType(type); for (int i = 0; i < namedArgumentCount; i++) { - var argument = CustomAttributeNamedArgument.FromReader(context, ref reader); + var argument = CustomAttributeNamedArgument.FromReader(context, genericContext, ref reader); result.NamedArguments.Add(argument); } diff --git a/src/AsmResolver.DotNet/Signatures/SerializedCustomAttributeSignature.cs b/src/AsmResolver.DotNet/Signatures/SerializedCustomAttributeSignature.cs index a7e6e6028..91b35e32c 100644 --- a/src/AsmResolver.DotNet/Signatures/SerializedCustomAttributeSignature.cs +++ b/src/AsmResolver.DotNet/Signatures/SerializedCustomAttributeSignature.cs @@ -10,22 +10,25 @@ namespace AsmResolver.DotNet.Signatures /// public class SerializedCustomAttributeSignature : CustomAttributeSignature { - private readonly BlobReaderContext _context; + private readonly BlobReaderContext _readerContext; + private readonly GenericContext _genericContext; private readonly TypeSignature[] _fixedArgTypes; private readonly BinaryStreamReader _reader; /// /// Initializes a new lazy custom attribute signature from an input blob stream reader. /// - /// The blob reading context the signature is situated in. + /// The blob reading context the signature is situated in. /// The types of all fixed arguments. + /// /// The input blob reader. - public SerializedCustomAttributeSignature( - in BlobReaderContext context, + public SerializedCustomAttributeSignature(in BlobReaderContext readerContext, IEnumerable fixedArgTypes, + in GenericContext genericContext, in BinaryStreamReader reader) { - _context = context; + _readerContext = readerContext; + _genericContext = genericContext; _fixedArgTypes = fixedArgTypes.ToArray(); _reader = reader; } @@ -40,16 +43,26 @@ protected override void Initialize( // Verify magic header. ushort prologue = reader.ReadUInt16(); if (prologue != CustomAttributeSignaturePrologue) - _context.ReaderContext.BadImage("Input stream does not point to a valid custom attribute signature."); + _readerContext.ReaderContext.BadImage("Input stream does not point to a valid custom attribute signature."); // Read fixed arguments. for (int i = 0; i < _fixedArgTypes.Length; i++) - fixedArguments.Add(CustomAttributeArgument.FromReader(_context, _fixedArgTypes[i], ref reader)); + { + fixedArguments.Add(CustomAttributeArgument.FromReader( + _readerContext, + _fixedArgTypes[i].InstantiateGenericTypes(_genericContext), + ref reader)); + } // Read named arguments. ushort namedArgumentCount = reader.ReadUInt16(); for (int i = 0; i < namedArgumentCount; i++) - namedArguments.Add(CustomAttributeNamedArgument.FromReader(_context, ref reader)); + { + namedArguments.Add(CustomAttributeNamedArgument.FromReader( + _readerContext, + _genericContext, + ref reader)); + } } /// diff --git a/test/AsmResolver.DotNet.Tests/CustomAttributeTest.cs b/test/AsmResolver.DotNet.Tests/CustomAttributeTest.cs index 6c57703a7..b93e25f7e 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 == attributeName); + .First(c => c.Constructor!.DeclaringType!.Name.Value.StartsWith(attributeName)); if (access) { @@ -477,7 +477,8 @@ public void CreateNewWithFixedArgumentsViaProperty() [InlineData(true, true)] public void FixedGenericInt32Argument(bool rebuild, bool access) { - var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.FixedGenericInt32Argument), rebuild, access); + var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.FixedGenericInt32Argument), + rebuild, access, true); var argument = attribute.Signature!.FixedArguments[0]; int value = Assert.IsAssignableFrom(argument.Element); @@ -490,10 +491,11 @@ public void FixedGenericInt32Argument(bool rebuild, bool access) [InlineData(true, true)] public void FixedGenericStringArgument(bool rebuild, bool access) { - var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.FixedGenericStringArgument), rebuild, access); + var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.FixedGenericStringArgument), + rebuild, access, true); var argument = attribute.Signature!.FixedArguments[0]; - string value = Assert.IsAssignableFrom(argument.Element); + string value = Assert.IsAssignableFrom(argument.Element); Assert.Equal("Fixed string generic argument", value); } @@ -503,7 +505,8 @@ public void FixedGenericStringArgument(bool rebuild, bool access) [InlineData(true, true)] public void FixedGenericInt32ArrayArgument(bool rebuild, bool access) { - var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.FixedGenericInt32ArrayArgument), rebuild, access); + var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.FixedGenericInt32ArrayArgument), + rebuild, access, true); var argument = attribute.Signature!.FixedArguments[0]; Assert.Equal(new int[] {1, 2, 3, 4}, argument.Elements.Cast()); @@ -515,7 +518,8 @@ public void FixedGenericInt32ArrayArgument(bool rebuild, bool access) [InlineData(true, true)] public void FixedGenericInt32ArrayAsObjectArgument(bool rebuild, bool access) { - var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.FixedGenericInt32ArrayAsObjectArgument), rebuild, access); + var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.FixedGenericInt32ArrayAsObjectArgument), + rebuild, access, true); var argument = attribute.Signature!.FixedArguments[0]; var boxedArgument = Assert.IsAssignableFrom(argument.Element); @@ -531,7 +535,8 @@ public void FixedGenericInt32ArrayAsObjectArgument(bool rebuild, bool access) [InlineData(true, true)] public void FixedGenericTypeArgument(bool rebuild, bool access) { - var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.FixedGenericTypeArgument), rebuild, access); + var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.FixedGenericTypeArgument), + rebuild, access, true); var argument = attribute.Signature!.FixedArguments[0]; var expected = attribute.Constructor!.Module!.CorLibTypeFactory.Int32; @@ -545,7 +550,8 @@ public void FixedGenericTypeArgument(bool rebuild, bool access) [InlineData(true, true)] public void FixedGenericTypeNullArgument(bool rebuild, bool access) { - var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.FixedGenericTypeNullArgument), rebuild, access); + var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.FixedGenericTypeNullArgument), + rebuild, access, true); var argument = attribute.Signature!.FixedArguments[0]; Assert.Null(argument.Element); @@ -557,7 +563,8 @@ public void FixedGenericTypeNullArgument(bool rebuild, bool access) [InlineData(true, true)] public void NamedGenericInt32Argument(bool rebuild, bool access) { - var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.NamedGenericInt32Argument), rebuild, access); + var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.NamedGenericInt32Argument), + rebuild, access, true); var argument = attribute.Signature!.NamedArguments[0]; Assert.Equal("Value", argument.MemberName); @@ -571,11 +578,12 @@ public void NamedGenericInt32Argument(bool rebuild, bool access) [InlineData(true, true)] public void NamedGenericStringArgument(bool rebuild, bool access) { - var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.NamedGenericStringArgument), rebuild, access); + var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.NamedGenericStringArgument), + rebuild, access, true); var argument = attribute.Signature!.NamedArguments[0]; Assert.Equal("Value", argument.MemberName); - string value = Assert.IsAssignableFrom(argument.Argument.Element); + string value = Assert.IsAssignableFrom(argument.Argument.Element); Assert.Equal("Named string generic argument", value); } @@ -585,7 +593,8 @@ public void NamedGenericStringArgument(bool rebuild, bool access) [InlineData(true, true)] public void NamedGenericInt32ArrayArgument(bool rebuild, bool access) { - var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.NamedGenericInt32ArrayArgument), rebuild, access); + var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.NamedGenericInt32ArrayArgument), + rebuild, access, true); var argument = attribute.Signature!.NamedArguments[0]; Assert.Equal("Value", argument.MemberName); @@ -598,7 +607,8 @@ public void NamedGenericInt32ArrayArgument(bool rebuild, bool access) [InlineData(true, true)] public void NamedGenericInt32ArrayAsObjectArgument(bool rebuild, bool access) { - var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.NamedGenericInt32ArrayAsObjectArgument), rebuild, access); + var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.NamedGenericInt32ArrayAsObjectArgument), + rebuild, access, true); var argument = attribute.Signature!.NamedArguments[0]; Assert.Equal("Value", argument.MemberName); @@ -615,7 +625,8 @@ public void NamedGenericInt32ArrayAsObjectArgument(bool rebuild, bool access) [InlineData(true, true)] public void NamedGenericTypeArgument(bool rebuild, bool access) { - var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.NamedGenericTypeArgument), rebuild, access); + var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.NamedGenericTypeArgument), + rebuild, access, true); var argument = attribute.Signature!.NamedArguments[0]; var expected = attribute.Constructor!.Module!.CorLibTypeFactory.Int32; @@ -629,7 +640,8 @@ public void NamedGenericTypeArgument(bool rebuild, bool access) [InlineData(true, true)] public void NamedGenericTypeNullArgument(bool rebuild, bool access) { - var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.NamedGenericTypeNullArgument), rebuild, access); + var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.NamedGenericTypeNullArgument), + rebuild, access, true); var argument = attribute.Signature!.NamedArguments[0]; Assert.Null(argument.Argument.Element); From d72edea8bbd6cda38eaa542dd5c79958a6fa0f43 Mon Sep 17 00:00:00 2001 From: Washi Date: Thu, 5 Jan 2023 21:03:13 +0100 Subject: [PATCH 14/35] Remove redundant generic context parameter for named arguments. --- .../Signatures/CustomAttributeNamedArgument.cs | 4 +--- .../Signatures/Security/SecurityAttribute.cs | 2 +- .../SerializedCustomAttributeSignature.cs | 15 ++++----------- 3 files changed, 6 insertions(+), 15 deletions(-) diff --git a/src/AsmResolver.DotNet/Signatures/CustomAttributeNamedArgument.cs b/src/AsmResolver.DotNet/Signatures/CustomAttributeNamedArgument.cs index 0790e15e5..6d4e08f62 100644 --- a/src/AsmResolver.DotNet/Signatures/CustomAttributeNamedArgument.cs +++ b/src/AsmResolver.DotNet/Signatures/CustomAttributeNamedArgument.cs @@ -67,11 +67,9 @@ public CustomAttributeArgument Argument /// Reads a single named argument from the input stream. /// /// The blob reader context. - /// /// The input stream. /// The argument. - public static CustomAttributeNamedArgument FromReader(in BlobReaderContext context, - GenericContext genericContext, ref BinaryStreamReader reader) + public static CustomAttributeNamedArgument FromReader(in BlobReaderContext context, ref BinaryStreamReader reader) { var memberType = (CustomAttributeArgumentMemberType) reader.ReadByte(); var argumentType = TypeSignature.ReadFieldOrPropType(context, ref reader); diff --git a/src/AsmResolver.DotNet/Signatures/Security/SecurityAttribute.cs b/src/AsmResolver.DotNet/Signatures/Security/SecurityAttribute.cs index 68a7038d6..4631a5925 100644 --- a/src/AsmResolver.DotNet/Signatures/Security/SecurityAttribute.cs +++ b/src/AsmResolver.DotNet/Signatures/Security/SecurityAttribute.cs @@ -41,7 +41,7 @@ public static SecurityAttribute FromReader(in BlobReaderContext context, ref Bin var genericContext = GenericContext.FromType(type); for (int i = 0; i < namedArgumentCount; i++) { - var argument = CustomAttributeNamedArgument.FromReader(context, genericContext, ref reader); + var argument = CustomAttributeNamedArgument.FromReader(context, ref reader); result.NamedArguments.Add(argument); } diff --git a/src/AsmResolver.DotNet/Signatures/SerializedCustomAttributeSignature.cs b/src/AsmResolver.DotNet/Signatures/SerializedCustomAttributeSignature.cs index 91b35e32c..4dfadd299 100644 --- a/src/AsmResolver.DotNet/Signatures/SerializedCustomAttributeSignature.cs +++ b/src/AsmResolver.DotNet/Signatures/SerializedCustomAttributeSignature.cs @@ -20,7 +20,7 @@ public class SerializedCustomAttributeSignature : CustomAttributeSignature /// /// The blob reading context the signature is situated in. /// The types of all fixed arguments. - /// + /// The generic context the arguments live in. /// The input blob reader. public SerializedCustomAttributeSignature(in BlobReaderContext readerContext, IEnumerable fixedArgTypes, @@ -48,21 +48,14 @@ protected override void Initialize( // Read fixed arguments. for (int i = 0; i < _fixedArgTypes.Length; i++) { - fixedArguments.Add(CustomAttributeArgument.FromReader( - _readerContext, - _fixedArgTypes[i].InstantiateGenericTypes(_genericContext), - ref reader)); + var instantiatedType = _fixedArgTypes[i].InstantiateGenericTypes(_genericContext); + fixedArguments.Add(CustomAttributeArgument.FromReader(_readerContext, instantiatedType, ref reader)); } // Read named arguments. ushort namedArgumentCount = reader.ReadUInt16(); for (int i = 0; i < namedArgumentCount; i++) - { - namedArguments.Add(CustomAttributeNamedArgument.FromReader( - _readerContext, - _genericContext, - ref reader)); - } + namedArguments.Add(CustomAttributeNamedArgument.FromReader(_readerContext, ref reader)); } /// From 9ce1f9fc38b6be61f4267c51d6d684c3b645a4dc Mon Sep 17 00:00:00 2001 From: Washi Date: Thu, 5 Jan 2023 21:18:07 +0100 Subject: [PATCH 15/35] Add generic overloads for [Try]LookupMember. --- src/AsmResolver.DotNet/ModuleDefinition.cs | 44 ++++++++++- .../ModuleDefinitionTest.cs | 78 +++++++++++++++---- 2 files changed, 103 insertions(+), 19 deletions(-) diff --git a/src/AsmResolver.DotNet/ModuleDefinition.cs b/src/AsmResolver.DotNet/ModuleDefinition.cs index ffb018860..710745aa4 100644 --- a/src/AsmResolver.DotNet/ModuleDefinition.cs +++ b/src/AsmResolver.DotNet/ModuleDefinition.cs @@ -792,7 +792,7 @@ public ReferenceImporter DefaultImporter /// /// Looks up a member by its metadata token. /// - /// The token of the member to lookup. + /// The token of the member to look up. /// The member. /// /// Occurs when the module does not support looking up members by its token. @@ -803,10 +803,28 @@ public ReferenceImporter DefaultImporter public virtual IMetadataMember LookupMember(MetadataToken token) => throw new InvalidOperationException("Cannot lookup members by tokens from a non-serialized module."); + /// + /// Looks up a member by its metadata token, and casts it to the provided metadata member type. + /// + /// The token of the member to look up. + /// The type of member to look up. + /// The casted member. + /// + /// Occurs when the module does not support looking up members by its token. + /// + /// + /// Occurs when a metadata token indexes a table that cannot be converted to a metadata member. + /// + public T LookupMember(MetadataToken token) + where T : class, IMetadataMember + { + return (T) LookupMember(token); + } + /// /// Attempts to look up a member by its metadata token. /// - /// The token of the member to lookup. + /// The token of the member to look up. /// The member, or null if the lookup failed. /// true if the member was successfully looked up, false otherwise. public virtual bool TryLookupMember(MetadataToken token, [NotNullWhen(true)] out IMetadataMember? member) @@ -815,10 +833,30 @@ public virtual bool TryLookupMember(MetadataToken token, [NotNullWhen(true)] out return false; } + /// + /// Attempts to look up a member by its metadata token, and cast it to the specified metadata member type. + /// + /// The token of the member to look up. + /// The member, or null if the lookup failed. + /// The type of member to look up. + /// true if the member was successfully looked up and of the correct type, false otherwise. + public bool TryLookupMember(MetadataToken token, [NotNullWhen((true))] out T? member) + where T : class, IMetadataMember + { + if (TryLookupMember(token, out var resolved) && resolved is T casted) + { + member = casted; + return true; + } + + member = null; + return false; + } + /// /// Looks up a user string by its string token. /// - /// The token of the string to lookup. + /// The token of the string to look up. /// The member. /// /// Occurs when the module does not support looking up string by its token. diff --git a/test/AsmResolver.DotNet.Tests/ModuleDefinitionTest.cs b/test/AsmResolver.DotNet.Tests/ModuleDefinitionTest.cs index e527ce48c..76e9b61fd 100644 --- a/test/AsmResolver.DotNet.Tests/ModuleDefinitionTest.cs +++ b/test/AsmResolver.DotNet.Tests/ModuleDefinitionTest.cs @@ -149,11 +149,30 @@ public void LookupTypeReference() { var module = ModuleDefinition.FromBytes(Properties.Resources.HelloWorld); var member = module.LookupMember(new MetadataToken(TableIndex.TypeRef, 12)); - Assert.IsAssignableFrom(member); - var typeRef = (TypeReference)member; - Assert.Equal("System", typeRef.Namespace); - Assert.Equal("Object", typeRef.Name); + var reference = Assert.IsAssignableFrom(member); + Assert.Equal("System", reference.Namespace); + Assert.Equal("Object", reference.Name); + } + + [Fact] + public void LookupTypeReferenceStronglyTyped() + { + var module = ModuleDefinition.FromBytes(Properties.Resources.HelloWorld); + var reference = module.LookupMember(new MetadataToken(TableIndex.TypeRef, 12)); + + Assert.Equal("System", reference.Namespace); + Assert.Equal("Object", reference.Name); + } + + [Fact] + public void TryLookupTypeReferenceStronglyTyped() + { + var module = ModuleDefinition.FromBytes(Properties.Resources.HelloWorld); + + Assert.True(module.TryLookupMember(new MetadataToken(TableIndex.TypeRef, 12), out TypeReference reference)); + Assert.Equal("System", reference.Namespace); + Assert.Equal("Object", reference.Name); } [Fact] @@ -161,11 +180,20 @@ public void LookupTypeDefinition() { var module = ModuleDefinition.FromBytes(Properties.Resources.HelloWorld); var member = module.LookupMember(new MetadataToken(TableIndex.TypeDef, 2)); - Assert.IsAssignableFrom(member); - var typeDef = (TypeDefinition)member; - Assert.Equal("HelloWorld", typeDef.Namespace); - Assert.Equal("Program", typeDef.Name); + var definition = Assert.IsAssignableFrom(member); + Assert.Equal("HelloWorld", definition.Namespace); + Assert.Equal("Program", definition.Name); + } + + [Fact] + public void LookupTypeDefinitionStronglyTyped() + { + var module = ModuleDefinition.FromBytes(Properties.Resources.HelloWorld); + var definition = module.LookupMember(new MetadataToken(TableIndex.TypeDef, 2)); + + Assert.Equal("HelloWorld", definition.Namespace); + Assert.Equal("Program", definition.Name); } [Fact] @@ -173,11 +201,20 @@ public void LookupAssemblyReference() { var module = ModuleDefinition.FromBytes(Properties.Resources.HelloWorld); var member = module.LookupMember(new MetadataToken(TableIndex.AssemblyRef, 1)); - Assert.IsAssignableFrom(member); - var assemblyRef = (AssemblyReference)member; - Assert.Equal("mscorlib", assemblyRef.Name); - Assert.Same(module.AssemblyReferences[0], assemblyRef); + var reference = Assert.IsAssignableFrom(member); + Assert.Equal("mscorlib", reference.Name); + Assert.Same(module.AssemblyReferences[0], reference); + } + + [Fact] + public void LookupAssemblyReferenceStronglyTyped() + { + var module = ModuleDefinition.FromBytes(Properties.Resources.HelloWorld); + var reference = module.LookupMember(new MetadataToken(TableIndex.AssemblyRef, 1)); + + Assert.Equal("mscorlib", reference.Name); + Assert.Same(module.AssemblyReferences[0], reference); } [Fact] @@ -185,11 +222,20 @@ public void LookupModuleReference() { var module = ModuleDefinition.FromFile(Path.Combine("Resources", "Manifest.exe")); var member = module.LookupMember(new MetadataToken(TableIndex.ModuleRef, 1)); - Assert.IsAssignableFrom(member); - var moduleRef = (ModuleReference)member; - Assert.Equal("MyModel.netmodule", moduleRef.Name); - Assert.Same(module.ModuleReferences[0], moduleRef); + var reference =Assert.IsAssignableFrom(member); + Assert.Equal("MyModel.netmodule", reference.Name); + Assert.Same(module.ModuleReferences[0], reference); + } + + [Fact] + public void LookupModuleReferenceStronglyTyped() + { + var module = ModuleDefinition.FromFile(Path.Combine("Resources", "Manifest.exe")); + var reference = module.LookupMember(new MetadataToken(TableIndex.ModuleRef, 1)); + + Assert.Equal("MyModel.netmodule", reference.Name); + Assert.Same(module.ModuleReferences[0], reference); } [Fact] From d35a232acd8e53bb5f641ab85f29f57b27a11875 Mon Sep 17 00:00:00 2001 From: Washi Date: Thu, 5 Jan 2023 22:20:20 +0100 Subject: [PATCH 16/35] Pass on docs. --- docs/dotnet/advanced-module-reading.rst | 4 +-- docs/dotnet/advanced-pe-image-building.rst | 4 +-- docs/dotnet/cloning.rst | 10 +++--- docs/dotnet/dynamic-methods.rst | 2 +- docs/dotnet/importing.rst | 6 ++-- docs/dotnet/index.rst | 4 +-- docs/dotnet/member-tree.rst | 2 +- docs/dotnet/token-allocation.rst | 2 +- docs/dotnet/type-signatures.rst | 39 ++++++++++++++++++++-- docs/pefile/basics.rst | 4 +-- docs/peimage/debug.rst | 8 ++--- docs/peimage/dotnet.rst | 6 ++-- docs/peimage/pe-building.rst | 8 ++--- docs/peimage/tls.rst | 2 +- 14 files changed, 68 insertions(+), 33 deletions(-) diff --git a/docs/dotnet/advanced-module-reading.rst b/docs/dotnet/advanced-module-reading.rst index 790793dac..fa0907281 100644 --- a/docs/dotnet/advanced-module-reading.rst +++ b/docs/dotnet/advanced-module-reading.rst @@ -3,7 +3,7 @@ Advanced Module Reading ======================= -Advanced users might have the need to configure AsmResolver's module reader. For example, instead of letting the module reader throw exceptions upon reading invalid data, errors should be ignored and recovered from. Other uses might include changing the way the underlying PE or method bodies are read. These kinds of settings can be configured using the ``ModuleReaderParameters`` class. +Advanced users might need to configure AsmResolver's module reader. For example, instead of letting the module reader throw exceptions upon reading invalid data, errors should be ignored and recovered from. Other uses might include changing the way the underlying PE or method bodies are read. These kinds of settings can be configured using the ``ModuleReaderParameters`` class. .. code-block:: csharp @@ -131,7 +131,7 @@ To let the reader use this implementation of the ``IMethodBodyReader``, set the Custom Field RVA reading ------------------------ -By default, the field RVA data storing the initial binary value of a field is interpreted as raw byte blobs, and are turned into instances of the ``DataSegment`` class. To adjust this behaviour, it is possible provide a custom implementation of the ``IFieldRvaDataReader`` interface. +By default, the field RVA data storing the initial binary value of a field is interpreted as raw byte blobs, and are turned into instances of the ``DataSegment`` class. To adjust this behaviour, it is possible to provide a custom implementation of the ``IFieldRvaDataReader`` interface. .. code-block:: csharp diff --git a/docs/dotnet/advanced-pe-image-building.rst b/docs/dotnet/advanced-pe-image-building.rst index 8acc2176f..6efa35b88 100644 --- a/docs/dotnet/advanced-pe-image-building.rst +++ b/docs/dotnet/advanced-pe-image-building.rst @@ -12,7 +12,7 @@ The easiest way to write a .NET module to the disk is by using the ``Write`` met 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. -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, were we pass on a custom ``IPEFileBuilder``, or a configured ``ManagedPEImageBuilder``: +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, where we pass on a custom ``IPEFileBuilder``, or a configured ``ManagedPEImageBuilder``: .. code-block:: csharp @@ -22,7 +22,7 @@ While this is easy, and would probably work for most .NET module processing, it module.Write(@"C:\Path\To\Output\Binary.exe", imageBuilder); -Alternatively, it is possible to call the ``CreateImage`` method 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. +Alternatively, it is possible to call the ``CreateImage`` method 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. .. code-block:: csharp diff --git a/docs/dotnet/cloning.rst b/docs/dotnet/cloning.rst index 871f6b499..6797896ef 100644 --- a/docs/dotnet/cloning.rst +++ b/docs/dotnet/cloning.rst @@ -1,7 +1,7 @@ Member Cloning ============== -When processing a .NET module, it often involves injecting additional code. Even though all models representing .NET metadata and CIL code are mutable, it might be very time consuming and error-prone to manually import and inject metadata members and/or CIL code into the target module. +Processing a .NET module often involves injecting additional code. Even though all models representing .NET metadata and CIL code are mutable, it might be very time-consuming and error-prone to manually import and inject metadata members and/or CIL code into the target module. To help developers in injecting existing code into a module, ``AsmResolver.DotNet`` comes with a feature that involves cloning metadata members from one module and copying it over to another. All relevant classes are in the ``AsmResolver.DotNet.Cloning`` namespace: @@ -116,7 +116,7 @@ When all members are included, it is possible to call ``MemberCloner.Clone`` to var result = cloner.Clone(); -The ``MemberCloner`` will automatically resolve any cross references between types, fields and methods that are included in the cloning process. +The ``MemberCloner`` will automatically resolve any cross-references between types, fields and methods that are included in the cloning process. For instance, going with the example in the previous section, if both the ``Rectangle`` as well as the ``Vector2`` classes are included, any reference in ``Rectangle`` to ``Vector2`` will be replaced with a reference to the cloned ``Vector2``. If not all members are included, the ``MemberCloner`` will assume that these are references to external libraries, and will use the ``ReferenceImporter`` to construct references to these members instead. @@ -173,12 +173,12 @@ All references to methods defined in the ``System.Runtime.CompilerServices`` nam See :ref:`dotnet-importer-common-caveats` for more information on reference importing and its caveats. -Post processing of cloned members +Post-processing of cloned members --------------------------------- In some cases, cloned members may need to be post-processed before they are injected into the target module. The ``MemberCloner`` class can be initialized with an instance of a ``IMemberClonerListener``, that gets notified by the cloner object every time a definition was cloned. -Below an example that appends the string ``_Cloned`` to the name for every cloned type. +Below is an example that appends the string ``_Cloned`` to the name for every cloned type. .. code-block:: csharp @@ -251,7 +251,7 @@ It is important to note that the ``MemberCloner`` class itself does not inject a destinationModule.TopLevelTypes.Add(clonedType); -However, since 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 the destination module. In such a case, the code can be reduced to the following: +However, since 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 the destination module. In such a case, the code can be reduced to the following: .. code-block:: csharp diff --git a/docs/dotnet/dynamic-methods.rst b/docs/dotnet/dynamic-methods.rst index 9b43317e7..645be53a0 100644 --- a/docs/dotnet/dynamic-methods.rst +++ b/docs/dotnet/dynamic-methods.rst @@ -3,7 +3,7 @@ Dynamic Methods Dynamic methods are methods that are constructed and assembled at run time. They allow for dynamically generating managed code, without having to go through the process of compiling or generating new assemblies. This is used a lot in obfuscators that implement for example reference proxies or virtual machines. -AsmResolver has support for reading dynamic methods, and transforming them into ``MethodDefinition`` objects that can be processed further. All relevant classes are present in the following namespace: +AsmResolver has support for reading dynamic methods and transforming them into ``MethodDefinition`` objects that can be processed further. All relevant classes are present in the following namespace: .. code-block:: csharp diff --git a/docs/dotnet/importing.rst b/docs/dotnet/importing.rst index 7b1bb8dd2..abf3f420c 100644 --- a/docs/dotnet/importing.rst +++ b/docs/dotnet/importing.rst @@ -3,7 +3,7 @@ Reference Importing =================== -.NET modules use entries in the TypeRef or MemberRef tables to reference types or members from external assemblies. Importing references into the current module therefore form a key role when creating new- or modifying existing .NET modules. When a member is not imported into the current module, a ``MemberNotImportedException`` will be thrown when you are trying to create a PE image or write the module to the disk. +.NET modules use entries in the TypeRef or MemberRef tables to reference types or members from external assemblies. Importing references into the current module, therefore, form a key role when creating new- or modifying existing .NET modules. When a member is not imported into the current module, a ``MemberNotImportedException`` will be thrown when you are trying to create a PE image or write the module to the disk. AsmResolver provides the ``ReferenceImporter`` class that does most of the heavy lifting. Obtaining an instance of ``ReferenceImporter`` can be done in two ways. @@ -22,7 +22,7 @@ Or obtain the default instance that comes with every ``ModuleDefinition`` object var importer = module.DefaultImporter; -The example snippets that will follow in this articule assume that there is such a ``ReferenceImporter`` object instantiated using either of these two methods, and is stored in an ``importer`` variable. +The example snippets that will follow in this article assume that there is such a ``ReferenceImporter`` object instantiated using either of these two methods, and is stored in an ``importer`` variable. Importing existing members @@ -125,7 +125,7 @@ Instantiations of generic methods are also supported. Creating new references ----------------------- -Member references can also be created and imported without having direct access to its member definition or ``System.Reflection`` instance. It is possible to create new instances of ``TypeReference`` and ``MemberReference`` using the constructors, but the preferred way is to use the factory methods that allow for a more fluent syntax. Below an example on how to create a fully imported reference to ``void System.Console.WriteLine(string)``: +Member references can also be created and imported without having direct access to its member definition or ``System.Reflection`` instance. It is possible to create new instances of ``TypeReference`` and ``MemberReference`` using the constructors, but the preferred way is to use the factory methods that allow for a more fluent syntax. Below is an example of how to create a fully imported reference to ``void System.Console.WriteLine(string)``: .. code-block:: csharp diff --git a/docs/dotnet/index.rst b/docs/dotnet/index.rst index 25405425a..13d22fbde 100644 --- a/docs/dotnet/index.rst +++ b/docs/dotnet/index.rst @@ -1,7 +1,7 @@ Overview ======== -The .NET image layer is the third layer of abstraction of the portable executable (PE) file format. It provides a high-level abstraction of the .NET metadata stored in a PE image, that is similar to APIs like ``System.Reflection``. Its root objects are ``AssemblyDefinition`` and ``ModuleDefinition``, and from there it is possible to disect the .NET assembly in a hierarchical manner. +The .NET image layer is the third layer of abstraction of the portable executable (PE) file format. It provides a high-level abstraction of the .NET metadata stored in a PE image, that is similar to APIs like ``System.Reflection``. Its root objects are ``AssemblyDefinition`` and ``ModuleDefinition``, and from there it is possible to dissect the .NET assembly hierarchically. In short, this means the following: @@ -11,4 +11,4 @@ In short, this means the following: * Methods include method bodies, * ... and so on. -The third layer of abstraction is the highest level of abstraction for a .NET assembly that AsmResolver provides. All objects exposed by this layer are completely mutable, and can be serialized back to a ``IPEImage`` from the second layer, to a ``PEFile`` from the first layer, or to the disk. +The third layer of abstraction is the highest level of abstraction for a .NET assembly that AsmResolver provides. All objects exposed by this layer are completely mutable and can be serialized back to a ``IPEImage`` from the second layer, to a ``PEFile`` from the first layer, or to the disk. diff --git a/docs/dotnet/member-tree.rst b/docs/dotnet/member-tree.rst index 633d16808..ad6d4f6a8 100644 --- a/docs/dotnet/member-tree.rst +++ b/docs/dotnet/member-tree.rst @@ -22,7 +22,7 @@ Obtaining types in a module 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. -Below, an example program that iterates through all types recursively and prints them: +Below is an example program that iterates through all types recursively and prints them: .. code-block:: csharp diff --git a/docs/dotnet/token-allocation.rst b/docs/dotnet/token-allocation.rst index 6dc6bc9e9..602c38af9 100644 --- a/docs/dotnet/token-allocation.rst +++ b/docs/dotnet/token-allocation.rst @@ -6,7 +6,7 @@ A lot of models in a .NET module are assigned a unique metadata token. This toke Custom Token Allocation ----------------------- -Some usecase of AsmResolver will depend on the knowledge of tokens of newly created members prior to serializing the module. Therefore, AsmResolver provides the ``TokenAllocator`` class, which allows for assigning new tokens to members pre-emptively. If a module is then written to the disk with the ``MetadataFlags.PreserveTableIndices`` flags set (see Advanced PE Image Building for more information on how that is done), this token will be preserved in the final image. +Some use-cases of AsmResolver will depend on the knowledge of tokens of newly created members before serializing the module. Therefore, AsmResolver provides the ``TokenAllocator`` class, which allows for assigning new tokens to members preemptively. If a module is then written to the disk with the ``MetadataFlags.PreserveTableIndices`` flags set (see Advanced PE Image Building for more information on how that is done), this token will be preserved in the final image. The token allocator for a particular module can be accessed through the ``ModuleDefinition.TokenAllocator`` property: diff --git a/docs/dotnet/type-signatures.rst b/docs/dotnet/type-signatures.rst index df16d8e31..34d5fcff1 100644 --- a/docs/dotnet/type-signatures.rst +++ b/docs/dotnet/type-signatures.rst @@ -27,6 +27,8 @@ Basic leaf type signatures: +----------------------------------+----------------------------------------------------------------------+ | ``FunctionPointerTypeSignature`` | ``method void *(int32, int64)`` | +----------------------------------+----------------------------------------------------------------------+ +| ``GenericParameterSignature`` | ``!0``, ``!!0`` | ++----------------------------------+----------------------------------------------------------------------+ | ``SentinelTypeSignature`` | (Used as a delimeter for vararg method signatures) | +----------------------------------+----------------------------------------------------------------------+ @@ -43,8 +45,6 @@ Decorator type signatures: +----------------------------------+----------------------------------------------------------------------+ | ``PointerTypeSignature`` | ``System.Int32*`` | +----------------------------------+----------------------------------------------------------------------+ -| ``GenericParameterSignature`` | ``!0``, ``!!0`` | -+----------------------------------+----------------------------------------------------------------------+ | ``CustomModifierTypeSignature`` | ``System.Int32 modreq (System.Runtime.CompilerServices.IsVolatile)`` | +----------------------------------+----------------------------------------------------------------------+ | ``BoxedTypeSignature`` | (Boxes a value type signature) | @@ -77,6 +77,7 @@ Corlib type signatures can also be looked up by their element type, by their ful If an invalid element type, name or type descriptor is passed on, these methods return ``null``. + TypeDefOrRefSignature --------------------- @@ -88,6 +89,13 @@ The ``TypeDefOrRefSignature`` class is used to reference types in either the ``T TypeSignature streamTypeSig = new TypeDefOrRefSignature(streamTypeRef); +Alternatively, ``CreateTypeReference`` can be used on any ``IResolutionScope``: + +.. code-block:: csharp + + var streamTypeSig = corlibScope.CreateTypeReference("System.IO", "Stream"); + + .. warning:: While it is technically possible to reference a basic type such as ``System.Int32`` as a ``TypeDefOrRefSignature``, it renders the .NET module invalid by most implementations of the CLR. Always use the ``CorLibTypeSignature`` to reference basic types within your blob signatures. @@ -109,6 +117,17 @@ The ``GenericInstanceTypeSignature`` class is used to instantiate generic types // listOfString now contains a reference to List. +Alternatively, a generic instance can also be generated via the ``MakeGenericType`` fluent syntax method: + +.. code-block:: csharp + + var listOfString = corlibScope + .CreateTypeReference("System.Collections.Generic", "List`1") + .MakeGenericInstanceType(module.CorLibTypeFactory.String); + + // listOfString now contains a reference to List. + + FunctionPointerTypeSignature ---------------------------- @@ -127,6 +146,21 @@ Function pointer signatures are strongly-typed pointer types used to describe ad // type now contains a reference to `method void *(int32, int32)`. +Alternatively, a function pointer signature can also be generated via the ``MakeFunctionPointerType`` fluent syntax method: + +.. code-block:: csharp + + var factory = module.CorLibTypeFactory; + var type = MethodSignature.CreateStatic( + factory.Void, + factory.Int32, + factory.Int32) + .MakeFunctionPointerType(); + + // type now contains a reference to `method void *(int32, int32)`. + + + Shortcuts --------- @@ -140,6 +174,7 @@ To quickly transform any ``ITypeDescriptor`` into a ``TypeSignature``, it is pos Likewise, a ``TypeSignature`` can also be converted back to a ``ITypeDefOrRef``, which can be referenced using a metadata token, using the ``TypeSignature.ToTypeDefOrRef()`` method. + Decorating type signatures -------------------------- diff --git a/docs/pefile/basics.rst b/docs/pefile/basics.rst index 2514cf826..d893d7a0f 100644 --- a/docs/pefile/basics.rst +++ b/docs/pefile/basics.rst @@ -28,7 +28,7 @@ Opening a file can be done through one of the `FromXXX` methods: var peFile = PEFile.FromReader(reader); -By default, AsmResolver assumes the PE file is in its unmapped form. This is usually the case when files are read directly from the file system. For memory mapped PE files, use the overload of the ``FromReader`` method, which allows for specifying the memory layout of the input. +By default, AsmResolver assumes the PE file is in its unmapped form. This is usually the case when files are read directly from the file system. For memory-mapped PE files, use the overload of the ``FromReader`` method, which allows for specifying the memory layout of the input. .. code-block:: csharp @@ -36,7 +36,7 @@ By default, AsmResolver assumes the PE file is in its unmapped form. This is usu var peFile = PEFile.FromReader(reader, PEMappingMode.Mapped); -If you want to read large files (+100MB), consider using memory mapped I/O instead: +If you want to read large files (+100MB), consider using memory-mapped I/O instead: .. code-block:: csharp diff --git a/docs/peimage/debug.rst b/docs/peimage/debug.rst index e3cce0738..30f092147 100644 --- a/docs/peimage/debug.rst +++ b/docs/peimage/debug.rst @@ -13,7 +13,7 @@ The relevant classes for this article are stored in the following namespace: The Debug Data Entries ---------------------- -The ``IPEImage`` exposes all debug information through the ``DebugData`` property. This is a list of ``DebugDataEntry``, providing access to the type of the debug data, as well as the version and raw contents of the data that is stored. +The ``IPEImage`` exposes all debug information through the ``DebugData`` property. This is a list of ``DebugDataEntry``, providing access to the type of debug data, as well as the version and raw contents of the data that is stored. .. code-block:: csharp @@ -24,15 +24,15 @@ The ``IPEImage`` exposes all debug information through the ``DebugData`` propert Console.WriteLine("Data start: {0:X8}", entry.Contents.Rva); } -Depending on the type of the debug data entry, the ``Contents`` property will be modelled using different implementations of ``IDebugDataSegment``. +Depending on the type of the debug data entry, the ``Contents`` property will be modeled using different implementations of ``IDebugDataSegment``. .. note:: - If a PE contains debug data using an unsuported or unrecognized format, then the contents will be modelled with a ``CustomDebugDataSegment`` instance instead, which exposes the raw contents as an ``ISegment``. + If a PE contains debug data using an unsupported or unrecognized format, then the contents will be modeled with a ``CustomDebugDataSegment`` instance instead, which exposes the raw contents as an ``ISegment``. .. note:: - Currently, AsmResolver only has rich-support for ``CodeView`` debug data. + Currently, AsmResolver only has rich support for ``CodeView`` debug data. CodeView Data diff --git a/docs/peimage/dotnet.rst b/docs/peimage/dotnet.rst index a4f41da3b..26cf9626b 100644 --- a/docs/peimage/dotnet.rst +++ b/docs/peimage/dotnet.rst @@ -68,7 +68,7 @@ AsmResolver supports parsing streams using the names in the table below. Any str | ``#US`` | ``UserStringsStream`` | +---------------------------+------------------------+ -Some streams support reading the raw contents using a ``BinaryStreamReader``. Effectively, every stream that was read from the disk is readable in this way. Below an example of a program that dumps for each readable stream the contents to a file on the disk: +Some streams support reading the raw contents using a ``BinaryStreamReader``. Effectively, every stream that was read from the disk is readable in this way. Below is an example of a program that dumps for each readable stream the contents to a file on the disk: .. code-block:: csharp @@ -101,7 +101,7 @@ The ``Streams`` property is mutable. You can add new streams, or remove existing Blob, Strings, US and GUID streams ---------------------------------- -The blob, strings, user-strings and GUID streams are all very similar in the sense that they all provide a storage for data referenced by the tables stream. Each of these streams have a very similar API in AsmResolver. +The blob, strings, user-strings and GUID streams are all very similar in the sense that they all provide a storage for data referenced by the tables stream. Each of these streams has a very similar API in AsmResolver. +------------------------+----------------------+ | Class | Method | @@ -346,7 +346,7 @@ Creating new segment references not present in the current PE image yet can be d TypeReference Hash (TRH) ------------------------ -Similar to the :ref:`pe-import-hash`, the TypeReference Hash (TRH) can be used to help identifying malware family 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``). +Similar to the :ref:`pe-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 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 for this that is based on `the reference implementation provided by GData `_. The hash can be obtained using the ``GetTypeReferenceHash`` extension method on ``IPEImage`` or on ``IMetadata``: diff --git a/docs/peimage/pe-building.rst b/docs/peimage/pe-building.rst index 13cc88d61..94098f680 100644 --- a/docs/peimage/pe-building.rst +++ b/docs/peimage/pe-building.rst @@ -3,7 +3,7 @@ PE File Building ================ -An ``IPEImage`` is a higher level abstraction of an actual PE file, and hides many of the actual implementation details such as raw section layout and contents of data directories. While this makes reading and interpretation of a PE very easy, it makes writing a more involved process. +An ``IPEImage`` is a higher-level abstraction of an actual PE file, and hides many of the actual implementation details such as raw section layout and contents of data directories. While this makes reading and interpreting a PE very easy, it makes writing a more involved process. Unfortunately, the PE file format is not well-structured. Compilers and linkers have a lot of freedom when it comes to the actual design and layout of the final PE file. For example, one compiler might place the Import Address Table (IAT) in a separate section (such as the MSVC), while others will put it next to the code in the text section (such as the C# compiler). This degree of freedom makes it virtually impossible to make a completely generic PE file builder, and as such AsmResolver does not come with one out of the box. @@ -47,9 +47,9 @@ In a lot of cases, however, sections do not consist of a single data structure, Using complex PE models ----------------------- -The PE file format defines many complex data structures. While some these models are represented in AsmResolver using classes that derive from ``ISegment``, a lot of the classes that represent these data structures do not implement this interface, and as such cannot be used as a value for the ``Contents`` property of a ``PESection`` directly. This is due to the fact that most of these models are not required to be one single entity or chunk of continuous memory within the PE file. Instead, they are often scattered around the PE file by a compiler. For example, the Import Directory has a second component the Import Address Table which is often put in a completely different PE section (usually ``.text`` or ``.data``) than the Import Directory itself (in ``.idata`` or ``.rdata``). To make reading and interpreting these data structures more convenient for the end-user, the ``AsmResolver.PE`` package adopted some design choices to abstract these details away to make things more natural to work with. The downside of this is that writing these structures requires you to specify where AsmResolver should place these models in the final PE file. +The PE file format defines many complex data structures. While some of these models are represented in AsmResolver using classes that derive from ``ISegment``, a lot of the classes that represent these data structures do not implement this interface, and as such cannot be used as a value for the ``Contents`` property of a ``PESection`` directly. This is due to the fact that most of these models are not required to be one single entity or chunk of continuous memory within the PE file. Instead, they are often scattered around the PE file by a compiler. For example, the Import Directory has a second component the Import Address Table which is often put in a completely different PE section (usually ``.text`` or ``.data``) than the Import Directory itself (in ``.idata`` or ``.rdata``). To make reading and interpreting these data structures more convenient for the end-user, the ``AsmResolver.PE`` package adopted some design choices to abstract these details away to make things more natural to work with. The downside of this is that writing these structures requires you to specify where AsmResolver should place these models in the final PE file. -In ``AsmResolver.PE``, most models for which is the case reside in their own namespace, and have their own set of classes dedicated to constructing new segments defined in a ``Builder`` sub namespace. For example, the Win32 resources directory models reside in ``AsmResolver.PE.Win32Resources``, but the actual builder classes are put in a sub namespace called ``AsmResolver.PE.Win32Resources.Builder``. +In ``AsmResolver.PE``, most models for which is the case reside in their own namespace, and have their own set of classes dedicated to constructing new segments defined in a ``Builder`` sub-namespace. For example, the Win32 resources directory models reside in ``AsmResolver.PE.Win32Resources``, but the actual builder classes are put in a sub namespace called ``AsmResolver.PE.Win32Resources.Builder``. .. code-block:: csharp @@ -94,7 +94,7 @@ A more complicated structure such as the Imports Directory can be build like the Using PEFileBuilders -------------------- -As a lot of the PE file building process will be similar for many types of PE file layouts (such as the construction of the file and optional headers), AsmResolver comes with a base class called ``PEFileBuilderBase`` that abstracts many of these similarities away. Rather than defining and building up everything yourself, the ``PEFileBuilderBase`` allows you to override a couple of methods: +As a lot of the PE file-building process will be similar for many types of PE file layouts (such as the construction of the file and optional headers), AsmResolver comes with a base class called ``PEFileBuilderBase`` that abstracts many of these similarities away. Rather than defining and building up everything yourself, the ``PEFileBuilderBase`` allows you to override a couple of methods: .. code-block:: csharp diff --git a/docs/peimage/tls.rst b/docs/peimage/tls.rst index ad2f97ac0..938d4dc74 100644 --- a/docs/peimage/tls.rst +++ b/docs/peimage/tls.rst @@ -55,7 +55,7 @@ Adding a new TLS directory to an image can be done using the parameterless const var tlsDirectory = new TlsDirectory(); image.TlsDirectory = tlsDirectory; -A TLS directory references all its sub segments using virtual addresses (VA) rather than relative addresses (RVA). This means that constructing a relocatable PE image with a TLS directory requires base relocation entries to be registered that let the Windows PE loader rebase all virtual addresses used in the directory when necessary. To quickly register all the required base relocations, you can call the ``GetRequiredBaseRelocations`` method and add all returned entries to the base relocation directory of the PE image: +A TLS directory references all its sub-segments using virtual addresses (VA) rather than relative addresses (RVA). This means that constructing a relocatable PE image with a TLS directory requires base relocation entries to be registered that let the Windows PE loader rebase all virtual addresses used in the directory when necessary. To quickly register all the required base relocations, you can call the ``GetRequiredBaseRelocations`` method and add all returned entries to the base relocation directory of the PE image: .. code-block:: csharp From d1d53e71380c12d69c33829f3dd5fb775ee458c5 Mon Sep 17 00:00:00 2001 From: Washi Date: Wed, 11 Jan 2023 22:06:23 +0100 Subject: [PATCH 17/35] Remove redundant code in security attribute. --- .../Signatures/Security/SecurityAttribute.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/AsmResolver.DotNet/Signatures/Security/SecurityAttribute.cs b/src/AsmResolver.DotNet/Signatures/Security/SecurityAttribute.cs index 4631a5925..8ec14c011 100644 --- a/src/AsmResolver.DotNet/Signatures/Security/SecurityAttribute.cs +++ b/src/AsmResolver.DotNet/Signatures/Security/SecurityAttribute.cs @@ -38,12 +38,8 @@ public static SecurityAttribute FromReader(in BlobReaderContext context, ref Bin return result; } - var genericContext = GenericContext.FromType(type); for (int i = 0; i < namedArgumentCount; i++) - { - var argument = CustomAttributeNamedArgument.FromReader(context, ref reader); - result.NamedArguments.Add(argument); - } + result.NamedArguments.Add(CustomAttributeNamedArgument.FromReader(context, ref reader)); return result; } From 5fb1d6790953fa0f49d0d594fb2ba7f45ecf51ef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Jan 2023 14:09:26 +0000 Subject: [PATCH 18/35] Bump BenchmarkDotNet from 0.13.3 to 0.13.4 Bumps [BenchmarkDotNet](https://github.com/dotnet/BenchmarkDotNet) from 0.13.3 to 0.13.4. - [Release notes](https://github.com/dotnet/BenchmarkDotNet/releases) - [Commits](https://github.com/dotnet/BenchmarkDotNet/compare/v0.13.3...v0.13.4) --- updated-dependencies: - dependency-name: BenchmarkDotNet dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- test/AsmResolver.Benchmarks/AsmResolver.Benchmarks.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/AsmResolver.Benchmarks/AsmResolver.Benchmarks.csproj b/test/AsmResolver.Benchmarks/AsmResolver.Benchmarks.csproj index f05cc3ab8..c83d52de8 100644 --- a/test/AsmResolver.Benchmarks/AsmResolver.Benchmarks.csproj +++ b/test/AsmResolver.Benchmarks/AsmResolver.Benchmarks.csproj @@ -7,7 +7,7 @@ - + From 0fd95ed3292aa7255f34a0cb6e9bafa6adb3e852 Mon Sep 17 00:00:00 2001 From: Washi Date: Tue, 17 Jan 2023 13:11:17 +0100 Subject: [PATCH 19/35] Initial patched segment implementation. --- src/AsmResolver/IReadableSegment.cs | 1 - src/AsmResolver/ISegment.cs | 27 ++++++ src/AsmResolver/Patching/BytesPatch.cs | 46 ++++++++++ src/AsmResolver/Patching/IPatch.cs | 15 ++++ src/AsmResolver/Patching/PatchContext.cs | 47 ++++++++++ src/AsmResolver/Patching/PatchedSegment.cs | 86 +++++++++++++++++++ .../Patching/PatchedSegmentTest.cs | 31 +++++++ 7 files changed, 252 insertions(+), 1 deletion(-) create mode 100644 src/AsmResolver/Patching/BytesPatch.cs create mode 100644 src/AsmResolver/Patching/IPatch.cs create mode 100644 src/AsmResolver/Patching/PatchContext.cs create mode 100644 src/AsmResolver/Patching/PatchedSegment.cs create mode 100644 test/AsmResolver.Tests/Patching/PatchedSegmentTest.cs diff --git a/src/AsmResolver/IReadableSegment.cs b/src/AsmResolver/IReadableSegment.cs index 2d3aea65a..f07323854 100644 --- a/src/AsmResolver/IReadableSegment.cs +++ b/src/AsmResolver/IReadableSegment.cs @@ -56,6 +56,5 @@ public static byte[] ToArray(this IReadableSegment segment) { return segment.CreateReader().ReadToEnd(); } - } } diff --git a/src/AsmResolver/ISegment.cs b/src/AsmResolver/ISegment.cs index dce4aec3c..68d604453 100644 --- a/src/AsmResolver/ISegment.cs +++ b/src/AsmResolver/ISegment.cs @@ -1,6 +1,8 @@ using System; +using System.IO; using System.Linq; using System.Text; +using AsmResolver.IO; namespace AsmResolver { @@ -147,5 +149,30 @@ public static string CreateEscapedString(this string literal) public static ISegmentReference ToReference(this ISegment segment, int additive) => additive == 0 ? new SegmentReference(segment) : new RelativeReference(segment, additive); + + /// + /// Serializes the segment by calling and writes the result into a byte array. + /// + /// The segment to serialize to + /// The resulting byte array. + public static byte[] WriteIntoArray(this ISegment segment) + { + using var stream = new MemoryStream(); + segment.Write(new BinaryStreamWriter(stream)); + return stream.ToArray(); + } + + /// + /// Serializes the segment by calling and writes the result into a byte array. + /// + /// The segment to serialize to + /// The memory stream writer pool to rent temporary writers from. + /// The resulting byte array. + public static byte[] WriteIntoArray(this ISegment segment, MemoryStreamWriterPool pool) + { + using var rentedWriter = pool.Rent(); + segment.Write(rentedWriter.Writer); + return rentedWriter.GetData(); + } } } diff --git a/src/AsmResolver/Patching/BytesPatch.cs b/src/AsmResolver/Patching/BytesPatch.cs new file mode 100644 index 000000000..9af11e0a8 --- /dev/null +++ b/src/AsmResolver/Patching/BytesPatch.cs @@ -0,0 +1,46 @@ +using System.Diagnostics; +using AsmResolver.IO; + +namespace AsmResolver.Patching +{ + /// + /// Patches an instance of with a sequence of bytes. + /// + [DebuggerDisplay("Patch {NewData.Length} bytes at offset {RelativeOffset}")] + public sealed class BytesPatch : IPatch + { + /// + /// Creates a new bytes patch. + /// + /// The offset to start writing at. + /// The new data. + public BytesPatch(uint relativeOffset, byte[] newData) + { + RelativeOffset = relativeOffset; + NewData = newData; + } + + /// + /// Gets the offset relative to the start of the segment to start writing at. + /// + public uint RelativeOffset + { + get; + } + + /// + /// Gets the data to write. + /// + public byte[] NewData + { + get; + } + + /// + public void Apply(in PatchContext context) + { + context.Writer.Offset = context.Segment.Offset + RelativeOffset; + context.Writer.WriteBytes(NewData); + } + } +} diff --git a/src/AsmResolver/Patching/IPatch.cs b/src/AsmResolver/Patching/IPatch.cs new file mode 100644 index 000000000..ec917fe65 --- /dev/null +++ b/src/AsmResolver/Patching/IPatch.cs @@ -0,0 +1,15 @@ +namespace AsmResolver.Patching +{ + /// + /// Provides a mechanism for patching an instance of after it was serialized into its + /// binary representation. + /// + public interface IPatch + { + /// + /// Applies the patch. + /// + /// The context in which to + void Apply(in PatchContext context); + } +} diff --git a/src/AsmResolver/Patching/PatchContext.cs b/src/AsmResolver/Patching/PatchContext.cs new file mode 100644 index 000000000..3d8949e71 --- /dev/null +++ b/src/AsmResolver/Patching/PatchContext.cs @@ -0,0 +1,47 @@ +using AsmResolver.IO; + +namespace AsmResolver.Patching +{ + /// + /// Provides members describing the context in which a patch may be situated in. + /// + public readonly struct PatchContext + { + /// + /// Creates a new instance of the structure. + /// + /// The segment to be patched. + /// The image base to assume while patching. + /// The object responsible for writing the patches. + public PatchContext(ISegment segment, ulong imageBase, IBinaryStreamWriter writer) + { + Segment = segment; + ImageBase = imageBase; + Writer = writer; + } + + /// + /// Gets the segment to be patched. + /// + public ISegment Segment + { + get; + } + + /// + /// Gets the image base that is assumed while patching. + /// + public ulong ImageBase + { + get; + } + + /// + /// Gets the object responsible for writing the patches. + /// + public IBinaryStreamWriter Writer + { + get; + } + } +} diff --git a/src/AsmResolver/Patching/PatchedSegment.cs b/src/AsmResolver/Patching/PatchedSegment.cs new file mode 100644 index 000000000..378542c34 --- /dev/null +++ b/src/AsmResolver/Patching/PatchedSegment.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using AsmResolver.IO; + +namespace AsmResolver.Patching +{ + /// + /// Provides a wrapper around an instance of a that patches its binary representation + /// while it is being serialized to an output stream. + /// + [DebuggerDisplay("Patched {Contents} (Count = {Patches.Count})")] + public class PatchedSegment : IReadableSegment + { + private ulong _imageBase; + + /// + /// Wraps a segment into a new instance of the class. + /// + /// The segment to patch. + public PatchedSegment(ISegment contents) + { + Contents = contents; + } + + /// + /// Gets the original segment that is being patched. + /// + public ISegment Contents + { + get; + } + + /// + /// Gets a list of patches to apply to the segment. + /// + public IList Patches + { + get; + } = new List(); + + /// + public ulong Offset => Contents.Offset; + + /// + public uint Rva => Contents.Rva; + + /// + public bool CanUpdateOffsets => Contents.CanUpdateOffsets; + + /// + public uint GetPhysicalSize() => Contents.GetPhysicalSize(); + + /// + public uint GetVirtualSize() => Contents.GetVirtualSize(); + + /// + public void UpdateOffsets(in RelocationParameters parameters) + { + Contents.UpdateOffsets(in parameters); + _imageBase = parameters.ImageBase; + } + + /// + public BinaryStreamReader CreateReader(ulong fileOffset, uint size) => Contents is IReadableSegment segment + ? segment.CreateReader(fileOffset, size) + : throw new InvalidOperationException("Segment is not readable."); + + /// + public void Write(IBinaryStreamWriter writer) + { + Contents.Write(writer); + ApplyPatches(writer); + } + + private void ApplyPatches(IBinaryStreamWriter writer) + { + ulong offset = writer.Offset; + + for (int i = 0; i < Patches.Count; i++) + Patches[i].Apply(new PatchContext(Contents, _imageBase, writer)); + + writer.Offset = offset; + } + } +} diff --git a/test/AsmResolver.Tests/Patching/PatchedSegmentTest.cs b/test/AsmResolver.Tests/Patching/PatchedSegmentTest.cs new file mode 100644 index 000000000..ffdc2bafd --- /dev/null +++ b/test/AsmResolver.Tests/Patching/PatchedSegmentTest.cs @@ -0,0 +1,31 @@ +using System; +using System.Linq; +using AsmResolver.Patching; +using Xunit; + +namespace AsmResolver.Tests.Patching +{ + public class PatchedSegmentTest + { + private readonly DataSegment _input = new(Enumerable + .Range(0, 1000) + .Select(x => (byte) (x & 0xFF)) + .ToArray()); + + [Fact] + public void SimpleBytesPatch() + { + var patched = new PatchedSegment(_input); + + uint relativeOffset = 10; + byte[] newData = {0xFF, 0xFE, 0xFD, 0xFC}; + patched.Patches.Add(new BytesPatch(relativeOffset, newData)); + + byte[] expected = _input.ToArray(); + Buffer.BlockCopy(newData, 0, expected, (int) relativeOffset, newData.Length); + + byte[] result = patched.WriteIntoArray(); + Assert.Equal(expected, result); + } + } +} From caa3d5557404552499c446ac0b5af4f021afd201 Mon Sep 17 00:00:00 2001 From: Washi Date: Tue, 17 Jan 2023 13:21:31 +0100 Subject: [PATCH 20/35] Add AsPatchedSegment and bytepatch fluent syntax helper method. --- src/AsmResolver/ISegment.cs | 34 +++++++++++++++++++ src/AsmResolver/Patching/PatchedSegment.cs | 12 +++++++ .../Patching/PatchedSegmentTest.cs | 25 ++++++++++++++ 3 files changed, 71 insertions(+) diff --git a/src/AsmResolver/ISegment.cs b/src/AsmResolver/ISegment.cs index 68d604453..dadb9dd84 100644 --- a/src/AsmResolver/ISegment.cs +++ b/src/AsmResolver/ISegment.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text; using AsmResolver.IO; +using AsmResolver.Patching; namespace AsmResolver { @@ -174,5 +175,38 @@ public static byte[] WriteIntoArray(this ISegment segment, MemoryStreamWriterPoo segment.Write(rentedWriter.Writer); return rentedWriter.GetData(); } + + /// + /// Wraps the provided segment into a , making it eligible for applying + /// post-serialization patches. + /// + /// The segment to wrap. + /// + /// The wrapped segment, or if it is already an instance of + /// . + /// + public static PatchedSegment AsPatchedSegment(this ISegment segment) => segment.AsPatchedSegment(false); + + /// + /// Wraps the provided segment into a , making it eligible for applying + /// post-serialization patches. + /// + /// The segment to wrap. + /// + /// Indicates whether the segment should always be wrapped into a new instance of , + /// regardless of whether is already an instance of + /// or not. + /// + /// + /// The wrapped segment, or if it is already an instance of + /// and is set to true. + /// + public static PatchedSegment AsPatchedSegment(this ISegment segment, bool alwaysCreateNew) + { + if (alwaysCreateNew) + return new PatchedSegment(segment); + + return segment as PatchedSegment ?? new PatchedSegment(segment); + } } } diff --git a/src/AsmResolver/Patching/PatchedSegment.cs b/src/AsmResolver/Patching/PatchedSegment.cs index 378542c34..82ac9a80e 100644 --- a/src/AsmResolver/Patching/PatchedSegment.cs +++ b/src/AsmResolver/Patching/PatchedSegment.cs @@ -82,5 +82,17 @@ private void ApplyPatches(IBinaryStreamWriter writer) writer.Offset = offset; } + + /// + /// Adds a bytes patch to the list of patches to apply. + /// + /// The offset to start patching at, relative to the start of the segment. + /// The new data to write. + /// The current instance. + public PatchedSegment Patch(uint relativeOffset, byte[] newData) + { + Patches.Add(new BytesPatch(relativeOffset, newData)); + return this; + } } } diff --git a/test/AsmResolver.Tests/Patching/PatchedSegmentTest.cs b/test/AsmResolver.Tests/Patching/PatchedSegmentTest.cs index ffdc2bafd..9f68452ec 100644 --- a/test/AsmResolver.Tests/Patching/PatchedSegmentTest.cs +++ b/test/AsmResolver.Tests/Patching/PatchedSegmentTest.cs @@ -27,5 +27,30 @@ public void SimpleBytesPatch() byte[] result = patched.WriteIntoArray(); Assert.Equal(expected, result); } + + [Fact] + public void DoublePatchedSegmentShouldReturnSameInstance() + { + var x = _input.AsPatchedSegment(); + var y = x.AsPatchedSegment(); + Assert.Same(x, y); + } + + [Fact] + public void SimpleBytesPatchFluent() + { + uint relativeOffset = 10; + byte[] newData = {0xFF, 0xFE, 0xFD, 0xFC}; + + var patched = _input + .AsPatchedSegment() + .Patch(10, newData); + + byte[] expected = _input.ToArray(); + Buffer.BlockCopy(newData, 0, expected, (int) relativeOffset, newData.Length); + + byte[] result = patched.WriteIntoArray(); + Assert.Equal(expected, result); + } } } From 904e75987781723d65aaf49098dd5f98a5ffffd8 Mon Sep 17 00:00:00 2001 From: Washi Date: Tue, 17 Jan 2023 15:01:27 +0100 Subject: [PATCH 21/35] Obsolete CodeSegment, add AddressFixupPatch. --- .../Code/Native/NativeMethodBodySerializer.cs | 12 +-- src/AsmResolver.PE/Code/AddressFixup.cs | 9 +- .../Code/AddressFixupExtensions.cs | 37 +++++++ src/AsmResolver.PE/Code/AddressFixupPatch.cs | 56 +++++++++++ src/AsmResolver.PE/Code/AddressFixupType.cs | 2 +- src/AsmResolver.PE/Code/CodeSegment.cs | 27 +---- src/AsmResolver.PE/Platforms/Amd64Platform.cs | 13 +-- src/AsmResolver.PE/Platforms/I386Platform.cs | 11 ++- src/AsmResolver/ISegment.cs | 4 +- .../Code/Native/NativeMethodBodyTest.cs | 7 +- .../Code/AddressFixupTest.cs | 98 +++++++++++++++++++ .../DotNet/Builder/MixedModeAssemblyTest.cs | 77 +++++++-------- 12 files changed, 260 insertions(+), 93 deletions(-) create mode 100644 src/AsmResolver.PE/Code/AddressFixupExtensions.cs create mode 100644 src/AsmResolver.PE/Code/AddressFixupPatch.cs create mode 100644 test/AsmResolver.PE.Tests/Code/AddressFixupTest.cs diff --git a/src/AsmResolver.DotNet/Code/Native/NativeMethodBodySerializer.cs b/src/AsmResolver.DotNet/Code/Native/NativeMethodBodySerializer.cs index b1a8d9f37..ca0b90984 100644 --- a/src/AsmResolver.DotNet/Code/Native/NativeMethodBodySerializer.cs +++ b/src/AsmResolver.DotNet/Code/Native/NativeMethodBodySerializer.cs @@ -24,17 +24,17 @@ public ISegmentReference SerializeMethodBody(MethodBodySerializationContext cont var provider = context.SymbolsProvider; // Create new raw code segment containing the native code. - var segment = new CodeSegment(nativeMethodBody.Code); + var segment = new DataSegment(nativeMethodBody.Code).AsPatchedSegment(); - // Process fixups. + // Process address fixups. for (int i = 0; i < nativeMethodBody.AddressFixups.Count; i++) { // Import symbol. var fixup = nativeMethodBody.AddressFixups[i]; var symbol = FinalizeSymbol(segment, provider, fixup.Symbol); - // Create new fixup with imported symbol. - segment.AddressFixups.Add(new AddressFixup(fixup.Offset, fixup.Type, symbol)); + // Fixup address. + segment.Patch(fixup.Offset, fixup.Type, symbol); // Add base relocation when necessary. AddBaseRelocations(segment, provider, fixup); @@ -49,7 +49,7 @@ public ISegmentReference SerializeMethodBody(MethodBodySerializationContext cont /// The code segment that is being constructed. /// The object responsible for providing symbols referenced by the native method body. /// The fixup to build base relocations for. - protected virtual void AddBaseRelocations(CodeSegment segment, INativeSymbolsProvider provider, AddressFixup fixup) + protected virtual void AddBaseRelocations(ISegment segment, INativeSymbolsProvider provider, AddressFixup fixup) { switch (fixup.Type) { @@ -74,7 +74,7 @@ protected virtual void AddBaseRelocations(CodeSegment segment, INativeSymbolsPro /// The object responsible for providing symbols referenced by the native method body. /// The symbol to reference. /// The symbol. - protected virtual ISymbol FinalizeSymbol(CodeSegment result, INativeSymbolsProvider provider, ISymbol symbol) + protected virtual ISymbol FinalizeSymbol(ISegment result, INativeSymbolsProvider provider, ISymbol symbol) { if (symbol is NativeLocalSymbol local) return new Symbol(result.ToReference((int) local.Offset)); diff --git a/src/AsmResolver.PE/Code/AddressFixup.cs b/src/AsmResolver.PE/Code/AddressFixup.cs index 32825c3b3..7ede26c26 100644 --- a/src/AsmResolver.PE/Code/AddressFixup.cs +++ b/src/AsmResolver.PE/Code/AddressFixup.cs @@ -1,11 +1,12 @@ using System; +using System.Diagnostics; namespace AsmResolver.PE.Code { /// - /// Provides information about a code or data segment referenced within a code segment for which - /// the final RVA is yet to be determined. + /// Provides information about a symbol referenced within a segment for which the final RVA is yet to be determined. /// + [DebuggerDisplay("Patch {Offset} with &{Symbol} as {Type}")] public readonly struct AddressFixup { /// @@ -20,7 +21,7 @@ public AddressFixup(uint offset, AddressFixupType type, ISymbol referencedObject Symbol = referencedObject ?? throw new ArgumentNullException(nameof(referencedObject)); Type = type; } - + /// /// Gets the offset relative to the start of the code segment pointing to the reference. /// @@ -48,4 +49,4 @@ public ISymbol Symbol /// public override string ToString() => $"+{Offset:X8} <{Symbol}> ({Type})"; } -} \ No newline at end of file +} diff --git a/src/AsmResolver.PE/Code/AddressFixupExtensions.cs b/src/AsmResolver.PE/Code/AddressFixupExtensions.cs new file mode 100644 index 000000000..38b93e076 --- /dev/null +++ b/src/AsmResolver.PE/Code/AddressFixupExtensions.cs @@ -0,0 +1,37 @@ +using AsmResolver.Patching; + +namespace AsmResolver.PE.Code +{ + /// + /// Provides extensions to that adds patch overloads to quickly construct instances of + /// . + /// + public static class AddressFixupExtensions + { + /// + /// Adds an address fixup to the list of patches to apply. + /// + /// The segment to add the patch to. + /// The offset to start writing the address at, relative to the start of the segment. + /// The type of address to write. + /// The reference to write the RVA for. + /// The patched segment. + public static PatchedSegment Patch(this PatchedSegment segment, uint relativeOffset, AddressFixupType type, + ISymbol referencedObject) + { + return segment.Patch(new AddressFixup(relativeOffset, type, referencedObject)); + } + + /// + /// Adds an address fixup to the list of patches to apply. + /// + /// The segment to add the patch to. + /// The fixup to apply. + /// The patched segment. + public static PatchedSegment Patch(this PatchedSegment segment, in AddressFixup fixup) + { + segment.Patches.Add(new AddressFixupPatch(fixup)); + return segment; + } + } +} diff --git a/src/AsmResolver.PE/Code/AddressFixupPatch.cs b/src/AsmResolver.PE/Code/AddressFixupPatch.cs new file mode 100644 index 000000000..a87631b18 --- /dev/null +++ b/src/AsmResolver.PE/Code/AddressFixupPatch.cs @@ -0,0 +1,56 @@ +using System; +using System.Diagnostics; +using AsmResolver.Patching; + +namespace AsmResolver.PE.Code +{ + /// + /// Implements a patch that patches a segment with an address to a symbol. + /// + [DebuggerDisplay("{Fixup}")] + public class AddressFixupPatch : IPatch + { + /// + /// Creates a new instance of the class. + /// + /// The fixup to apply. + public AddressFixupPatch(AddressFixup fixup) + { + Fixup = fixup; + } + + /// + /// Gets the fixup to apply. + /// + public AddressFixup Fixup + { + get; + } + + /// + public void Apply(in PatchContext context) + { + context.Writer.Offset = context.Segment.Offset + Fixup.Offset; + uint writerRva = context.Segment.Rva + Fixup.Offset; + uint targetRva = Fixup.Symbol.GetReference()?.Rva ?? 0; + + switch (Fixup.Type) + { + case AddressFixupType.Absolute32BitAddress: + context.Writer.WriteUInt32((uint) (context.ImageBase + targetRva)); + break; + + case AddressFixupType.Relative32BitAddress: + context.Writer.WriteInt32((int) (targetRva - (writerRva + 4))); + break; + + case AddressFixupType.Absolute64BitAddress: + context.Writer.WriteUInt64(context.ImageBase + targetRva); + break; + + default: + throw new ArgumentOutOfRangeException(); + } + } + } +} diff --git a/src/AsmResolver.PE/Code/AddressFixupType.cs b/src/AsmResolver.PE/Code/AddressFixupType.cs index 4210ab7f3..b8b522057 100644 --- a/src/AsmResolver.PE/Code/AddressFixupType.cs +++ b/src/AsmResolver.PE/Code/AddressFixupType.cs @@ -1,7 +1,7 @@ namespace AsmResolver.PE.Code { /// - /// Defines all possible address fixup types that can be applied in a . + /// Defines all possible address fixup types that can be applied in an . /// public enum AddressFixupType { diff --git a/src/AsmResolver.PE/Code/CodeSegment.cs b/src/AsmResolver.PE/Code/CodeSegment.cs index b392e146e..f6dba5c0c 100644 --- a/src/AsmResolver.PE/Code/CodeSegment.cs +++ b/src/AsmResolver.PE/Code/CodeSegment.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; using AsmResolver.IO; +using AsmResolver.Patching; namespace AsmResolver.PE.Code { /// /// Represents a chunk of native code in a portable executable. /// + [Obsolete("This class has been superseded by AsmResolver.Patching.PatchedSegment.")] public class CodeSegment : SegmentBase { /// @@ -72,32 +74,9 @@ public override void Write(IBinaryStreamWriter writer) writer.WriteBytes(Code); for (int i = 0; i < AddressFixups.Count; i++) - ApplyAddressFixup(writer, AddressFixups[i]); + new AddressFixupPatch(AddressFixups[i]).Apply(new PatchContext(this, ImageBase, writer)); writer.Offset = Offset + GetPhysicalSize(); } - - private void ApplyAddressFixup(IBinaryStreamWriter writer, in AddressFixup fixup) - { - writer.Offset = Offset + fixup.Offset; - uint writerRva = Rva + fixup.Offset; - uint targetRva = fixup.Symbol.GetReference()?.Rva ?? 0; - - switch (fixup.Type) - { - case AddressFixupType.Absolute32BitAddress: - writer.WriteUInt32((uint) (ImageBase + targetRva)); - break; - case AddressFixupType.Relative32BitAddress: - writer.WriteInt32((int) (targetRva - (writerRva + 4))); - break; - case AddressFixupType.Absolute64BitAddress: - writer.WriteUInt64(ImageBase + targetRva); - break; - default: - throw new ArgumentOutOfRangeException(); - } - } - } } diff --git a/src/AsmResolver.PE/Platforms/Amd64Platform.cs b/src/AsmResolver.PE/Platforms/Amd64Platform.cs index 895e39c9d..08c042736 100644 --- a/src/AsmResolver.PE/Platforms/Amd64Platform.cs +++ b/src/AsmResolver.PE/Platforms/Amd64Platform.cs @@ -27,12 +27,13 @@ public static Amd64Platform Instance /// public override RelocatableSegment CreateThunkStub(ISymbol entryPoint) { - var segment = new CodeSegment(new byte[] - { - 0x48, 0xA1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // rex.w rex.b mov rax, [&symbol] - 0xFF, 0xE0 // jmp [rax] - }); - segment.AddressFixups.Add(new AddressFixup(2, AddressFixupType.Absolute64BitAddress, entryPoint)); + var segment = new DataSegment(new byte[] + { + 0x48, 0xA1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // rex.w rex.b mov rax, [&symbol] + 0xFF, 0xE0 // jmp [rax] + }) + .AsPatchedSegment() + .Patch(2, AddressFixupType.Absolute64BitAddress, entryPoint); return new RelocatableSegment(segment, new[] { diff --git a/src/AsmResolver.PE/Platforms/I386Platform.cs b/src/AsmResolver.PE/Platforms/I386Platform.cs index 87c431ae0..26cb29d78 100644 --- a/src/AsmResolver.PE/Platforms/I386Platform.cs +++ b/src/AsmResolver.PE/Platforms/I386Platform.cs @@ -27,11 +27,12 @@ public static I386Platform Instance /// public override RelocatableSegment CreateThunkStub(ISymbol entryPoint) { - var segment = new CodeSegment(new byte[] - { - 0xFF, 0x25, 0x00, 0x00, 0x00, 0x00 // jmp [&symbol] - }); - segment.AddressFixups.Add(new AddressFixup(2, AddressFixupType.Absolute32BitAddress, entryPoint)); + var segment = new DataSegment(new byte[] + { + 0xFF, 0x25, 0x00, 0x00, 0x00, 0x00 // jmp [&symbol] + }) + .AsPatchedSegment() + .Patch(2, AddressFixupType.Absolute32BitAddress, entryPoint); return new RelocatableSegment(segment, new[] { diff --git a/src/AsmResolver/ISegment.cs b/src/AsmResolver/ISegment.cs index dadb9dd84..849245fd0 100644 --- a/src/AsmResolver/ISegment.cs +++ b/src/AsmResolver/ISegment.cs @@ -152,7 +152,7 @@ public static ISegmentReference ToReference(this ISegment segment, int additive) : new RelativeReference(segment, additive); /// - /// Serializes the segment by calling and writes the result into a byte array. + /// Serializes the segment by calling and writes the result into a byte array. /// /// The segment to serialize to /// The resulting byte array. @@ -164,7 +164,7 @@ public static byte[] WriteIntoArray(this ISegment segment) } /// - /// Serializes the segment by calling and writes the result into a byte array. + /// Serializes the segment by calling and writes the result into a byte array. /// /// The segment to serialize to /// The memory stream writer pool to rent temporary writers from. diff --git a/test/AsmResolver.DotNet.Tests/Code/Native/NativeMethodBodyTest.cs b/test/AsmResolver.DotNet.Tests/Code/Native/NativeMethodBodyTest.cs index 356163a4d..e59af51e6 100644 --- a/test/AsmResolver.DotNet.Tests/Code/Native/NativeMethodBodyTest.cs +++ b/test/AsmResolver.DotNet.Tests/Code/Native/NativeMethodBodyTest.cs @@ -59,15 +59,14 @@ private static NativeMethodBody CreateDummyBody(bool isVoid, bool is32Bit) return method.NativeMethodBody = new NativeMethodBody(method); } - private static CodeSegment GetNewCodeSegment(IPEImage image) + private static IReadableSegment GetNewCodeSegment(IPEImage image) { var methodTable = image.DotNetDirectory!.Metadata! .GetStream() .GetTable(TableIndex.Method); var row = methodTable.First(r => (r.ImplAttributes & MethodImplAttributes.Native) != 0); Assert.True(row.Body.IsBounded); - var segment = Assert.IsAssignableFrom(row.Body.GetSegment()); - return segment; + return Assert.IsAssignableFrom(row.Body.GetSegment()); } [Fact] @@ -89,7 +88,7 @@ public void NativeMethodBodyShouldResultInRawCodeSegment() var segment = GetNewCodeSegment(image); // Verify code segment was created. - Assert.Equal(segment.Code, body.Code); + Assert.Equal(segment.ToArray(), body.Code); } [Fact] diff --git a/test/AsmResolver.PE.Tests/Code/AddressFixupTest.cs b/test/AsmResolver.PE.Tests/Code/AddressFixupTest.cs new file mode 100644 index 000000000..605df4fd5 --- /dev/null +++ b/test/AsmResolver.PE.Tests/Code/AddressFixupTest.cs @@ -0,0 +1,98 @@ +using System; +using System.Linq; +using AsmResolver.Patching; +using AsmResolver.PE.Code; +using Xunit; + +namespace AsmResolver.PE.Tests.Code +{ + public class AddressFixupTest + { + private readonly DataSegment _input = new(Enumerable + .Range(0, 1000) + .Select(x => (byte) (x & 0xFF)) + .ToArray()); + + private readonly ISymbol _dummySymbol = new Symbol(new VirtualAddress(0x0000_2000)); + + [Fact] + public void PatchAbsolute32BitAddress() + { + var patched = new PatchedSegment(_input); + patched.Patches.Add(new AddressFixupPatch( + new AddressFixup(10, AddressFixupType.Absolute32BitAddress, _dummySymbol))); + + patched.UpdateOffsets(new RelocationParameters(0x0040_0000, 0, 0, true)); + + byte[] expected = _input.ToArray(); + Buffer.BlockCopy(BitConverter.GetBytes(0x0040_2000), 0, expected, 10, sizeof(int)); + + byte[] actual = patched.WriteIntoArray(); + Assert.Equal(expected, actual); + } + + [Fact] + public void PatchAbsolute32BitAddressFluent() + { + var patched = _input + .AsPatchedSegment() + .Patch(10, AddressFixupType.Absolute32BitAddress, _dummySymbol); + + patched.UpdateOffsets(new RelocationParameters(0x0040_0000, 0, 0, true)); + + byte[] expected = _input.ToArray(); + Buffer.BlockCopy(BitConverter.GetBytes(0x0040_2000), 0, expected, 10, sizeof(int)); + + byte[] actual = patched.WriteIntoArray(); + Assert.Equal(expected, actual); + } + + [Fact] + public void PatchAbsolute64BitAddress() + { + var patched = new PatchedSegment(_input); + patched.Patches.Add(new AddressFixupPatch( + new AddressFixup(10, AddressFixupType.Absolute64BitAddress, _dummySymbol))); + + patched.UpdateOffsets(new RelocationParameters(0x0000_0001_0000_0000, 0, 0, true)); + + byte[] expected = _input.ToArray(); + Buffer.BlockCopy(BitConverter.GetBytes(0x0000_0001_0000_2000), 0, expected, 10, sizeof(ulong)); + + byte[] actual = patched.WriteIntoArray(); + Assert.Equal(expected, actual); + } + + [Fact] + public void PatchAbsolute64BitAddressFluent() + { + var patched = _input + .AsPatchedSegment() + .Patch(10, AddressFixupType.Absolute64BitAddress, _dummySymbol); + + patched.UpdateOffsets(new RelocationParameters(0x0000_0001_0000_0000, 0, 0, true)); + + byte[] expected = _input.ToArray(); + Buffer.BlockCopy(BitConverter.GetBytes(0x0000_0001_0000_2000), 0, expected, 10, sizeof(ulong)); + + byte[] actual = patched.WriteIntoArray(); + Assert.Equal(expected, actual); + } + + [Fact] + public void PatchRelative32BitAddress() + { + var patched = new PatchedSegment(_input); + patched.Patches.Add(new AddressFixupPatch( + new AddressFixup(10, AddressFixupType.Relative32BitAddress, _dummySymbol))); + + patched.UpdateOffsets(new RelocationParameters(0x0040_0000, 0, 0, true)); + + byte[] expected = _input.ToArray(); + Buffer.BlockCopy(BitConverter.GetBytes(0x2000 - 10 - 4), 0, expected, 10, sizeof(int)); + + byte[] actual = patched.WriteIntoArray(); + Assert.Equal(expected, actual); + } + } +} diff --git a/test/AsmResolver.PE.Tests/DotNet/Builder/MixedModeAssemblyTest.cs b/test/AsmResolver.PE.Tests/DotNet/Builder/MixedModeAssemblyTest.cs index b5e6988d6..678015d14 100644 --- a/test/AsmResolver.PE.Tests/DotNet/Builder/MixedModeAssemblyTest.cs +++ b/test/AsmResolver.PE.Tests/DotNet/Builder/MixedModeAssemblyTest.cs @@ -26,7 +26,7 @@ public MixedModeAssemblyTest(TemporaryDirectoryFixture fixture) _fixture = fixture; } - private static void ReplaceBodyWithNativeCode(IPEImage image, CodeSegment body, bool is32bit) + private static void ReplaceBodyWithNativeCode(IPEImage image, ISegment body, bool is32bit) { // Adjust image flags appropriately. image.DotNetDirectory!.Flags &= ~DotNetDirectoryFlags.ILOnly; @@ -89,7 +89,7 @@ public void NativeBodyWithNoCalls() // Read image var image = PEImage.FromBytes(Properties.Resources.TheAnswer_NetFx); - ReplaceBodyWithNativeCode(image, new CodeSegment(new byte[] + ReplaceBodyWithNativeCode(image, new DataSegment(new byte[] { 0xb8, 0x39, 0x05, 0x00, 0x00, // mov rax, 1337 0xc3 // ret @@ -120,27 +120,24 @@ public void NativeBodyWithCall() var function = new ImportedSymbol(0x4fc, "puts"); module.Symbols.Add(function); - var body = new CodeSegment(new byte[] - { - /* 00: */ 0x48, 0x83, 0xEC, 0x28, // sub rsp, 0x28 - /* 04: */ 0x48, 0x8D, 0x0D, 0x10, 0x00, 0x00, 0x00, // lea rcx, qword [rel str] - /* 0B: */ 0xFF, 0x15, 0x00, 0x00, 0x00, 0x00, // call qword [rel puts] - /* 11: */ 0xB8, 0x37, 0x13, 0x00, 0x00, // mov eax, 0x1337 - /* 16: */ 0x48, 0x83, 0xC4, 0x28, // add rsp, 0x28 - /* 1A: */ 0xC3, // ret - - // str: - 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x66, // "Hello f" - 0x72, 0x6f, 0x6d, 0x20, 0x74, 0x68, 0x65, // "rom the" - 0x20, 0x75, 0x6e, 0x6d, 0x61, 0x6e, 0x61, // " unmana" - 0x67, 0x65, 0x64, 0x20, 0x77, 0x6f, 0x72, // "ged wor" - 0x6c, 0x64, 0x21, 0x00 // "ld!" - }); - - // Fixup puts call. - body.AddressFixups.Add(new AddressFixup( - 0xD, AddressFixupType.Relative32BitAddress, function - )); + var body = new DataSegment(new byte[] + { + /* 00: */ 0x48, 0x83, 0xEC, 0x28, // sub rsp, 0x28 + /* 04: */ 0x48, 0x8D, 0x0D, 0x10, 0x00, 0x00, 0x00, // lea rcx, qword [rel str] + /* 0B: */ 0xFF, 0x15, 0x00, 0x00, 0x00, 0x00, // call qword [rel puts] + /* 11: */ 0xB8, 0x37, 0x13, 0x00, 0x00, // mov eax, 0x1337 + /* 16: */ 0x48, 0x83, 0xC4, 0x28, // add rsp, 0x28 + /* 1A: */ 0xC3, // ret + + // str: + 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x66, // "Hello f" + 0x72, 0x6f, 0x6d, 0x20, 0x74, 0x68, 0x65, // "rom the" + 0x20, 0x75, 0x6e, 0x6d, 0x61, 0x6e, 0x61, // " unmana" + 0x67, 0x65, 0x64, 0x20, 0x77, 0x6f, 0x72, // "ged wor" + 0x6c, 0x64, 0x21, 0x00 // "ld!" + }) + .AsPatchedSegment() + .Patch(0xD, AddressFixupType.Relative32BitAddress, function); // Replace body. ReplaceBodyWithNativeCode(image, body, false); @@ -170,24 +167,22 @@ public void NativeBodyWithCallX86() var function = new ImportedSymbol(0x4fc, "puts"); module.Symbols.Add(function); - var body = new CodeSegment(new byte[] - { - /* 00: */ 0x55, // push ebp - /* 01: */ 0x89, 0xE5, // mov ebp,esp - /* 03: */ 0x6A, 0x6F, // push byte +0x6f ; H - /* 05: */ 0x68, 0x48, 0x65, 0x6C, 0x6C, // push dword 0x6c6c6548 ; ello - /* 0A: */ 0x54, // push esp - /* 0B: */ 0xFF, 0x15, 0x00, 0x00, 0x00, 0x00, // call [dword puts] - /* 11: */ 0x83, 0xC4, 0x0C, // add esp,byte +0xc - /* 14: */ 0xB8, 0x37, 0x13, 0x00, 0x00, // mov eax,0x1337 - /* 19: */ 0x5D, // pop ebp - /* 1A: */ 0xC3, // ret - }); - - // Fix up puts call. - body.AddressFixups.Add(new AddressFixup( - 0xD, AddressFixupType.Absolute32BitAddress, function - )); + var body = new DataSegment(new byte[] + { + /* 00: */ 0x55, // push ebp + /* 01: */ 0x89, 0xE5, // mov ebp,esp + /* 03: */ 0x6A, 0x6F, // push byte +0x6f ; H + /* 05: */ 0x68, 0x48, 0x65, 0x6C, 0x6C, // push dword 0x6c6c6548 ; ello + /* 0A: */ 0x54, // push esp + /* 0B: */ 0xFF, 0x15, 0x00, 0x00, 0x00, 0x00, // call [dword puts] + /* 11: */ 0x83, 0xC4, 0x0C, // add esp,byte +0xc + /* 14: */ 0xB8, 0x37, 0x13, 0x00, 0x00, // mov eax,0x1337 + /* 19: */ 0x5D, // pop ebp + /* 1A: */ 0xC3, // ret + }) + .AsPatchedSegment() + .Patch(0xD, AddressFixupType.Absolute32BitAddress, function); + image.Relocations.Clear(); image.Relocations.Add(new BaseRelocation(RelocationType.HighLow, body.ToReference(0xD))); From c7e92752d41f27fdc439abb2be7204bcc0c753dd Mon Sep 17 00:00:00 2001 From: Washi Date: Tue, 17 Jan 2023 22:11:39 +0100 Subject: [PATCH 22/35] Add metadata builder flags for metadata stream preservation, add tests. --- .../Builder/MetadataBuilderFlags.cs | 18 ++++- .../Serialized/ModuleReaderContext.cs | 30 ++++++++ .../Builder/ManagedPEImageBuilderTest.cs | 68 +++++++++++++++++++ 3 files changed, 113 insertions(+), 3 deletions(-) diff --git a/src/AsmResolver.DotNet/Builder/MetadataBuilderFlags.cs b/src/AsmResolver.DotNet/Builder/MetadataBuilderFlags.cs index d0e4fcb7c..04672823d 100644 --- a/src/AsmResolver.DotNet/Builder/MetadataBuilderFlags.cs +++ b/src/AsmResolver.DotNet/Builder/MetadataBuilderFlags.cs @@ -126,11 +126,23 @@ public enum MetadataBuilderFlags | PreserveAssemblyReferenceIndices | PreserveMethodSpecificationIndices, /// - /// Indicates any kind of index into a blob or tables stream should be preserved whenever possible during the - /// construction of the metadata directory. + /// Indicates unconventional / spurious metadata streams present in the .NET metadata directory should be + /// preserved when possible. + /// + PreserveUnknownStreams = 0x20000, + + /// + /// Indicates unconventional metadata stream order in the .NET metadata directory should be preserved when + /// possible. + /// + PreserveStreamOrder = 0x40000, + + /// + /// Indicates any kind of index into a blob or tables stream, as well as unknown spurious metadata streams + /// should be preserved whenever possible during the construction of the metadata directory. /// PreserveAll = PreserveBlobIndices | PreserveGuidIndices | PreserveStringIndices | PreserveUserStringIndices - | PreserveTableIndices, + | PreserveTableIndices | PreserveUnknownStreams | PreserveStreamOrder, /// /// diff --git a/src/AsmResolver.DotNet/Serialized/ModuleReaderContext.cs b/src/AsmResolver.DotNet/Serialized/ModuleReaderContext.cs index e5dafc8ff..fdde28f7d 100644 --- a/src/AsmResolver.DotNet/Serialized/ModuleReaderContext.cs +++ b/src/AsmResolver.DotNet/Serialized/ModuleReaderContext.cs @@ -46,18 +46,23 @@ public ModuleReaderContext(IPEImage image, SerializedModuleDefinition parentModu { case TablesStream tablesStream when TablesStream is null: TablesStream = tablesStream; + TablesStreamIndex = i; break; case BlobStream blobStream when BlobStream is null || !isEncMetadata: BlobStream = blobStream; + BlobStreamIndex = i; break; case GuidStream guidStream when GuidStream is null || !isEncMetadata: GuidStream = guidStream; + GuidStreamIndex = i; break; case StringsStream stringsStream when StringsStream is null || !isEncMetadata: StringsStream = stringsStream; + StringsStreamIndex = i; break; case UserStringsStream userStringsStream when UserStringsStream is null || !isEncMetadata: UserStringsStream = userStringsStream; + UserStringsStreamIndex = i; break; } } @@ -96,6 +101,11 @@ public TablesStream TablesStream get; } + public int TablesStreamIndex + { + get; + } = -1; + /// /// Gets the main blob stream in the metadata directory. /// @@ -104,6 +114,11 @@ public BlobStream? BlobStream get; } + public int BlobStreamIndex + { + get; + } = -1; + /// /// Gets the main GUID stream in the metadata directory. /// @@ -112,6 +127,11 @@ public GuidStream? GuidStream get; } + public int GuidStreamIndex + { + get; + } = -1; + /// /// Gets the main strings stream in the metadata directory. /// @@ -120,6 +140,11 @@ public StringsStream? StringsStream get; } + public int StringsStreamIndex + { + get; + } = -1; + /// /// Gets the main user-strings stream in the metadata directory. /// @@ -128,6 +153,11 @@ public UserStringsStream? UserStringsStream get; } + public int UserStringsStreamIndex + { + get; + } = -1; + /// /// Gets the reader parameters. /// diff --git a/test/AsmResolver.DotNet.Tests/Builder/ManagedPEImageBuilderTest.cs b/test/AsmResolver.DotNet.Tests/Builder/ManagedPEImageBuilderTest.cs index 44daafcce..82c2630e5 100644 --- a/test/AsmResolver.DotNet.Tests/Builder/ManagedPEImageBuilderTest.cs +++ b/test/AsmResolver.DotNet.Tests/Builder/ManagedPEImageBuilderTest.cs @@ -2,6 +2,7 @@ using System.Linq; using AsmResolver.DotNet.Builder; using AsmResolver.PE; +using AsmResolver.PE.DotNet.Metadata; using Xunit; namespace AsmResolver.DotNet.Tests.Builder @@ -79,5 +80,72 @@ public void ConstructPEImageFromExistingModuleWithPreservation() var newModule = ModuleDefinition.FromImage(result); Assert.Equal(module.Name, newModule.Name); } + + [Fact] + public void PreserveUnknownStreams() + { + // Prepare a PE image with an extra unconventional stream. + var image = PEImage.FromBytes(Properties.Resources.HelloWorld); + byte[] data = { 1, 2, 3, 4 }; + image.DotNetDirectory!.Metadata!.Streams.Add(new CustomMetadataStream("#Custom", data)); + + // Load and rebuild. + var module = ModuleDefinition.FromImage(image); + var newImage = module.ToPEImage(new ManagedPEImageBuilder(MetadataBuilderFlags.PreserveUnknownStreams)); + + // Verify unconventional stream is still present. + var newStream = Assert.IsAssignableFrom( + newImage.DotNetDirectory!.Metadata!.GetStream("#Custom")); + Assert.Equal(data, Assert.IsAssignableFrom(newStream.Contents).ToArray()); + } + + [Fact] + public void PreserveStreamOrder() + { + // Prepare a PE image with an unconventional stream order. + var image = PEImage.FromBytes(Properties.Resources.HelloWorld); + var streams = image.DotNetDirectory!.Metadata!.Streams; + for (int i = 0; i < streams.Count / 2; i++) + (streams[i], streams[streams.Count - i - 1]) = (streams[streams.Count - i - 1], streams[i]); + + // Load and rebuild. + var module = ModuleDefinition.FromImage(image); + var newImage = module.ToPEImage(new ManagedPEImageBuilder(MetadataBuilderFlags.PreserveStreamOrder)); + + // Verify order is still the same. + Assert.Equal( + streams.Select(x => x.Name), + newImage.DotNetDirectory!.Metadata!.Streams.Select(x => x.Name)); + } + + [Fact] + public void PreserveUnknownStreamsAndStreamOrder() + { + // Prepare a PE image with an unconventional stream order and custom stream. + var image = PEImage.FromBytes(Properties.Resources.HelloWorld); + var streams = image.DotNetDirectory!.Metadata!.Streams; + + for (int i = 0; i < streams.Count / 2; i++) + (streams[i], streams[streams.Count - i - 1]) = (streams[streams.Count - i - 1], streams[i]); + + byte[] data = { 1, 2, 3, 4 }; + image.DotNetDirectory!.Metadata!.Streams.Insert(streams.Count / 2, + new CustomMetadataStream("#Custom", data)); + + // Load and rebuild. + var module = ModuleDefinition.FromImage(image); + var newImage = module.ToPEImage(new ManagedPEImageBuilder( + MetadataBuilderFlags.PreserveStreamOrder | MetadataBuilderFlags.PreserveUnknownStreams)); + + // Verify order is still the same. + Assert.Equal( + streams.Select(x => x.Name), + newImage.DotNetDirectory!.Metadata!.Streams.Select(x => x.Name)); + + // Verify unconventional stream is still present. + var newStream = Assert.IsAssignableFrom( + newImage.DotNetDirectory!.Metadata!.GetStream("#Custom")); + Assert.Equal(data, Assert.IsAssignableFrom(newStream.Contents).ToArray()); + } } } From 77ff7dcbd68d6fe6d4e230a1659b53edc5b03331 Mon Sep 17 00:00:00 2001 From: Washi Date: Tue, 17 Jan 2023 22:12:03 +0100 Subject: [PATCH 23/35] Implement metadata stream preservation. --- .../Builder/DotNetDirectoryFactory.cs | 70 ++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/src/AsmResolver.DotNet/Builder/DotNetDirectoryFactory.cs b/src/AsmResolver.DotNet/Builder/DotNetDirectoryFactory.cs index 7750a6146..41218981a 100644 --- a/src/AsmResolver.DotNet/Builder/DotNetDirectoryFactory.cs +++ b/src/AsmResolver.DotNet/Builder/DotNetDirectoryFactory.cs @@ -1,10 +1,13 @@ using System; +using System.Linq; using AsmResolver.DotNet.Builder.Discovery; using AsmResolver.DotNet.Builder.Metadata; using AsmResolver.DotNet.Code; using AsmResolver.DotNet.Code.Cil; using AsmResolver.DotNet.Code.Native; +using AsmResolver.DotNet.Serialized; using AsmResolver.PE.DotNet; +using AsmResolver.PE.DotNet.Metadata; using AsmResolver.PE.DotNet.Metadata.Blob; using AsmResolver.PE.DotNet.Metadata.Guid; using AsmResolver.PE.DotNet.Metadata.Strings; @@ -122,7 +125,16 @@ public virtual DotNetDirectoryBuildResult CreateDotNetDirectory( else if ((module.Attributes & DotNetDirectoryFlags.StrongNameSigned) != 0) buffer.StrongNameSize = 0x80; - return buffer.CreateDirectory(); + var result = buffer.CreateDirectory(); + + // If we need to preserve streams or stream order, apply the shifts accordingly. + if (module is SerializedModuleDefinition serializedModule + && (MetadataBuilderFlags & (MetadataBuilderFlags.PreserveUnknownStreams | MetadataBuilderFlags.PreserveStreamOrder)) != 0) + { + ReorderMetadataStreams(serializedModule, result.Directory.Metadata!); + } + + return result; } private MemberDiscoveryResult DiscoverMemberDefinitionsInModule(ModuleDefinition module) @@ -278,5 +290,61 @@ private static void ImportTables(ModuleDefinition module, TableIndex ta foreach (var member in module.TokenAllocator.GetAssignees(tableIndex)) importAction((TMember) member); } + + private void ReorderMetadataStreams(SerializedModuleDefinition serializedModule, IMetadata newMetadata) + { + IMetadataStream? GetStreamOrNull() + where TStream : class, IMetadataStream + { + return newMetadata.TryGetStream(out TStream? stream) + ? stream + : null; + } + + var readerContext = serializedModule.ReaderContext; + var streamIndices = new (int Index, IMetadataStream? Stream)[] + { + (readerContext.TablesStreamIndex, GetStreamOrNull()), + (readerContext.BlobStreamIndex, GetStreamOrNull()), + (readerContext.GuidStreamIndex, GetStreamOrNull()), + (readerContext.StringsStreamIndex, GetStreamOrNull()), + (readerContext.UserStringsStreamIndex, GetStreamOrNull()), + }; + + if ((MetadataBuilderFlags & MetadataBuilderFlags.PreserveUnknownStreams) != 0) + { + var originalStreams = serializedModule.DotNetDirectory.Metadata!.Streams; + + if ((MetadataBuilderFlags & MetadataBuilderFlags.PreserveStreamOrder) != 0) + { + newMetadata.Streams.Clear(); + + for (int i = 0; i < originalStreams.Count; i++) + { + var entry = streamIndices.FirstOrDefault(x => x.Index == i); + newMetadata.Streams.Insert(i, entry.Stream ?? originalStreams[i]); + } + } + else + { + for (int i = 0; i < originalStreams.Count; i++) + { + if (streamIndices.All(x => x.Index != i)) + newMetadata.Streams.Add(originalStreams[i]); + } + } + } + else if ((MetadataBuilderFlags & MetadataBuilderFlags.PreserveStreamOrder) != 0) + { + Array.Sort(streamIndices, (a, b) => a.Index.CompareTo(b.Index)); + newMetadata.Streams.Clear(); + + for (int i = 0; i < streamIndices.Length; i++) + { + if (streamIndices[i].Stream is { } stream) + newMetadata.Streams.Add(stream); + } + } + } } } From 57e99dd850a325635b89eed99d6d0b48c3479610 Mon Sep 17 00:00:00 2001 From: Washi Date: Tue, 17 Jan 2023 22:13:58 +0100 Subject: [PATCH 24/35] Add missing xmldoc. --- .../Serialized/ModuleReaderContext.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/AsmResolver.DotNet/Serialized/ModuleReaderContext.cs b/src/AsmResolver.DotNet/Serialized/ModuleReaderContext.cs index fdde28f7d..737eab418 100644 --- a/src/AsmResolver.DotNet/Serialized/ModuleReaderContext.cs +++ b/src/AsmResolver.DotNet/Serialized/ModuleReaderContext.cs @@ -101,6 +101,9 @@ public TablesStream TablesStream get; } + /// + /// Gets the original index of the tables stream. + /// public int TablesStreamIndex { get; @@ -114,6 +117,9 @@ public BlobStream? BlobStream get; } + /// + /// Gets the original index of the blob stream. + /// public int BlobStreamIndex { get; @@ -127,6 +133,9 @@ public GuidStream? GuidStream get; } + /// + /// Gets the original index of the GUID stream. + /// public int GuidStreamIndex { get; @@ -140,6 +149,9 @@ public StringsStream? StringsStream get; } + /// + /// Gets the original index of the strings stream. + /// public int StringsStreamIndex { get; @@ -153,6 +165,9 @@ public UserStringsStream? UserStringsStream get; } + /// + /// Gets the original index of the user-strings stream. + /// public int UserStringsStreamIndex { get; From c8940f2e8a9334058cea9f19df36bff5e8a66d3f Mon Sep 17 00:00:00 2001 From: Washi Date: Thu, 19 Jan 2023 20:15:24 +0100 Subject: [PATCH 25/35] Update docs on new metadata builder flags. --- docs/dotnet/advanced-pe-image-building.rst | 33 ++++++++++++++++++---- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/docs/dotnet/advanced-pe-image-building.rst b/docs/dotnet/advanced-pe-image-building.rst index 6efa35b88..db94d483c 100644 --- a/docs/dotnet/advanced-pe-image-building.rst +++ b/docs/dotnet/advanced-pe-image-building.rst @@ -72,10 +72,34 @@ Some .NET modules are carefully crafted and rely on the raw structure of all met - RIDs of rows within a metadata table. - Indices of blobs within the ``#Blob``, ``#Strings``, ``#US`` or ``#GUID`` heaps. - -The default PE image builder for .NET modules (``ManagedPEImageBuilder``) defines a property called ``DotNetDirectoryFactory``, which contains the object responsible for constructing the .NET data directory, can be configured to preserve as much of this structure as possible. With the help of the ``MetadataBuilderFlags`` enum, it is possible to indicate which structures of the metadata directory need to preserved. - -Below an example on how to configure the image builder to preserve blob data and all metadata tokens to type references: +- Unknown or unconventional metadata streams and their order. + +The default PE image builder for .NET modules (``ManagedPEImageBuilder``) defines a property called ``DotNetDirectoryFactory``, which contains the object responsible for constructing the .NET data directory, can be configured to preserve as much of this structure as possible. With the help of the ``MetadataBuilderFlags`` enum, it is possible to indicate which structures of the metadata directory need to preserved. The following table provides an overview of all preservation metadata builder flags that can be used and combined: + ++----------------------------------------+-------------------------------------------------------------------+ +| flag | Description | ++========================================+===================================================================+ +| ``PreserveXXXIndices`` | Preserves all row indices of the original ``XXX`` metadata table. | ++----------------------------------------+-------------------------------------------------------------------+ +| ``PreserveTableIndices`` | Preserves all row indices from all original metadata tables. | ++----------------------------------------+-------------------------------------------------------------------+ +| ``PreserveBlobIndices`` | Preserves all blob indices in the ``#Blob`` stream. | ++----------------------------------------+-------------------------------------------------------------------+ +| ``PreserveGuidIndices`` | Preserves all GUID indices in the ``#GUID`` stream. | ++----------------------------------------+-------------------------------------------------------------------+ +| ``PreserveStringIndices`` | Preserves all string indices in the ``#Strings`` stream. | ++----------------------------------------+-------------------------------------------------------------------+ +| ``PreserveUserStringIndices`` | Preserves all user-string indices in the ``#US`` stream. | ++----------------------------------------+-------------------------------------------------------------------+ +| ``PreserveUnknownStreams`` | Preserves any of the unknown / unconventional metadata streams. | ++----------------------------------------+-------------------------------------------------------------------+ +| ``PreserveStreamOrder`` | Preserves the original order of all metadata streams. | ++----------------------------------------+-------------------------------------------------------------------+ +| ``PreserveAll`` | Preserves as much of the original metadata as possible. | ++----------------------------------------+-------------------------------------------------------------------+ + + +Below is an example on how to configure the image builder to preserve blob data and all metadata tokens to type references: .. code-block:: csharp @@ -84,7 +108,6 @@ Below an example on how to configure the image builder to preserve blob data and | MetadataBuilderFlags.PreserveTypeReferenceIndices; imageBuilder.DotNetDirectoryFactory = factory; -If everything is supposed to be preserved as much as possible, then instead of specifying all flags defined in the ``MetadataBuilderFlags`` enum, we can also use ``MetadataBuilderFlags.PreserveAll`` as a shortcut. .. warning:: From 468d8a44bb67a17f132a2729a5a136cc0ab1d09c Mon Sep 17 00:00:00 2001 From: Washi Date: Tue, 24 Jan 2023 22:24:26 +0100 Subject: [PATCH 26/35] Add documentation article on segments. --- docs/index.rst | 4 +- docs/segments.rst | 242 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 244 insertions(+), 2 deletions(-) create mode 100644 docs/segments.rst diff --git a/docs/index.rst b/docs/index.rst index da9a805c7..e170599d5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,6 +13,7 @@ Table of Contents: overview faq + segments .. toctree:: @@ -47,7 +48,7 @@ Table of Contents: .. toctree:: :maxdepth: 1 :caption: .NET assemblies and modules - :name: sec-peimage + :name: sec-dotnet dotnet/index dotnet/basics @@ -55,7 +56,6 @@ Table of Contents: dotnet/member-tree dotnet/type-signatures dotnet/importing - dotnet/methods dotnet/managed-method-bodies dotnet/unmanaged-method-bodies dotnet/dynamic-methods diff --git a/docs/segments.rst b/docs/segments.rst new file mode 100644 index 000000000..8348757ef --- /dev/null +++ b/docs/segments.rst @@ -0,0 +1,242 @@ +Segments +======== + +Segments are the basis of everything in AsmResolver. +They are the fundamental building blocks that together make up a binary file (such as a PE file). +Segments are organized as a tree, where the leaves are single contiguous chunk of memory, while the nodes are segments that comprise multiple smaller sub-segments. +The aim of segments is to abstract away the complicated mess that comes with calculating offsets, sizes and updating them accordingly, allowing programmers to easily read binary files, as well as construct new ones. + +Every class that directly translates to a concrete segment in a file on the disk implements the ``ISegment`` interface. +In the following, some of the basics of ``ISegment`` as well as common examples will be introduced. + + +Basic Data Segments +------------------- + +The simplest and arguably the most commonly used form of segment is the ``DataSegment`` class. +This is a class that wraps around a ``byte[]`` into an instance of ``ISegment``, allowing it to be used in any context where a segment are expected in AsmResolver. + +.. code-block:: csharp + + byte[] data = new byte[] { 1, 2, 3, 4 }; + var segment = new DataSegment(data); + + +While the name of the ``DataSegment`` class implies it is used for defining literal data (such as a constant for a variable), it can be used to define *any* type of contiguous memory. +This also includes a raw code stream of a function body and sometimes entire program sections. + + +Reading Segment Contents +------------------------ + +Some implementations of ``ISegment`` (such as ``DataSegment``) allow for reading binary data directly. +Segments that allow for this implement ``IReadableSegment``, which defines a function ``CreateReader`` that can be used to create an instance of ``BinaryStreamReader`` that starts at the beginning of the raw contents of the segment. +This reader can then be used to read the contents of the segment. + +.. code-block:: csharp + + byte[] data = new byte[] { 1, 2, 3, 4 }; + IReadableSegment segment = new DataSegment(data); + + var reader = segment.CreateReader(); + reader.ReadByte(); // returns 1 + reader.ReadByte(); // returns 2 + reader.ReadByte(); // returns 3 + reader.ReadByte(); // returns 4 + reader.ReadByte(); // throws EndOfStreamException. + + +Alternatively, a ``IReadableSegment`` can be turned into a ``byte[]`` quickly using the ``ToArray()`` method. + +.. code-block:: csharp + + byte[] data = new byte[] { 1, 2, 3, 4 }; + IReadableSegment segment = new DataSegment(data); + + byte[] allData = segment.ToArray(); // Returns { 1, 2, 3, 4 } + + +Composing new Segments +---------------------- + +Many segments comprise multiple smaller sub-segments. +For example, PE sections often do not contain just a single data structure, but are a collection of structures concatenated together. +To facilitate more complicated structures like these, the ``SegmentBuilder`` class can be used to combine ``ISegment`` instances into one effortlessly: + +.. code-block:: csharp + + var builder = new SegmentBuilder(); + + builder.Add(new DataSegment(...)); + builder.Add(new DataSegment(...)); + + +Many segments in an executable file format require segments to be aligned to a certain byte-boundary. +The ``SegmentBuilder::Add`` method allows for specifying this alignment, and automatically adjust the offsets and sizes accordingly: + +.. code-block:: csharp + + var builder = new SegmentBuilder(); + + // Add some segment with potentially a size that is not a multiple of 4 bytes. + builder.Add(new DataSegment(...)); + + // Ensure the next segment is aligned to a 4-byte boundary in the final file. + builder.Add(new DataSegment(...), alignment: 4); + + +Since ``SegmentBuilder`` implements ``ISegment`` itself, it can also be used within another ``SegmentBuilder``, allowing for recursive constructions like the following: + +.. code-block:: csharp + + var child = new SegmentBuilder(); + child.Add(new DataSegment(...)); + child.Add(new DataSegment(...)); + + var root = new SegmentBuilder(); + builder.Add(new DataSegment(...)); + builder.Add(child); // Nest segment builders into each other. + + +Resizing Segments at Runtime +---------------------------- + +Most segments in an executable file retain their size at runtime. +However, some segments (such as a ``.bss`` section in a PE file) may be resized upon mapping it into memory. +AsmResolver represents these segments using the ``VirtualSegment`` class: + +.. code-block:: csharp + + var physicalContents = new DataSegment(new byte[] {1, 2, 3, 4}); + section.Contents = new VirtualSegment(physicalContents, 0x1000); // Create a new segment with a virtual size of 0x1000 bytes. + + +Patching Segments +----------------- + +Some use-cases of AsmResolver require segments to be hot-patched with new data after serialization. +This is done via the ``PatchedSegment`` class. + +Any segment can be wrapped into a ``PatchedSegment`` via its constructor: + +.. code-block:: csharp + + ISegment segment = ... + var patchedSegment = new PatchedSegment(segment); + + +Alternatively, you can use (the preferred) fluent syntax: + +.. code-block:: csharp + + ISegment segment = ... + var patchedSegment = segment.AsPatchedSegment(); + + +Applying the patches can then be done by repeatedly calling one of the ``Patch`` method overloads. +Below is an example of patching a section within a PE file: + +.. code-block:: csharp + + var peFile = PEFile.FromFile("input.exe"); + var section = peFile.Sections.First(s => s.Name == ".text"); + + var someSymbol = peImage + .Imports.First(m => m.Name == "ucrtbased.dll") + .Symbols.First(s => s.Name == "puts"); + + section.Contents = section.Contents.AsPatchedSegment() // Create patched segment. + .Patch(offset: 0x10, data: new byte[] {1, 2, 3, 4}) // Apply literal bytes patch + .Patch(offset: 0x20, AddressFixupType.Absolute64BitAddress, someSymbol); // Apply address fixup patch. + + +The patching API can be extended by implementing the ``IPatch`` yourself. + + +Calculating Offsets and Sizes +----------------------------- + +Typically, the ``ISegment`` API aims to abstract away any raw offset, relative virtual address (RVA), and/or size of a data structure within a binary file. +However, in case the final offset and/or size of a segment still need to be determined and used (e.g., when implementing new segments), it is important to understand how this is done. + +Two properties are responsible for representing the offsets: + +- ``Offset``: The starting file or memory address of the segment. +- ``Rva``: The virtual address of the segment, relative to the executable's image base at runtime. + + +Typically, these properties are read-only and managed by AsmResolver itself. +However, to update the offsets and RVAs of a segment, you can call the ``UpdateOffsets`` method. +This method traverses the entire segment recursively, and updates the offsets accordingly. + +.. code-block:: csharp + + ISegment segment = ... + + // Relocate a segment to an offsets-rva pair: + segment.UpdateOffsets(new RelocationParameters(offset: 0x200, rva: 0x2000); + + Console.WriteLine("Offset: 0x{0:X8}", segment.Offset); // Prints 0x200 + Console.WriteLine("Rva: 0x{0:X8}", segment.Rva); // Prints 0x2000 + +.. warning:: + + Try to call ``UpdateOffsets()`` as sparsely as possible. + The method does a full pass on the entire segment, and updates all offsets of all sub-segments as well. + It can thus be very inefficient to call them repeatedly. + + +The size (in bytes) of a segment can be calculated using either the ``GetPhysicalSize()`` or ``GetVirtualSize()``. +Typically, these two measurements are going to be equal, but for some segments (such as a ``VirtualSegment``) this may differ: + +.. code-block:: csharp + + ISegment segment = ... + + // Measure the size of the segment: + uint size segment.GetPhysicalSize(); + + Console.WriteLine("Size: 0x{0:X8}", size); + + +.. warning:: + + Only call ``GetPhysicalSize()`` and ``GetVirtualSize()`` whenever you know the offsets of the segment are up to date. + Due to padding requirements, many segments will have a slightly different size depending on the final file offset they are placed at. + + +.. warning:: + + Try to call ``GetPhysicalSize()`` and ``GetVirtualSize()`` as sparsely as possible. + These methods do a full pass on the entire segment, and measure the total amount of bytes required to represent it. + It can thus be very inefficient to call them repeatedly. + + +Serializing Segments +-------------------- + +Segments are serialized using the ``ISegment::Write`` method. + +.. code-block:: csharp + + ISegment segment = ... + + using var stream = new MemoryStream(); + segment.Write(new BinaryStreamWriter(stream)); + + byte[] serializedData = stream.ToArray(); + + +Alternatively, you can quickly serialize a segment to a ``byte[]`` using the ``WriteIntoArray()`` extension method: + +.. code-block:: csharp + + ISegment segment = ... + + byte[] serializedData = stream.WriteIntoArray(); + + +.. warning:: + + Only call ``Write`` whenever you know the offsets of the segment are up to date. + Many segments will contain offsets to other segments in the file, which may not be accurate until all offsets are calculated. From f2ad02b9f999757751e8f9689309c32992333149 Mon Sep 17 00:00:00 2001 From: Washi Date: Tue, 24 Jan 2023 22:34:35 +0100 Subject: [PATCH 27/35] Update docs on PE section building. --- docs/pefile/sections.rst | 6 ++-- docs/peimage/pe-building.rst | 70 +++++++++++++++++++++++++++--------- docs/segments.rst | 10 ++++-- 3 files changed, 65 insertions(+), 21 deletions(-) diff --git a/docs/pefile/sections.rst b/docs/pefile/sections.rst index 73cf12419..605425cb6 100644 --- a/docs/pefile/sections.rst +++ b/docs/pefile/sections.rst @@ -23,7 +23,7 @@ This can be used to read the data that is present in the section. If you want to .. code-block:: csharp byte[] data = section.ToArray(); - + The ``Sections`` property is mutable, which means you can add new sections and remove others from the PE. @@ -42,8 +42,8 @@ Some sections (such as `.data` or `.bss`) contain uninitialized data, and might var section = new PESection(".asmres", SectionFlags.MemoryRead | SectionFlags.ContentUninitializedData); var physicalContents = new DataSegment(new byte[] {1, 2, 3, 4}); section.Contents = new VirtualSegment(physicalContents, 0x1000); // Create a new segment with a virtual size of 0x1000 bytes. - + peFile.Sections.Add(section); -For more advanced section building, see :ref:`pe-building-sections`. \ No newline at end of file +For more advanced section building, see :ref:`pe-building-sections` and :ref:`segments`. diff --git a/docs/peimage/pe-building.rst b/docs/peimage/pe-building.rst index 94098f680..51a21c479 100644 --- a/docs/peimage/pe-building.rst +++ b/docs/peimage/pe-building.rst @@ -3,11 +3,16 @@ PE File Building ================ -An ``IPEImage`` is a higher-level abstraction of an actual PE file, and hides many of the actual implementation details such as raw section layout and contents of data directories. While this makes reading and interpreting a PE very easy, it makes writing a more involved process. +An ``IPEImage`` is a higher-level abstraction of an actual PE file, and hides many of the actual implementation details such as raw section layout and contents of data directories. +While this makes reading and interpreting a PE very easy, it makes writing a more involved process. -Unfortunately, the PE file format is not well-structured. Compilers and linkers have a lot of freedom when it comes to the actual design and layout of the final PE file. For example, one compiler might place the Import Address Table (IAT) in a separate section (such as the MSVC), while others will put it next to the code in the text section (such as the C# compiler). This degree of freedom makes it virtually impossible to make a completely generic PE file builder, and as such AsmResolver does not come with one out of the box. +Unfortunately, the PE file format is not well-structured. +Compilers and linkers have a lot of freedom when it comes to the actual design and layout of the final PE file. +For example, one compiler might place the Import Address Table (IAT) in a separate section (such as the MSVC), while others will put it next to the code in the text section (such as the C# compiler). +This degree of freedom makes it virtually impossible to make a completely generic PE file builder, and as such AsmResolver does not come with one out of the box. -It is still possible to build PE files from instances of ``IPEImage``, it might just take more manual labor than you might expect at first. This article will discuss certain strategies that can be adopted when constructing new PE files from PE images. +It is still possible to build PE files from instances of ``IPEImage``, it might just take more manual labor than you might expect at first. +This article will discuss certain strategies that can be adopted when constructing new PE files from PE images. .. _pe-building-sections: @@ -15,27 +20,33 @@ It is still possible to build PE files from instances of ``IPEImage``, it might Building Sections ----------------- -As discussed in :ref:`pe-file-sections`, the ``PESection`` class represents a single section in the PE file. This class exposes a property called ``Contents`` which is of type ``ISegment``. A lot of models in AsmResolver implement this interface, and as such, these models can directly be used as the contents of a new section. +As discussed in :ref:`pe-file-sections`, the ``PESection`` class represents a single section in the PE file. +This class exposes a property called ``Contents`` which is of type ``ISegment``. +A lot of models in AsmResolver implement this interface, and as such, these models can directly be used as the contents of a new section. -.. code-block:: csharp +.. code-block:: csharp var peFile = new PEFile(); var text = new PESection(".text", SectionFlags.ContentCode | SectionFlags.MemoryRead | SectionFlags.MemoryExecute); - text.Content = new CodeSegment(new byte[] { ... } ); + text.Content = new DataSegment(new byte[] { ... } ); peFile.Sections.Add(text); -In a lot of cases, however, sections do not consist of a single data structure, but often comprise many segments concatenated together, potentially with some padding in between. Assigning a single instance of ``ISegment`` might therefore not be as convenient. To facilitate more complicated structures, the ``SegmentBuilder`` class can help a lot with building sections like these. This is a class that combines a collection of ``ISegment`` instances into one, allowing you to concatenate segments that belong together easily. Below an example that builds up a ``.text`` section that consists of a .NET data directory as well as some native code. In the example, the native code is aligned to a 4-byte boundary: +In a lot of cases, however, sections do not consist of a single data structure, but often comprise many segments concatenated together, potentially with some padding in between. +To compose new segments from a collection of smaller sub-segments structures, the ``SegmentBuilder`` class can be used. +This is a class that combines a collection of ``ISegment`` instances into one, allowing you to concatenate segments that belong together easily. +Below an example that builds up a ``.text`` section that consists of a .NET data directory as well as some native code. +In the example, the native code is aligned to a 4-byte boundary: -.. code-block:: csharp +.. code-block:: csharp var peFile = new PEFile(); var textBuilder = new SegmentBuilder(); textBuilder.Add(new DotNetDirectory { ... }); - textBuilder.Add(new CodeSegment(new byte[] { ... } ), alignment: 4); + textBuilder.Add(new DataSegment(new byte[] { ... } ), alignment: 4); var text = new PESection(".text", SectionFlags.ContentCode | SectionFlags.MemoryRead | SectionFlags.MemoryExecute); @@ -44,12 +55,38 @@ In a lot of cases, however, sections do not consist of a single data structure, peFile.Sections.Add(text); -Using complex PE models +PE files can also be patched using the ``PatchedSegment`` API. +Below is an example of patching some data in the ``.text`` section of a PE File with some literal data ``{1, 2, 3, 4}``, as well as writing an address to a symbol in the imports table: + +.. code-block:: csharp + + var peFile = PEFile.FromFile("input.exe"); + var section = peFile.Sections.First(s => s.Name == ".text"); + + var someSymbol = peImage + .Imports.First(m => m.Name == "ucrtbased.dll") + .Symbols.First(s => s.Name == "puts"); + + section.Contents = section.Contents.AsPatchedSegment() // Create patched segment. + .Patch(offset: 0x10, data: new byte[] {1, 2, 3, 4}) // Apply literal bytes patch + .Patch(offset: 0x20, AddressFixupType.Absolute64BitAddress, someSymbol); // Apply address fixup patch. + + +More detailed information on how to use segments can be found in :ref:`segments`. + + +Using complex PE models ----------------------- -The PE file format defines many complex data structures. While some of these models are represented in AsmResolver using classes that derive from ``ISegment``, a lot of the classes that represent these data structures do not implement this interface, and as such cannot be used as a value for the ``Contents`` property of a ``PESection`` directly. This is due to the fact that most of these models are not required to be one single entity or chunk of continuous memory within the PE file. Instead, they are often scattered around the PE file by a compiler. For example, the Import Directory has a second component the Import Address Table which is often put in a completely different PE section (usually ``.text`` or ``.data``) than the Import Directory itself (in ``.idata`` or ``.rdata``). To make reading and interpreting these data structures more convenient for the end-user, the ``AsmResolver.PE`` package adopted some design choices to abstract these details away to make things more natural to work with. The downside of this is that writing these structures requires you to specify where AsmResolver should place these models in the final PE file. +The PE file format defines many complex data structures. +While some of these models are represented in AsmResolver using classes that derive from ``ISegment``, a lot of the classes that represent these data structures do not implement this interface, and as such cannot be used as a value for the ``Contents`` property of a ``PESection`` directly. +This is due to the fact that most of these models are not required to be one single entity or chunk of continuous memory within the PE file. Instead, they are often scattered around the PE file by a compiler. +For example, the Import Directory has a second component the Import Address Table which is often put in a completely different PE section (usually ``.text`` or ``.data``) than the Import Directory itself (in ``.idata`` or ``.rdata``). +To make reading and interpreting these data structures more convenient for the end-user, the ``AsmResolver.PE`` package adopted some design choices to abstract these details away to make things more natural to work with. +The downside of this is that writing these structures requires you to specify where AsmResolver should place these models in the final PE file. -In ``AsmResolver.PE``, most models for which is the case reside in their own namespace, and have their own set of classes dedicated to constructing new segments defined in a ``Builder`` sub-namespace. For example, the Win32 resources directory models reside in ``AsmResolver.PE.Win32Resources``, but the actual builder classes are put in a sub namespace called ``AsmResolver.PE.Win32Resources.Builder``. +In ``AsmResolver.PE``, most models for which is the case reside in their own namespace, and have their own set of classes dedicated to constructing new segments defined in a ``Builder`` sub-namespace. +For example, the Win32 resources directory models reside in ``AsmResolver.PE.Win32Resources``, but the actual builder classes are put in a sub namespace called ``AsmResolver.PE.Win32Resources.Builder``. .. code-block:: csharp @@ -94,7 +131,8 @@ A more complicated structure such as the Imports Directory can be build like the Using PEFileBuilders -------------------- -As a lot of the PE file-building process will be similar for many types of PE file layouts (such as the construction of the file and optional headers), AsmResolver comes with a base class called ``PEFileBuilderBase`` that abstracts many of these similarities away. Rather than defining and building up everything yourself, the ``PEFileBuilderBase`` allows you to override a couple of methods: +As a lot of the PE file-building process will be similar for many types of PE file layouts (such as the construction of the file and optional headers), AsmResolver comes with a base class called ``PEFileBuilderBase`` that abstracts many of these similarities away. +Rather than defining and building up everything yourself, the ``PEFileBuilderBase`` allows you to override a couple of methods: .. code-block:: csharp @@ -116,10 +154,10 @@ As a lot of the PE file-building process will be similar for many types of PE fi protected override IEnumerable CreateDataDirectories(PEFile peFile, IPEImage image, MyBuilderContext context) { /* Create data directories here */ - } + } } - public class MyBuilderContext + public class MyBuilderContext { /* Define here additional state data to be used in your builder. */ } @@ -132,4 +170,4 @@ This can then be used like the following: IPEImage image = ... var builder = new MyPEFileBuilder(); - PEFile file = builder.CreateFile(image); \ No newline at end of file + PEFile file = builder.CreateFile(image); diff --git a/docs/segments.rst b/docs/segments.rst index 8348757ef..616344f3f 100644 --- a/docs/segments.rst +++ b/docs/segments.rst @@ -1,5 +1,7 @@ -Segments -======== +.. _segments: + +Reading and Writing File Segments +================================= Segments are the basis of everything in AsmResolver. They are the fundamental building blocks that together make up a binary file (such as a PE file). @@ -121,6 +123,8 @@ Any segment can be wrapped into a ``PatchedSegment`` via its constructor: .. code-block:: csharp + using AsmResolver.Patching; + ISegment segment = ... var patchedSegment = new PatchedSegment(segment); @@ -129,6 +133,8 @@ Alternatively, you can use (the preferred) fluent syntax: .. code-block:: csharp + using AsmResolver.Patching; + ISegment segment = ... var patchedSegment = segment.AsPatchedSegment(); From 8274efb27a9bfb4636c52dd01faace5fcd93bd0c Mon Sep 17 00:00:00 2001 From: Washi Date: Tue, 24 Jan 2023 22:42:05 +0100 Subject: [PATCH 28/35] Add more generic Patch overload. --- src/AsmResolver/Patching/PatchedSegment.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/AsmResolver/Patching/PatchedSegment.cs b/src/AsmResolver/Patching/PatchedSegment.cs index 82ac9a80e..6ed40509c 100644 --- a/src/AsmResolver/Patching/PatchedSegment.cs +++ b/src/AsmResolver/Patching/PatchedSegment.cs @@ -83,6 +83,17 @@ private void ApplyPatches(IBinaryStreamWriter writer) writer.Offset = offset; } + /// + /// Adds a patch to the list of patches to apply. + /// + /// The patch to apply. + /// The current instance. + public PatchedSegment Patch(IPatch patch) + { + Patches.Add(patch); + return this; + } + /// /// Adds a bytes patch to the list of patches to apply. /// From 2fbe96dc9234484aafe7329a68183ed4a678bc85 Mon Sep 17 00:00:00 2001 From: Washi Date: Thu, 26 Jan 2023 21:17:26 +0100 Subject: [PATCH 29/35] Add CA parameter count validation logic. --- .../DotNetDirectoryBuffer.CodedIndices.cs | 5 +++ .../Signatures/CustomAttributeSignature.cs | 37 +++++++++++++++++++ .../SerializedCustomAttributeSignature.cs | 6 +++ .../CustomAttributeTest.cs | 20 ++++++++++ 4 files changed, 68 insertions(+) diff --git a/src/AsmResolver.DotNet/Builder/DotNetDirectoryBuffer.CodedIndices.cs b/src/AsmResolver.DotNet/Builder/DotNetDirectoryBuffer.CodedIndices.cs index 2aeee551a..52e6c3df8 100644 --- a/src/AsmResolver.DotNet/Builder/DotNetDirectoryBuffer.CodedIndices.cs +++ b/src/AsmResolver.DotNet/Builder/DotNetDirectoryBuffer.CodedIndices.cs @@ -17,6 +17,11 @@ private void AddCustomAttribute(MetadataToken ownerToken, CustomAttribute attrib { var table = Metadata.TablesStream.GetSortedTable(TableIndex.CustomAttribute); + // Ensure the signature defines the right parameters w.r.t. the constructor's parameters. + if (attribute.Constructor is not null && attribute.Signature is not null) + attribute.Signature.IsCompatibleWith(attribute.Constructor, ErrorListener); + + // Add it. var encoder = Metadata.TablesStream.GetIndexEncoder(CodedIndex.HasCustomAttribute); var row = new CustomAttributeRow( encoder.EncodeToken(ownerToken), diff --git a/src/AsmResolver.DotNet/Signatures/CustomAttributeSignature.cs b/src/AsmResolver.DotNet/Signatures/CustomAttributeSignature.cs index eaa877fdb..79bc6a216 100644 --- a/src/AsmResolver.DotNet/Signatures/CustomAttributeSignature.cs +++ b/src/AsmResolver.DotNet/Signatures/CustomAttributeSignature.cs @@ -156,6 +156,43 @@ protected override void WriteContents(in BlobSerializationContext context) for (int i = 0; i < NamedArguments.Count; i++) NamedArguments[i].Write(context); } + + /// + /// Validates whether the signature is compatible with the provided attribute constructor. + /// + /// The constructor to validate against. + /// true if the constructor is compatible, false otherwise. + public bool IsCompatibleWith(ICustomAttributeType constructor) + { + return IsCompatibleWith(constructor, EmptyErrorListener.Instance); + } + + /// + /// Validates whether the signature is compatible with the provided attribute constructor. + /// + /// The constructor to validate against. + /// The object responsible for reporting any errors during the validation of the signature. + /// true if the constructor is compatible, false otherwise. + public virtual bool IsCompatibleWith(ICustomAttributeType constructor, IErrorListener listener) + { + var signature = constructor.Signature; + + int expectedCount = signature?.ParameterTypes.Count ?? 0; + if (expectedCount != FixedArguments.Count) + { + listener.MetadataBuilder( + $"{constructor.SafeToString()} expects {expectedCount} arguments but the signature provided {FixedArguments.Count} arguments."); + return false; + } + + if (signature?.SentinelParameterTypes.Count > 0) + { + listener.MetadataBuilder($"{constructor.SafeToString()} defines sentinel parameters."); + return false; + } + + return true; + } } } diff --git a/src/AsmResolver.DotNet/Signatures/SerializedCustomAttributeSignature.cs b/src/AsmResolver.DotNet/Signatures/SerializedCustomAttributeSignature.cs index 4dfadd299..2b2138205 100644 --- a/src/AsmResolver.DotNet/Signatures/SerializedCustomAttributeSignature.cs +++ b/src/AsmResolver.DotNet/Signatures/SerializedCustomAttributeSignature.cs @@ -75,5 +75,11 @@ protected override void WriteContents(in BlobSerializationContext context) else _reader.Fork().WriteToOutput(context.Writer); } + + /// + public override bool IsCompatibleWith(ICustomAttributeType constructor, IErrorListener listener) + { + return !IsInitialized || base.IsCompatibleWith(constructor, listener); + } } } diff --git a/test/AsmResolver.DotNet.Tests/CustomAttributeTest.cs b/test/AsmResolver.DotNet.Tests/CustomAttributeTest.cs index b93e25f7e..ae0e5db79 100644 --- a/test/AsmResolver.DotNet.Tests/CustomAttributeTest.cs +++ b/test/AsmResolver.DotNet.Tests/CustomAttributeTest.cs @@ -646,5 +646,25 @@ public void NamedGenericTypeNullArgument(bool rebuild, bool access) Assert.Null(argument.Argument.Element); } + + [Fact] + public void TestSignatureCompatibility() + { + var module = new ModuleDefinition("Dummy"); + var factory = module.CorLibTypeFactory; + var ctor = factory.CorLibScope + .CreateTypeReference("System", "CLSCompliantAttribute") + .CreateMemberReference(".ctor", MethodSignature.CreateInstance(factory.Void, factory.Boolean)) + .ImportWith(module.DefaultImporter); + + var attribute = new CustomAttribute(ctor); + + // Empty signature is not compatible with a ctor that takes a boolean. + Assert.False(attribute.Signature!.IsCompatibleWith(attribute.Constructor!)); + + // If we add it, it should be compatible again. + attribute.Signature.FixedArguments.Add(new CustomAttributeArgument(factory.Boolean, true)); + Assert.True(attribute.Signature!.IsCompatibleWith(attribute.Constructor!)); + } } } From dc4ffd9f587b6531dfbd5a68177c8bbd14eb421c Mon Sep 17 00:00:00 2001 From: Washi Date: Thu, 26 Jan 2023 21:39:32 +0100 Subject: [PATCH 30/35] Clarify CA errors are coming from the CA constructor validation. --- src/AsmResolver.DotNet/Signatures/CustomAttributeSignature.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/AsmResolver.DotNet/Signatures/CustomAttributeSignature.cs b/src/AsmResolver.DotNet/Signatures/CustomAttributeSignature.cs index 79bc6a216..f382851b4 100644 --- a/src/AsmResolver.DotNet/Signatures/CustomAttributeSignature.cs +++ b/src/AsmResolver.DotNet/Signatures/CustomAttributeSignature.cs @@ -181,13 +181,13 @@ public virtual bool IsCompatibleWith(ICustomAttributeType constructor, IErrorLis if (expectedCount != FixedArguments.Count) { listener.MetadataBuilder( - $"{constructor.SafeToString()} expects {expectedCount} arguments but the signature provided {FixedArguments.Count} arguments."); + $"Custom attribute constructor {constructor.SafeToString()} expects {expectedCount} arguments but the signature provided {FixedArguments.Count} arguments."); return false; } if (signature?.SentinelParameterTypes.Count > 0) { - listener.MetadataBuilder($"{constructor.SafeToString()} defines sentinel parameters."); + listener.MetadataBuilder($"Custom attribute constructor {constructor.SafeToString()} defines sentinel parameters."); return false; } From f5df95cc933ee5b28d3daf8fa8633c230f8ddbab Mon Sep 17 00:00:00 2001 From: Washi Date: Thu, 26 Jan 2023 21:50:00 +0100 Subject: [PATCH 31/35] Bump version. --- Directory.Build.props | 2 +- appveyor.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 128e21272..f94ba5755 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -7,7 +7,7 @@ https://github.com/Washi1337/AsmResolver git 10 - 5.0.0 + 5.1.0 diff --git a/appveyor.yml b/appveyor.yml index 7a1ff9bec..30adc28f9 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -4,7 +4,7 @@ - master image: Visual Studio 2022 - version: 5.0.0-master-build.{build} + version: 5.1.0-master-build.{build} configuration: Release skip_commits: @@ -33,7 +33,7 @@ - development image: Visual Studio 2022 - version: 5.0.0-dev-build.{build} + version: 5.1.0-dev-build.{build} configuration: Release skip_commits: From da3109d03b613dd6dc072674a76163da33b830c9 Mon Sep 17 00:00:00 2001 From: Washi Date: Thu, 26 Jan 2023 21:58:37 +0100 Subject: [PATCH 32/35] Fix typos in segments docs. --- docs/segments.rst | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/segments.rst b/docs/segments.rst index 616344f3f..b58e0c50e 100644 --- a/docs/segments.rst +++ b/docs/segments.rst @@ -96,8 +96,8 @@ Since ``SegmentBuilder`` implements ``ISegment`` itself, it can also be used wit child.Add(new DataSegment(...)); var root = new SegmentBuilder(); - builder.Add(new DataSegment(...)); - builder.Add(child); // Nest segment builders into each other. + root.Add(new DataSegment(...)); + root.Add(child); // Nest segment builders into each other. Resizing Segments at Runtime @@ -200,9 +200,11 @@ Typically, these two measurements are going to be equal, but for some segments ( ISegment segment = ... // Measure the size of the segment: - uint size segment.GetPhysicalSize(); + uint physicalSize = segment.GetPhysicalSize(); + uint virtualSize = segment.GetVirtualSize(); - Console.WriteLine("Size: 0x{0:X8}", size); + Console.WriteLine("Physical (File) Size: 0x{0:X8}", physicalSize); + Console.WriteLine("Virtual (Runtime) Size: 0x{0:X8}", virtualSize); .. warning:: From 739012c180bbf1a825178a87d02fd5973231a01e Mon Sep 17 00:00:00 2001 From: Washi Date: Sun, 29 Jan 2023 12:03:24 +0100 Subject: [PATCH 33/35] Add Patch overload that takes a relative symbol offset. --- .../Code/AddressFixupExtensions.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/AsmResolver.PE/Code/AddressFixupExtensions.cs b/src/AsmResolver.PE/Code/AddressFixupExtensions.cs index 38b93e076..39e50411c 100644 --- a/src/AsmResolver.PE/Code/AddressFixupExtensions.cs +++ b/src/AsmResolver.PE/Code/AddressFixupExtensions.cs @@ -22,6 +22,23 @@ public static PatchedSegment Patch(this PatchedSegment segment, uint relativeOff return segment.Patch(new AddressFixup(relativeOffset, type, referencedObject)); } + /// + /// Adds an address fixup to the list of patches to apply. + /// + /// The segment to add the patch to. + /// The offset to start writing the address at, relative to the start of the segment. + /// The type of address to write. + /// The offset within the segment to point to, relative to the start of the segment. + /// The patched segment. + public static PatchedSegment Patch(this PatchedSegment segment, uint relativeOffset, AddressFixupType type, + uint symbolOffset) + { + return segment.Patch(new AddressFixup( + relativeOffset, + type, + new Symbol(segment.ToReference((int) symbolOffset)))); + } + /// /// Adds an address fixup to the list of patches to apply. /// From ade3b59690a9c328f3c15a7e59d91c0dde972aed Mon Sep 17 00:00:00 2001 From: Washi Date: Sun, 29 Jan 2023 12:03:42 +0100 Subject: [PATCH 34/35] Add helper ToString methods for segment references. --- src/AsmResolver.PE.File/PESegmentReference.cs | 3 +++ src/AsmResolver/Patching/BytesPatch.cs | 2 +- src/AsmResolver/SegmentReference.cs | 3 +++ src/AsmResolver/Symbol.cs | 3 +++ src/AsmResolver/VirtualAddress.cs | 3 +++ 5 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/AsmResolver.PE.File/PESegmentReference.cs b/src/AsmResolver.PE.File/PESegmentReference.cs index 9eeeaea0a..f05889f7a 100644 --- a/src/AsmResolver.PE.File/PESegmentReference.cs +++ b/src/AsmResolver.PE.File/PESegmentReference.cs @@ -43,5 +43,8 @@ public uint Rva /// public ISegment? GetSegment() => throw new InvalidOperationException(); + + /// + public override string ToString() => $"0x{Rva:X8}"; } } diff --git a/src/AsmResolver/Patching/BytesPatch.cs b/src/AsmResolver/Patching/BytesPatch.cs index 9af11e0a8..6e1233c9b 100644 --- a/src/AsmResolver/Patching/BytesPatch.cs +++ b/src/AsmResolver/Patching/BytesPatch.cs @@ -6,7 +6,7 @@ namespace AsmResolver.Patching /// /// Patches an instance of with a sequence of bytes. /// - [DebuggerDisplay("Patch {NewData.Length} bytes at offset {RelativeOffset}")] + [DebuggerDisplay("Patch {RelativeOffset} with {NewData.Length} bytes")] public sealed class BytesPatch : IPatch { /// diff --git a/src/AsmResolver/SegmentReference.cs b/src/AsmResolver/SegmentReference.cs index ecda7b3e2..5d939db07 100644 --- a/src/AsmResolver/SegmentReference.cs +++ b/src/AsmResolver/SegmentReference.cs @@ -56,5 +56,8 @@ public BinaryStreamReader CreateReader() } ISegment? ISegmentReference.GetSegment() => Segment; + + /// + public override string ToString() => Segment?.ToString() ?? "null"; } } diff --git a/src/AsmResolver/Symbol.cs b/src/AsmResolver/Symbol.cs index ce4759ab0..990cf4ba5 100644 --- a/src/AsmResolver/Symbol.cs +++ b/src/AsmResolver/Symbol.cs @@ -24,5 +24,8 @@ public ISegmentReference Address /// public ISegmentReference GetReference() => Address; + + /// + public override string? ToString() => Address.ToString(); } } diff --git a/src/AsmResolver/VirtualAddress.cs b/src/AsmResolver/VirtualAddress.cs index 7975ba7a6..4fd7546c7 100644 --- a/src/AsmResolver/VirtualAddress.cs +++ b/src/AsmResolver/VirtualAddress.cs @@ -33,5 +33,8 @@ public uint Rva BinaryStreamReader ISegmentReference.CreateReader() => throw new InvalidOperationException(); ISegment? ISegmentReference.GetSegment() => throw new InvalidOperationException(); + + /// + public override string ToString() => $"0x{Rva:X8}"; } } From f8cbe85ce9f1ee8e7da72add25a0f357b26e3878 Mon Sep 17 00:00:00 2001 From: Washi Date: Sun, 29 Jan 2023 12:14:45 +0100 Subject: [PATCH 35/35] Move segment docs to separate 'Core' section. --- docs/{ => core}/segments.rst | 0 docs/index.rst | 9 ++++++++- 2 files changed, 8 insertions(+), 1 deletion(-) rename docs/{ => core}/segments.rst (100%) diff --git a/docs/segments.rst b/docs/core/segments.rst similarity index 100% rename from docs/segments.rst rename to docs/core/segments.rst diff --git a/docs/index.rst b/docs/index.rst index e170599d5..74e1d1ad9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,7 +13,14 @@ Table of Contents: overview faq - segments + + +.. toctree:: + :maxdepth: 1 + :caption: Core API + :name: sec-core + + core/segments .. toctree::