diff --git a/Xamarin.Android.sln b/Xamarin.Android.sln index 55754ff2c4d..078e2122c73 100644 --- a/Xamarin.Android.sln +++ b/Xamarin.Android.sln @@ -129,6 +129,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "proguard-android", "src\pro EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Android.Runtime.NativeAOT", "src\Microsoft.Android.Runtime.NativeAOT\Microsoft.Android.Runtime.NativeAOT.csproj", "{E8831F32-11D7-D42C-E43C-711998BC357A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Android.AppTools", "tools\Microsoft.Android.AppTools\Microsoft.Android.AppTools.csproj", "{CDDB9868-704F-461A-9A9C-BAA0DFBC56D8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "xapp", "tools\xapp\xapp.csproj", "{4CDD887F-AACE-44E3-B179-FB668E87D253}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|AnyCPU = Debug|AnyCPU @@ -359,6 +363,14 @@ Global {E8831F32-11D7-D42C-E43C-711998BC357A}.Debug|AnyCPU.Build.0 = Debug|Any CPU {E8831F32-11D7-D42C-E43C-711998BC357A}.Release|AnyCPU.ActiveCfg = Release|Any CPU {E8831F32-11D7-D42C-E43C-711998BC357A}.Release|AnyCPU.Build.0 = Release|Any CPU + {CDDB9868-704F-461A-9A9C-BAA0DFBC56D8}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU + {CDDB9868-704F-461A-9A9C-BAA0DFBC56D8}.Debug|AnyCPU.Build.0 = Debug|Any CPU + {CDDB9868-704F-461A-9A9C-BAA0DFBC56D8}.Release|AnyCPU.ActiveCfg = Release|Any CPU + {CDDB9868-704F-461A-9A9C-BAA0DFBC56D8}.Release|AnyCPU.Build.0 = Release|Any CPU + {4CDD887F-AACE-44E3-B179-FB668E87D253}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU + {4CDD887F-AACE-44E3-B179-FB668E87D253}.Debug|AnyCPU.Build.0 = Debug|Any CPU + {4CDD887F-AACE-44E3-B179-FB668E87D253}.Release|AnyCPU.ActiveCfg = Release|Any CPU + {4CDD887F-AACE-44E3-B179-FB668E87D253}.Release|AnyCPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -420,6 +432,8 @@ Global {5E806C9F-1B67-4B6B-A6AB-258834250DBB} = {FFCF518F-2A4A-40A2-9174-2EE13B76C723} {5FD0133B-69E5-4474-9B67-9FD1D0150C70} = {FFCF518F-2A4A-40A2-9174-2EE13B76C723} {E8831F32-11D7-D42C-E43C-711998BC357A} = {04E3E11E-B47D-4599-8AFC-50515A95E715} + {CDDB9868-704F-461A-9A9C-BAA0DFBC56D8} = {04E3E11E-B47D-4599-8AFC-50515A95E715} + {4CDD887F-AACE-44E3-B179-FB668E87D253} = {04E3E11E-B47D-4599-8AFC-50515A95E715} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {53A1F287-EFB2-4D97-A4BB-4A5E145613F6} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfig.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfig.cs index 40ea29ee665..52d707d0023 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfig.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfig.cs @@ -44,18 +44,26 @@ sealed class ApplicationConfig public uint number_of_aot_cache_entries; public uint number_of_shared_libraries; +#if !IN_APPTOOLS [NativeAssembler (NumberFormat = LLVMIR.LlvmIrVariableNumberFormat.Hexadecimal)] +#endif public uint android_runtime_jnienv_class_token; +#if !IN_APPTOOLS [NativeAssembler (NumberFormat = LLVMIR.LlvmIrVariableNumberFormat.Hexadecimal)] +#endif public uint jnienv_initialize_method_token; +#if !IN_APPTOOLS [NativeAssembler (NumberFormat = LLVMIR.LlvmIrVariableNumberFormat.Hexadecimal)] +#endif public uint jnienv_registerjninatives_method_token; public uint jni_remapping_replacement_type_count; public uint jni_remapping_replacement_method_index_entry_count; +#if !IN_APPTOOLS [NativeAssembler (NumberFormat = LLVMIR.LlvmIrVariableNumberFormat.Hexadecimal)] +#endif public uint mono_components_mask; public string android_package_name = String.Empty; public bool managed_marshal_methods_lookup_enabled; diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigCLR.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigCLR.cs index 0efed3ccc3c..c4234563491 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigCLR.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigCLR.cs @@ -38,13 +38,19 @@ sealed class ApplicationConfigCLR public uint number_of_aot_cache_entries; public uint number_of_shared_libraries; +#if !IN_APPTOOLS [NativeAssembler (NumberFormat = LLVMIR.LlvmIrVariableNumberFormat.Hexadecimal)] +#endif public uint android_runtime_jnienv_class_token; +#if !IN_APPTOOLS [NativeAssembler (NumberFormat = LLVMIR.LlvmIrVariableNumberFormat.Hexadecimal)] +#endif public uint jnienv_initialize_method_token; +#if !IN_APPTOOLS [NativeAssembler (NumberFormat = LLVMIR.LlvmIrVariableNumberFormat.Hexadecimal)] +#endif public uint jnienv_registerjninatives_method_token; public uint jni_remapping_replacement_type_count; public uint jni_remapping_replacement_method_index_entry_count; diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Assemblies/AssemblyStore.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Assemblies/AssemblyStore.cs new file mode 100644 index 00000000000..6bdc552575b --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Assemblies/AssemblyStore.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; + +using Xamarin.Android.Tools; + +namespace Microsoft.Android.AppTools.Assemblies; + +public class AssemblyStore +{ + static List? emptyAssemblyList; + + readonly ILogger log; + readonly AssemblyStoreExplorer explorer; + + public IList Assemblies => explorer.Assemblies ?? GetEmptyAssemblyList (); + public uint FullFormatVersion { get; private set; } = 0; + public ulong NumberOfAssemblies => explorer.AssemblyCount; + public AndroidTargetArch TargetArchitecture => explorer.TargetArch ?? AndroidTargetArch.None; + public Version? Version { get; private set; } + + internal AssemblyStore (ILogger log, AssemblyStoreExplorer explorer) + { + this.log = log; + this.explorer = explorer; + } + + static List GetEmptyAssemblyList () + { + if (emptyAssemblyList != null) { + return emptyAssemblyList; + } + + emptyAssemblyList = new List (); + return emptyAssemblyList; + } +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Assemblies/AssemblyStoreExplorer.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Assemblies/AssemblyStoreExplorer.cs new file mode 100644 index 00000000000..360a9aef569 --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Assemblies/AssemblyStoreExplorer.cs @@ -0,0 +1,212 @@ +using System; +using System.Collections.Generic; +using System.IO; + +using Xamarin.Android.Tools; +using Xamarin.Tools.Zip; + +namespace Microsoft.Android.AppTools.Assemblies; + +class AssemblyStoreExplorer +{ + readonly AssemblyStoreReader reader; + + public string StorePath { get; } + public AndroidTargetArch? TargetArch { get; } + public uint AssemblyCount { get; } + public uint IndexEntryCount { get; } + public IList? Assemblies { get; } + public IDictionary? AssembliesByName { get; } + public bool Is64Bit { get; } + + protected AssemblyStoreExplorer (Stream storeStream, string path) + { + StorePath = path; + var storeReader = AssemblyStoreReader.Create (storeStream, path); + if (storeReader == null) { + storeStream.Dispose (); + throw new NotSupportedException ($"Format of assembly store '{path}' is unsupported"); + } + + reader = storeReader; + TargetArch = reader.TargetArch; + AssemblyCount = reader.AssemblyCount; + IndexEntryCount = reader.IndexEntryCount; + Assemblies = reader.Assemblies; + Is64Bit = reader.Is64Bit; + + if (Assemblies == null) { + return; + } + + var dict = new Dictionary (StringComparer.Ordinal); + foreach (AssemblyStoreItem item in Assemblies) { + dict.Add (item.Name, item); + } + AssembliesByName = dict.AsReadOnly (); + } + + protected AssemblyStoreExplorer (FileInfo storeInfo) + : this (storeInfo.OpenRead (), storeInfo.FullName) + {} + + public static IList? Open (ILogger log, string inputFile, FileFormat format, FileInfo info) + { + switch (format) { + case FileFormat.Unknown: + log.Debug ($"File '{inputFile}' has an unknown format."); + return null; + + case FileFormat.Zip: + log.Debug ($"File '{inputFile}' is a ZIP archive, but not an Android one."); + return null; + + case FileFormat.AssemblyStore: + case FileFormat.ELF: + return new List { new AssemblyStoreExplorer (info)}; + + case FileFormat.Aab: + return OpenAab (log, info); + + case FileFormat.AabBase: + return OpenAabBase (log, info); + + case FileFormat.Apk: + return OpenApk (log, info); + + default: + log.Debug ($"File '{inputFile}' has an unsupported format '{format}'"); + return null; + } + } + + static IList? OpenAab (ILogger log, FileInfo fi) + { + return OpenCommon ( + log, + fi, + new List> { + StoreReader_V2.AabPaths, + } + ); + } + + static IList? OpenAabBase (ILogger log, FileInfo fi) + { + return OpenCommon ( + log, + fi, + new List> { + StoreReader_V2.AabBasePaths, + } + ); + } + + static IList? OpenApk (ILogger log, FileInfo fi) + { + return OpenCommon ( + log, + fi, + new List> { + StoreReader_V2.ApkPaths, + } + ); + } + + static IList? OpenCommon (ILogger log, FileInfo fi, List> pathLists) + { + using var zip = ZipArchive.Open (fi.FullName, FileMode.Open); + IList? explorers; + bool pathsFound; + + foreach (IList paths in pathLists) { + (explorers, pathsFound) = TryLoad (log, fi, zip, paths); + if (pathsFound) { + return explorers; + } + } + + log.Debug ("Unable to find any blob entries"); + return null; + } + + static (IList? explorers, bool pathsFound) TryLoad (ILogger log, FileInfo fi, ZipArchive zip, IList paths) + { + var ret = new List (); + + foreach (string path in paths) { + if (!zip.ContainsEntry (path)) { + continue; + } + + ZipEntry entry = zip.ReadEntry (path); + var stream = new MemoryStream (); + entry.Extract (stream); + ret.Add (new AssemblyStoreExplorer (stream, $"{fi.FullName}!{path}")); + } + + if (ret.Count == 0) { + return (null, false); + } + + return (ret, true); + } + + public Stream? ReadImageData (AssemblyStoreItem item, bool uncompressIfNeeded = false) + { + return reader.ReadEntryImageData (item, uncompressIfNeeded); + } + + string EnsureCorrectAssemblyName (string assemblyName) + { + assemblyName = Path.GetFileName (assemblyName); + if (reader.NeedsExtensionInName) { + if (!assemblyName.EndsWith (".dll", StringComparison.OrdinalIgnoreCase)) { + return $"{assemblyName}.dll"; + } + } else { + if (assemblyName.EndsWith (".dll", StringComparison.OrdinalIgnoreCase)) { + return Path.GetFileNameWithoutExtension (assemblyName); + } + } + + return assemblyName; + } + + public IList? Find (string assemblyName, AndroidTargetArch? targetArch = null) + { + if (Assemblies == null) { + return null; + } + + assemblyName = EnsureCorrectAssemblyName (assemblyName); + var items = new List (); + foreach (AssemblyStoreItem item in Assemblies) { + if (String.CompareOrdinal (assemblyName, item.Name) != 0) { + continue; + } + + if (targetArch != null && item.TargetArch != targetArch) { + continue; + } + + items.Add (item); + } + + if (items.Count == 0) { + return null; + } + + return items; + } + + public bool Contains (string assemblyName, AndroidTargetArch? targetArch = null) + { + IList? items = Find (assemblyName, targetArch); + if (items == null || items.Count == 0) { + return false; + } + + return true; + } +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Assemblies/AssemblyStoreItem.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Assemblies/AssemblyStoreItem.cs new file mode 100644 index 00000000000..d95d7a1fd7b --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Assemblies/AssemblyStoreItem.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +using Xamarin.Android.Tools; + +namespace Microsoft.Android.AppTools.Assemblies; + +public abstract class AssemblyStoreItem +{ + public string Name { get; } + public IList Hashes { get; } + public bool Is64Bit { get; } + public uint DataOffset { get; protected set; } + public uint DataSize { get; protected set; } + public uint DebugOffset { get; protected set; } + public uint DebugSize { get; protected set; } + public uint ConfigOffset { get; protected set; } + public uint ConfigSize { get; protected set; } + public AndroidTargetArch TargetArch { get; protected set; } + + protected AssemblyStoreItem (string name, bool is64Bit, List hashes) + { + Name = name; + Hashes = hashes.AsReadOnly (); + Is64Bit = is64Bit; + } +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Assemblies/AssemblyStoreReader.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Assemblies/AssemblyStoreReader.cs new file mode 100644 index 00000000000..d42d209805a --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Assemblies/AssemblyStoreReader.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +using Xamarin.Android.Tools; + +namespace Microsoft.Android.AppTools.Assemblies; + +abstract class AssemblyStoreReader +{ + static readonly UTF8Encoding ReaderEncoding = new UTF8Encoding (false); + + protected Stream StoreStream { get; } + + public abstract string Description { get; } + public abstract bool NeedsExtensionInName { get; } + public string StorePath { get; } + + public AndroidTargetArch TargetArch { get; protected set; } = AndroidTargetArch.Arm; + public uint AssemblyCount { get; protected set; } + public uint IndexEntryCount { get; protected set; } + public IList? Assemblies { get; protected set; } + public bool Is64Bit { get; protected set; } + + protected AssemblyStoreReader (Stream store, string path) + { + StoreStream = store; + StorePath = path; + } + + public static AssemblyStoreReader? Create (Stream store, string path) + { + AssemblyStoreReader? reader = MakeReaderReady (new StoreReader_V2 (store, path)); + if (reader != null) { + return reader; + } + + return null; + } + + static AssemblyStoreReader? MakeReaderReady (AssemblyStoreReader reader) + { + if (!reader.IsSupported ()) { + return null; + } + + reader.Prepare (); + return reader; + } + + protected BinaryReader CreateReader () => new BinaryReader (StoreStream, ReaderEncoding, leaveOpen: true); + + protected abstract bool IsSupported (); + protected abstract void Prepare (); + protected abstract ulong GetStoreStartDataOffset (); + + public Stream ReadEntryImageData (AssemblyStoreItem entry, bool uncompressIfNeeded = false) + { + ulong startOffset = GetStoreStartDataOffset (); + StoreStream.Seek ((uint)startOffset + entry.DataOffset, SeekOrigin.Begin); + var stream = new MemoryStream (); + + if (uncompressIfNeeded) { + throw new NotImplementedException (); + } + + const long BufferSize = 65535; + byte[] buffer = Utils.BytePool.Rent ((int)BufferSize); + long remainingToRead = entry.DataSize; + + while (remainingToRead > 0) { + int nread = StoreStream.Read (buffer, 0, (int)Math.Min (BufferSize, remainingToRead)); + stream.Write (buffer, 0, nread); + remainingToRead -= (long)nread; + } + stream.Flush (); + stream.Seek (0, SeekOrigin.Begin); + + return stream; + } +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Assemblies/StoreReader_V2.Classes.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Assemblies/StoreReader_V2.Classes.cs new file mode 100644 index 00000000000..bcdb9e59dc0 --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Assemblies/StoreReader_V2.Classes.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; + +using Xamarin.Android.Tools; + +namespace Microsoft.Android.AppTools.Assemblies; + +partial class StoreReader_V2 +{ + sealed class Header + { + public const uint NativeSize = 5 * sizeof (uint); + + public readonly uint magic; + public readonly uint version; + public readonly uint entry_count; + public readonly uint index_entry_count; + + // Index size in bytes + public readonly uint index_size; + + public Header (uint magic, uint version, uint entry_count, uint index_entry_count, uint index_size) + { + this.magic = magic; + this.version = version; + this.entry_count = entry_count; + this.index_entry_count = index_entry_count; + this.index_size = index_size; + } + } + + sealed class IndexEntry + { + public readonly ulong name_hash; + public readonly uint descriptor_index; + + public IndexEntry (ulong name_hash, uint descriptor_index) + { + this.name_hash = name_hash; + this.descriptor_index = descriptor_index; + } + } + + sealed class EntryDescriptor + { + public uint mapping_index; + + public uint data_offset; + public uint data_size; + + public uint debug_data_offset; + public uint debug_data_size; + + public uint config_data_offset; + public uint config_data_size; + } + + sealed class StoreItem_V2 : AssemblyStoreItem + { + public StoreItem_V2 (AndroidTargetArch targetArch, string name, bool is64Bit, List indexEntries, EntryDescriptor descriptor) + : base (name, is64Bit, IndexToHashes (indexEntries)) + { + DataOffset = descriptor.data_offset; + DataSize = descriptor.data_size; + DebugOffset = descriptor.debug_data_offset; + DebugSize = descriptor.debug_data_size; + ConfigOffset = descriptor.config_data_offset; + ConfigSize = descriptor.config_data_size; + TargetArch = targetArch; + } + + static List IndexToHashes (List indexEntries) + { + var ret = new List (); + foreach (IndexEntry ie in indexEntries) { + ret.Add (ie.name_hash); + } + + return ret; + } + } + + sealed class TemporaryItem + { + public readonly string Name; + public readonly List IndexEntries = new List (); + public readonly EntryDescriptor Descriptor; + + public TemporaryItem (string name, EntryDescriptor descriptor) + { + Name = name; + Descriptor = descriptor; + } + } +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Assemblies/StoreReader_V2.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Assemblies/StoreReader_V2.cs new file mode 100644 index 00000000000..ead628f7d39 --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Assemblies/StoreReader_V2.cs @@ -0,0 +1,218 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +using Xamarin.Android.Tools; +using Xamarin.Android.Tasks; + +namespace Microsoft.Android.AppTools.Assemblies; + +partial class StoreReader_V2 : AssemblyStoreReader +{ + // Bit 31 is set for 64-bit platforms, cleared for the 32-bit ones + const uint ASSEMBLY_STORE_FORMAT_VERSION_64BIT = 0x80000002; // Must match the ASSEMBLY_STORE_FORMAT_VERSION native constant + const uint ASSEMBLY_STORE_FORMAT_VERSION_32BIT = 0x00000002; + const uint ASSEMBLY_STORE_FORMAT_VERSION_MASK = 0xF0000000; + + const uint ASSEMBLY_STORE_ABI_AARCH64 = 0x00010000; + const uint ASSEMBLY_STORE_ABI_ARM = 0x00020000; + const uint ASSEMBLY_STORE_ABI_X64 = 0x00030000; + const uint ASSEMBLY_STORE_ABI_X86 = 0x00040000; + const uint ASSEMBLY_STORE_ABI_MASK = 0x00FF0000; + + public override string Description => "Assembly store v2"; + public override bool NeedsExtensionInName => true; + + public static IList ApkPaths { get; } + public static IList AabPaths { get; } + public static IList AabBasePaths { get; } + + readonly HashSet supportedVersions; + + Header? header; + ulong elfOffset = 0; + + static StoreReader_V2 () + { + var paths = new List { + GetArchPath (AndroidTargetArch.Arm64), + GetArchPath (AndroidTargetArch.Arm), + GetArchPath (AndroidTargetArch.X86_64), + GetArchPath (AndroidTargetArch.X86), + }; + ApkPaths = paths.AsReadOnly (); + AabBasePaths = ApkPaths; + + const string AabBaseDir = "base"; + paths = new List { + GetArchPath (AndroidTargetArch.Arm64, AabBaseDir), + GetArchPath (AndroidTargetArch.Arm, AabBaseDir), + GetArchPath (AndroidTargetArch.X86_64, AabBaseDir), + GetArchPath (AndroidTargetArch.X86, AabBaseDir), + }; + AabPaths = paths.AsReadOnly (); + + string GetArchPath (AndroidTargetArch arch, string? root = null) + { + const string LibDirName = "lib"; + + string abi = MonoAndroidHelper.ArchToAbi (arch); + var parts = new List (); + if (!String.IsNullOrEmpty (root)) { + parts.Add (LibDirName); + } else { + root = LibDirName; + } + parts.Add (abi); + parts.Add (GetBlobName (abi)); + + return MonoAndroidHelper.MakeZipArchivePath (root, parts); + } + } + + public StoreReader_V2 (Stream store, string path) + : base (store, path) + { + supportedVersions = new HashSet { + ASSEMBLY_STORE_FORMAT_VERSION_64BIT | ASSEMBLY_STORE_ABI_AARCH64, + ASSEMBLY_STORE_FORMAT_VERSION_64BIT | ASSEMBLY_STORE_ABI_X64, + ASSEMBLY_STORE_FORMAT_VERSION_32BIT | ASSEMBLY_STORE_ABI_ARM, + ASSEMBLY_STORE_FORMAT_VERSION_32BIT | ASSEMBLY_STORE_ABI_X86, + }; + } + + static string GetBlobName (string abi) => $"libassemblies.{abi}.blob.so"; + + protected override ulong GetStoreStartDataOffset () => elfOffset; + + protected override bool IsSupported () + { + StoreStream.Seek (0, SeekOrigin.Begin); + using var reader = CreateReader (); + + uint magic = reader.ReadUInt32 (); + if (magic == Utils.ELF_MAGIC) { + ELFPayloadError error; + (elfOffset, _, error) = Utils.FindELFPayloadSectionOffsetAndSize (StoreStream); + + if (error != ELFPayloadError.None) { + string message = error switch { + ELFPayloadError.NotELF => $"Store '{StorePath}' is not a valid ELF binary", + ELFPayloadError.LoadFailed => $"Store '{StorePath}' could not be loaded", + ELFPayloadError.NotSharedLibrary => $"Store '{StorePath}' is not a shared ELF library", + ELFPayloadError.NotLittleEndian => $"Store '{StorePath}' is not a little-endian ELF image", + ELFPayloadError.NoPayloadSection => $"Store '{StorePath}' does not contain the 'payload' section", + _ => $"Unknown ELF payload section error for store '{StorePath}': {error}" + }; + Utils.Log?.Debug (message); + } else if (elfOffset >= 0) { + StoreStream.Seek ((long)elfOffset, SeekOrigin.Begin); + magic = reader.ReadUInt32 (); + } + } + + if (magic != Utils.ASSEMBLY_STORE_MAGIC) { + Utils.Log?.Debug ($"Store '{StorePath}' has invalid header magic number."); + return false; + } + + uint version = reader.ReadUInt32 (); + if (!supportedVersions.Contains (version)) { + Utils.Log?.Debug ($"Store '{StorePath}' has unsupported version 0x{version:x}"); + return false; + } + + uint entry_count = reader.ReadUInt32 (); + uint index_entry_count = reader.ReadUInt32 (); + uint index_size = reader.ReadUInt32 (); + + header = new Header (magic, version, entry_count, index_entry_count, index_size); + return true; + } + + protected override void Prepare () + { + if (header == null) { + throw new InvalidOperationException ("Internal error: header not set, was IsSupported() called?"); + } + + TargetArch = (header.version & ASSEMBLY_STORE_ABI_MASK) switch { + ASSEMBLY_STORE_ABI_AARCH64 => AndroidTargetArch.Arm64, + ASSEMBLY_STORE_ABI_ARM => AndroidTargetArch.Arm, + ASSEMBLY_STORE_ABI_X64 => AndroidTargetArch.X86_64, + ASSEMBLY_STORE_ABI_X86 => AndroidTargetArch.X86, + _ => throw new NotSupportedException ($"Unsupported ABI in store version: 0x{header.version:x}") + }; + + Is64Bit = (header.version & ASSEMBLY_STORE_FORMAT_VERSION_MASK) != 0; + AssemblyCount = header.entry_count; + IndexEntryCount = header.index_entry_count; + + StoreStream.Seek ((long)elfOffset + Header.NativeSize, SeekOrigin.Begin); + using var reader = CreateReader (); + + var index = new List (); + for (uint i = 0; i < header.index_entry_count; i++) { + ulong name_hash; + if (Is64Bit) { + name_hash = reader.ReadUInt64 (); + } else { + name_hash = (ulong)reader.ReadUInt32 (); + } + + uint descriptor_index = reader.ReadUInt32 (); + index.Add (new IndexEntry (name_hash, descriptor_index)); + } + + var descriptors = new List (); + for (uint i = 0; i < header.entry_count; i++) { + uint mapping_index = reader.ReadUInt32 (); + uint data_offset = reader.ReadUInt32 (); + uint data_size = reader.ReadUInt32 (); + uint debug_data_offset = reader.ReadUInt32 (); + uint debug_data_size = reader.ReadUInt32 (); + uint config_data_offset = reader.ReadUInt32 (); + uint config_data_size = reader.ReadUInt32 (); + + var desc = new EntryDescriptor { + mapping_index = mapping_index, + data_offset = data_offset, + data_size = data_size, + debug_data_offset = debug_data_offset, + debug_data_size = debug_data_size, + config_data_offset = config_data_offset, + config_data_size = config_data_size, + }; + descriptors.Add (desc); + } + + var names = new List (); + for (uint i = 0; i < header.entry_count; i++) { + uint name_length = reader.ReadUInt32 (); + byte[] name_bytes = reader.ReadBytes ((int)name_length); + names.Add (Encoding.UTF8.GetString (name_bytes)); + } + + var tempItems = new Dictionary (); + foreach (IndexEntry ie in index) { + if (!tempItems.TryGetValue (ie.descriptor_index, out TemporaryItem? item)) { + item = new TemporaryItem (names[(int)ie.descriptor_index], descriptors[(int)ie.descriptor_index]); + tempItems.Add (ie.descriptor_index, item); + } + item.IndexEntries.Add (ie); + } + + if (tempItems.Count != descriptors.Count) { + throw new InvalidOperationException ($"Assembly store '{StorePath}' index is corrupted."); + } + + var storeItems = new List (); + foreach (var kvp in tempItems) { + TemporaryItem ti = kvp.Value; + var item = new StoreItem_V2 (TargetArch, ti.Name, Is64Bit, ti.IndexEntries, ti.Descriptor); + storeItems.Add (item); + } + Assemblies = storeItems.AsReadOnly (); + } +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Native/AnELF.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Native/AnELF.cs new file mode 100644 index 00000000000..956facf8082 --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Native/AnELF.cs @@ -0,0 +1,332 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; + +using ELFSharp; +using ELFSharp.ELF; +using ELFSharp.ELF.Sections; + +namespace Microsoft.Android.AppTools.Native; + +abstract class AnELF +{ + protected static readonly byte[] EmptyArray = Array.Empty (); + + const string DynsymSectionName = ".dynsym"; + const string SymtabSectionName = ".symtab"; + const string RodataSectionName = ".rodata"; + + ISymbolTable dynamicSymbolsSection; + ISection rodataSection; + ISymbolTable? symbolsSection; + string filePath; + IELF elf; + Stream elfStream; + + protected ISymbolTable DynSymSection => dynamicSymbolsSection; + protected ISymbolTable? SymSection => symbolsSection; + protected ISection RodataSection => rodataSection; + public IELF AnyELF => elf; + protected Stream ELFStream => elfStream; + protected ILogger Log { get; } + + public string FilePath => filePath; + public int PointerSize => Is64Bit ? 8 : 4; + + public abstract bool Is64Bit { get; } + public abstract string Bitness { get; } + + protected AnELF (ILogger log, Stream stream, string filePath, IELF elf, ISymbolTable dynsymSection, ISection rodataSection, ISymbolTable? symSection) + { + Log = log; + this.filePath = filePath; + this.elf = elf; + elfStream = stream; + dynamicSymbolsSection = dynsymSection; + this.rodataSection = rodataSection; + symbolsSection = symSection; + } + + public ISymbolEntry? GetSymbol (string symbolName) + { + ISymbolEntry? symbol = null; + + if (symbolsSection != null) { + symbol = GetSymbol (symbolsSection, symbolName); + } + + if (symbol == null) { + symbol = GetSymbol (dynamicSymbolsSection, symbolName); + } + + return symbol; + } + + protected static ISymbolEntry? GetSymbol (ISymbolTable symtab, string symbolName) + { + return symtab.Entries.Where (entry => String.Compare (entry.Name, symbolName, StringComparison.Ordinal) == 0).FirstOrDefault (); + } + + protected static SymbolEntry? GetSymbol (SymbolTable symtab, T symbolValue) where T: struct + { + return symtab.Entries.Where (entry => entry.Value.Equals (symbolValue)).FirstOrDefault (); + } + + public bool HasSymbol (string symbolName) + { + return GetSymbol (symbolName) != null; + } + + public byte[] GetData (string symbolName) + { + return GetData (symbolName, out ISymbolEntry? _); + } + + public byte[] GetData (string symbolName, out ISymbolEntry? symbolEntry) + { + Log.DebugLine ($"Looking for symbol: {symbolName}"); + symbolEntry = GetSymbol (symbolName); + if (symbolEntry == null) + return EmptyArray; + + if (Is64Bit) { + var symbol64 = symbolEntry as SymbolEntry; + if (symbol64 == null) + throw new InvalidOperationException ($"Symbol '{symbolName}' is not a valid 64-bit symbol"); + return GetData (symbol64); + } + + var symbol32 = symbolEntry as SymbolEntry; + if (symbol32 == null) + throw new InvalidOperationException ($"Symbol '{symbolName}' is not a valid 32-bit symbol"); + + return GetData (symbol32); + } + + public string? GetStringFromPointer (ISymbolEntry symbolEntry) + { + return GetStringFromPointerField (symbolEntry, 0); + } + + public abstract string? GetStringFromPointerField (ISymbolEntry symbolEntry, ulong pointerFieldOffset); + public abstract byte[] GetData (ulong symbolValue, ulong size); + + public string? GetASCIIZ (ulong symbolValue) + { + return GetASCIIZ (GetData (symbolValue, 0), 0); + } + + public string? GetASCIIZ (byte[] data, ulong offset) + { + if (offset >= (ulong)data.LongLength) { + Log.DebugLine ("Not enough data to retrieve an ASCIIZ string"); + return null; + } + + int count = data.Length; + + for (ulong i = offset; i < (ulong)data.LongLength; i++) { + if (data[i] == 0) { + count = (int)(i - offset); + break; + } + } + + return Encoding.ASCII.GetString (data, (int)offset, count); + } + + public ulong GetPaddedSize (ulong sizeSoFar) => NativeHelpers.GetPaddedSize (sizeSoFar, Is64Bit); + + public ulong GetPaddedSize (ulong sizeSoFar, S _) + { + return GetPaddedSize (sizeSoFar); + } + + protected virtual byte[] GetData (SymbolEntry symbol) + { + throw new NotSupportedException (); + } + + protected virtual byte[] GetData (SymbolEntry symbol) + { + throw new NotSupportedException (); + } + + protected byte[] GetData (ISymbolEntry symbol, ulong size, ulong offset) + { + return GetData (symbol.PointedSection, size, offset); + } + + protected byte[] GetData (ISection section, ulong size, ulong offset) + { + ulong sectionOffset = (elf.Class == Class.Bit64 ? ((Section)section).Offset : ((Section)section).Offset); + Log.VerboseLine ($"AnELF.GetData: section == {section.Name}; type == {section.Type}; flags == {section.Flags}; offset into binary == {sectionOffset}; size == {size}"); + byte[] data = section.GetContents (); + + Log.VerboseLine ($" section data length: {data.Length} (long: {data.LongLength})"); + Log.VerboseLine ($" offset into section: {offset}; symbol data length: {size}"); + if ((ulong)data.LongLength < (offset + size)) { + return EmptyArray; + } + + if (size == 0) + size = (ulong)data.Length - offset; + + var ret = new byte[size]; + checked { + Array.Copy (data, (int)offset, ret, 0, (int)size); + } + + return ret; + } + + public uint GetUInt32 (string symbolName) + { + return GetUInt32 (GetData (symbolName), 0, symbolName); + } + + public uint GetUInt32 (ulong symbolValue) + { + return GetUInt32 (GetData (symbolValue, 4), 0, symbolValue.ToString ()); + } + + protected uint GetUInt32 (byte[] data, ulong offset, string symbolName) + { + if (data.Length < 4) { + throw new InvalidOperationException ($"Data not big enough to retrieve a 32-bit integer from it (need 4, got {data.Length})"); + } + + return BitConverter.ToUInt32 (GetIntegerData (4, data, offset, symbolName), 0); + } + + public ulong GetUInt64 (string symbolName) + { + return GetUInt64 (GetData (symbolName), 0, symbolName); + } + + public ulong GetUInt64 (ulong symbolValue) + { + return GetUInt64 (GetData (symbolValue, 8), 0, symbolValue.ToString ()); + } + + protected ulong GetUInt64 (byte[] data, ulong offset, string symbolName) + { + if (data.Length < 8) + throw new InvalidOperationException ("Data not big enough to retrieve a 64-bit integer from it"); + return BitConverter.ToUInt64 (GetIntegerData (8, data, offset, symbolName), 0); + } + + byte[] GetIntegerData (uint size, byte[] data, ulong offset, string symbolName) + { + if ((ulong)data.LongLength < (offset + size)) { + string bits = size == 4 ? "32" : "64"; + throw new InvalidOperationException ($"Unable to read UInt{bits} value for symbol '{symbolName}': data not long enough"); + } + + byte[] ret = new byte[size]; + Array.Copy (data, (int)offset, ret, 0, ret.Length); + Endianess myEndianness = BitConverter.IsLittleEndian ? Endianess.LittleEndian : Endianess.BigEndian; + if (AnyELF.Endianess != myEndianness) { + Array.Reverse (ret); + } + + return ret; + } + + public static bool TryLoad (ILogger log, string filePath, out AnELF? anElf) + { + using var fs = File.OpenRead (filePath); + return TryLoad (log, fs, filePath, out anElf); + } + + public static bool TryLoad (ILogger log, Stream stream, string filePath, out AnELF? anElf) + { + anElf = null; + Class elfClass = ELFReader.CheckELFType (stream); + if (elfClass == Class.NotELF) { + log.WarningLine ($"AnELF.TryLoad: {filePath} is not an ELF binary"); + return false; + } + + IELF elf = ELFReader.Load (stream, shouldOwnStream: false); + + if (elf.Type != FileType.SharedObject) { + log.WarningLine ($"AnELF.TryLoad: {filePath} is not a shared library"); + return false; + } + + if (elf.Endianess != Endianess.LittleEndian) { + log.WarningLine ($"AnELF.TryLoad: {filePath} is not a little-endian binary"); + return false; + } + + bool is64; + switch (elf.Machine) { + case Machine.ARM: + case Machine.Intel386: + is64 = false; + + break; + + case Machine.AArch64: + case Machine.AMD64: + is64 = true; + + break; + + default: + log.WarningLine ($"{filePath} is for an unsupported machine type {elf.Machine}"); + return false; + } + + ISymbolTable? symtab = GetSymbolTable (elf, DynsymSectionName); + if (symtab == null) { + log.WarningLine ($"{filePath} does not contain dynamic symbol section '{DynsymSectionName}'"); + return false; + } + ISymbolTable dynsym = symtab; + + ISection? sec = GetSection (elf, RodataSectionName); + if (sec == null) { + log.WarningLine ("${filePath} does not contain read-only data section ('{RodataSectionName}')"); + return false; + } + ISection rodata = sec; + + ISymbolTable? sym = GetSymbolTable (elf, SymtabSectionName); + + if (is64) { + anElf = new ELF64 (log, stream, filePath, elf, dynsym, rodata, sym); + } else { + anElf = new ELF32 (log, stream, filePath, elf, dynsym, rodata, sym); + } + + log.DebugLine ($"AnELF.TryLoad: {filePath} is a {anElf.Bitness}-bit ELF binary ({elf.Machine})"); + return true; + } + + protected static ISymbolTable? GetSymbolTable (IELF elf, string sectionName) + { + ISection? section = GetSection (elf, sectionName); + if (section == null) { + return null; + } + + var symtab = section as ISymbolTable; + if (symtab == null) { + return null; + } + + return symtab; + } + + protected static ISection? GetSection (IELF elf, string sectionName) + { + if (!elf.TryGetSection (sectionName, out ISection section)) { + return null; + } + + return section; + } +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Native/DSOCache.MonoVM.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Native/DSOCache.MonoVM.cs new file mode 100644 index 00000000000..0b3fa2c464d --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Native/DSOCache.MonoVM.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.IO; + +using ELFSharp.ELF.Sections; + +namespace Microsoft.Android.AppTools.Native; + +// Must be kept in sync with `struct DSOCacheEntry` in src/native/mono/monodroid/xamarin-app.hh +sealed class DSOCacheEntryMonoVM +{ + public readonly ulong hash; + public readonly bool ignore; + public readonly string name; + public readonly IntPtr handle = IntPtr.Zero; + + public DSOCacheEntryMonoVM (ILogger log, BinaryReader reader, AnELF elf, ISymbolEntry symbolEntry) + { + bool is64Bit = elf.Is64Bit; + ulong sizeSoFar = 0; + ulong entryOffset = (ulong)reader.BaseStream.Position; + + sizeSoFar += NativeHelpers.ReadField (reader, ref hash, sizeSoFar, is64Bit); + sizeSoFar += NativeHelpers.ReadField (reader, ref ignore, sizeSoFar, is64Bit); + + ulong pointerOffset = NativeHelpers.GetPadding (sizeSoFar, is64Bit) + sizeSoFar + entryOffset; + name = elf.GetStringFromPointerField (symbolEntry, pointerOffset) ?? Constants.UnableToLoadDataForPointer; + sizeSoFar += NativeHelpers.ReadField (reader, ref name, sizeSoFar, is64Bit); + sizeSoFar += NativeHelpers.ReadField (reader, ref handle, sizeSoFar, is64Bit); + } +} + +class DSOCacheMonoVM +{ + public List Entries { get; } = new List (); + + public DSOCacheMonoVM (ILogger log, byte[] data, AnELF elf, ISymbolEntry symbolEntry) + { + using var stream = new MemoryStream (data); + using var reader = new BinaryReader (stream); + + while (stream.Position < stream.Length) { + Entries.Add (new DSOCacheEntryMonoVM (log, reader, elf, symbolEntry)); + } + } +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Native/ELF32.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Native/ELF32.cs new file mode 100644 index 00000000000..1aa2be2d2f9 --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Native/ELF32.cs @@ -0,0 +1,92 @@ +using System; +using System.IO; + +using ELFSharp; +using ELFSharp.ELF; +using ELFSharp.ELF.Sections; + +namespace Microsoft.Android.AppTools.Native; + +class ELF32 : AnELF +{ + public override bool Is64Bit => false; + public override string Bitness => "32"; + + SymbolTable DynamicSymbols => (SymbolTable)DynSymSection; + SymbolTable? Symbols => (SymbolTable?)SymSection; + Section Rodata => (Section)RodataSection; + ELF ELF => (ELF)AnyELF; + + public ELF32 (ILogger log, Stream stream, string filePath, IELF elf, ISymbolTable dynsymSection, ISection rodataSection, ISymbolTable? symSection) + : base (log, stream, filePath, elf, dynsymSection, rodataSection, symSection) + {} + + public override string GetStringFromPointerField(ISymbolEntry symbolEntry, ulong pointerFieldOffset) + { + throw new NotImplementedException(); + } + + public override byte[] GetData (ulong symbolValue, ulong size = 0) + { + checked { + return GetData ((uint)symbolValue, size); + } + } + + byte[] GetData (uint symbolValue, ulong size) + { + Log.Debug ($"ELF64.GetData: Looking for symbol value {symbolValue:X08}"); + + SymbolEntry? symbol = GetSymbol (DynamicSymbols, symbolValue); + if (symbol == null && Symbols != null) { + symbol = GetSymbol (Symbols, symbolValue); + } + + if (symbol != null) { + Log.Debug ($"ELF64.GetData: found in section {symbol.PointedSection.Name}"); + return GetData (symbol); + } + + Section section = FindSectionForValue (symbolValue); + + Log.Debug ($"ELF64.GetData: found in section {section} {section.Name}"); + return GetData (section, size, OffsetInSection (section, symbolValue)); + } + + protected override byte[] GetData (SymbolEntry symbol) + { + ulong offset = symbol.Value - symbol.PointedSection.LoadAddress; + return GetData (symbol, symbol.Size, offset); + } + + Section FindSectionForValue (uint symbolValue) + { + Log.Debug ($"FindSectionForValue ({symbolValue:X08})"); + int nsections = ELF.Sections.Count; + + for (int i = nsections - 1; i >= 0; i--) { + Section section = ELF.GetSection (i); + if (section.Type != SectionType.ProgBits) + continue; + + if (SectionInRange (section, symbolValue)) + return section; + } + + throw new InvalidOperationException ($"Section matching symbol value {symbolValue:X08} cannot be found"); + } + + bool SectionInRange (Section section, uint symbolValue) + { + Log.Debug ($"SectionInRange ({section.Name}, {symbolValue:X08})"); + Log.Debug ($" address == {section.LoadAddress:X08}; size == {section.Size}; last address = {section.LoadAddress + section.Size:X08}"); + Log.Debug ($" symbolValue >= section.LoadAddress? {symbolValue >= section.LoadAddress}"); + Log.Debug ($" (section.LoadAddress + section.Size) >= symbolValue? {(section.LoadAddress + section.Size) >= symbolValue}"); + return symbolValue >= section.LoadAddress && (section.LoadAddress + section.Size) >= symbolValue; + } + + ulong OffsetInSection (Section section, uint symbolValue) + { + return symbolValue - section.LoadAddress; + } +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Native/ELF64.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Native/ELF64.cs new file mode 100644 index 00000000000..00f67279d2b --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Native/ELF64.cs @@ -0,0 +1,206 @@ +using System; +using System.IO; + +using ELFSharp.ELF; +using ELFSharp.ELF.Sections; + +namespace Microsoft.Android.AppTools.Native; + +class ELF64 : AnELF +{ + public override bool Is64Bit => true; + public override string Bitness => "64"; + + SymbolTable DynamicSymbols => (SymbolTable)DynSymSection; + SymbolTable? Symbols => (SymbolTable?)SymSection; + Section Rodata => (Section)RodataSection; + ELF ELF => (ELF)AnyELF; + + public ELF64 (ILogger log, Stream stream, string filePath, IELF elf, ISymbolTable dynsymSection, ISection rodataSection, ISymbolTable? symSection) + : base (log, stream, filePath, elf, dynsymSection, rodataSection, symSection) + {} + + public override string? GetStringFromPointerField (ISymbolEntry symbolEntry, ulong pointerFieldOffset) + { + var symbol = symbolEntry as SymbolEntry; + if (symbol == null) { + throw new InvalidOperationException ($"Expected a 64-bit symbol entry, got {symbolEntry}"); + } + + switch (ELF.Machine) { + case Machine.AArch64: + return GetStringFromPointerField_ARM64 (symbol, pointerFieldOffset); + + case Machine.AMD64: + return GetStringFromPointerField_X64 (symbol, pointerFieldOffset); + + default: + throw new InvalidOperationException ($"Unsupported ELF machine type '{ELF.Machine}'"); + } + } + + string? GetStringFromPointerField_ARM64 (SymbolEntry symbolEntry, ulong pointerFieldOffset) + { + return GetStringFromPointerField_Common ( + symbolEntry, + pointerFieldOffset, + (ELF64_Rela rela) => { + // We only support R_AARCH64_RELATIVE right now + return (RelocationTypeARM64)rela.r_info == RelocationTypeARM64.R_AARCH64_RELATIVE; + } + ); + } + + string? GetStringFromPointerField_X64 (SymbolEntry symbolEntry, ulong pointerFieldOffset) + { + return GetStringFromPointerField_Common ( + symbolEntry, + pointerFieldOffset, + (ELF64_Rela rela) => { + // We only support R_X86_64_RELATIVE right now + return (RelocationTypeX64)rela.r_info == RelocationTypeX64.R_X86_64_RELATIVE; + } + ); + } + + string? GetStringFromPointerField_Common (SymbolEntry symbolEntry, ulong pointerFieldOffset, Func validRelocation) + { + Log.VerboseLine ($"[ARM64] Getting string from a pointer field in symbol '{symbolEntry.Name}', at offset {pointerFieldOffset} into the structure"); + + if (symbolEntry.PointedSection.Type != SectionType.ProgBits || !symbolEntry.PointedSection.Flags.HasFlag (SectionFlags.Writable)) { + Log.VerboseLine (" Symbol section isn't a writable data one, pointers require a writable section to apply relocations"); + Log.VerboseLine ($" Section info: {symbolEntry.PointedSection}"); + return null; + } + + // Steps: + // + // 1. Calculate address of the field in the symbol data: [symbol section virtual address] + [symbol offset into section] + pointerFieldOffset + // ELFSharp does part of the job for us - symbol's value is its virtual address + ulong pointerVA = symbolEntry.Value + pointerFieldOffset; + Log.VerboseLine ($" Section address == 0x{symbolEntry.PointedSection.LoadAddress:x}; offset == 0x{symbolEntry.PointedSection.Offset:x}"); + Log.VerboseLine ($" Symbol entry value == 0x{symbolEntry.Value:x}"); + Log.VerboseLine ($" Virtual address of the pointer: 0x{pointerVA:x} ({pointerVA})"); + + // 2. Find the .rela.dyn section + const string RelaDynSectionName = ".rela.dyn"; + Section? relaDynSection = ELF.GetSection (RelaDynSectionName); + Log.VerboseLine ($" Relocation section: {Utils.ToStringOrNull (relaDynSection)}"); + if (relaDynSection == null) { + Log.VerboseLine ($" Section '{RelaDynSectionName}' not found"); + return null; + } + + // Make sure section type is what we need and expect + if (relaDynSection.Type != SectionType.RelocationAddends) { + Log.VerboseLine ($" Section '{RelaDynSectionName}' has invalid type. Expected {SectionType.RelocationAddends}, got {relaDynSection.Type}"); + return null; + } + var relocationReader = new RelocationSectionAddend64 (relaDynSection); + + // 3. Find relocation entry with offset matching the address calculated in 1. Relocation entry should have code 0x403 (1027) - R_AARCH64_RELATIVE + if (!relocationReader.Entries.TryGetValue (pointerVA, out ELF64_Rela? relocation) || relocation == null) { + Log.VerboseLine ($" Relocation for pointer address 0x{pointerVA:x} not found"); + return null; + } + Log.VerboseLine ($" Found relocation: {relocation}"); + + if (!validRelocation (relocation)) { + // Yell, so that we can fix it + throw new NotSupportedException ($"AArch64 relocation type {relocation.r_info} not supported. Please report at https://github.com/xamarin/xamarin.android/issues/"); + } + + // 4. Read relocation entry (see elf(5) for Elf32_Rela and Elf64_Rela structures) and get the addend value + ulong addend = (ulong)relocation.r_addend; + + // 5. Find section the addend from 4. falls within + Section? pointeeSection = FindSectionForValue (addend); + if (pointeeSection == null) { + Log.DebugLine ($" Unable to find section in which pointee 0x{addend:x} resides"); + return null; + } + Log.VerboseLine ($" Pointee 0x{addend:x} falls within section {pointeeSection}"); + + // 6. Read that section data + byte[] data = pointeeSection.GetContents (); + + // 7. Subtract section address from the addend, this will give offset into the section + ulong addendSectionOffset = addend - pointeeSection.LoadAddress; + Log.VerboseLine ($" Pointee offset into section data == 0x{addendSectionOffset:x} ({addendSectionOffset})"); + + // 8. Read ASCIIZ data from the offset obtained in 7. + return GetASCIIZ (data, addendSectionOffset); + } + + public override byte[] GetData (ulong symbolValue, ulong size = 0) + { + Log.VerboseLine ($"ELF64.GetData: Looking for symbol value {symbolValue:X08}"); + + SymbolEntry? symbol = GetSymbol (DynamicSymbols, symbolValue); + if (symbol == null && Symbols != null) { + symbol = GetSymbol (Symbols, symbolValue); + } + + if (symbol != null) { + Log.VerboseLine ($"ELF64.GetData: found in section {symbol.PointedSection.Name}"); + if (symbol.Size == 0) { + return EmptyArray; + } + + return GetData (symbol); + } + + Section section = FindProgBitsSectionForValue (symbolValue); + + Log.VerboseLine ($"ELF64.GetData: found in section {section} {section.Name}"); + return GetData (section, size, OffsetInSection (section, symbolValue)); + } + + protected override byte[] GetData (SymbolEntry symbol) + { + if (symbol.Size == 0) { + return EmptyArray; + } + + return GetData (symbol, symbol.Size, OffsetInSection (symbol.PointedSection, symbol.Value)); + } + + Section FindProgBitsSectionForValue (ulong symbolValue) + { + return FindSectionForValue (symbolValue, SectionType.ProgBits) ?? throw new InvalidOperationException ($"Section matching symbol value {symbolValue:X08} cannot be found"); + } + + Section? FindSectionForValue (ulong symbolValue, SectionType requiredType = SectionType.Null) + { + Log.VerboseLine ($"FindSectionForValue ({symbolValue:X08}, {requiredType})"); + int nsections = ELF.Sections.Count; + + for (int i = nsections - 1; i >= 0; i--) { + Section section = ELF.GetSection (i); + if (requiredType != SectionType.Null && section.Type != requiredType) { + continue; + } + + if (SectionInRange (section, symbolValue)) { + return section; + } + } + + Log.DebugLine ($"Section matching symbol value {symbolValue:X08} cannot be found"); + return null; + } + + bool SectionInRange (Section section, ulong symbolValue) + { + Log.VerboseLine ($"SectionInRange ({section.Name}, {symbolValue:X08})"); + Log.VerboseLine ($" address == {section.LoadAddress:X08}; size == {section.Size}; last address = {section.LoadAddress + section.Size:X08}"); + Log.VerboseLine ($" symbolValue >= section.LoadAddress? {symbolValue >= section.LoadAddress}"); + Log.VerboseLine ($" (section.LoadAddress + section.Size) >= symbolValue? {(section.LoadAddress + section.Size) >= symbolValue}"); + return symbolValue >= section.LoadAddress && (section.LoadAddress + section.Size) >= symbolValue; + } + + ulong OffsetInSection (Section section, ulong symbolValue) + { + return symbolValue - section.LoadAddress; + } +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Native/ELF_RelocationWithAddend.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Native/ELF_RelocationWithAddend.cs new file mode 100644 index 00000000000..0fcf9318aa5 --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Native/ELF_RelocationWithAddend.cs @@ -0,0 +1,52 @@ +using System; +using System.IO; + +namespace Microsoft.Android.AppTools.Native; + +abstract class ELF_Rela +{ + protected abstract long StructureSize { get; } + + public readonly TUnsigned r_offset; + public readonly TUnsigned r_info; + public readonly TSigned r_addend; + + protected ELF_Rela (BinaryReader reader, ulong offsetIntoData) + { + if (reader.BaseStream.Length < (long)offsetIntoData + StructureSize) { + throw new ArgumentOutOfRangeException ("Data array too short"); + } + ReadData (reader, out r_offset, out r_info, out r_addend); + } + + protected abstract void ReadData (BinaryReader reader, out TUnsigned offset, out TUnsigned info, out TSigned addend); + + public override string ToString() + { + return $"{GetType ()}: r_offset == 0x{r_offset:x} ({r_offset}); r_info == 0x{r_info:x} ({r_info}); r_addend == 0x{r_addend:x} ({r_addend})"; + } +} + +// Corresponds to Elf64_Rela structure from ELF documentation: +// +// typedef struct { +// Elf64_Addr r_offset; +// uint64_t r_info; +// int64_t r_addend; +// } Elf64_Rela; +// +sealed class ELF64_Rela : ELF_Rela +{ + protected override long StructureSize => 3 * sizeof (ulong); + + public ELF64_Rela (BinaryReader data, ulong offsetIntoData) + : base (data, offsetIntoData) + {} + + protected override void ReadData (BinaryReader reader, out ulong offset, out ulong info, out long addend) + { + offset = reader.ReadUInt64 (); + info = reader.ReadUInt64 (); + addend = reader.ReadInt64 (); + } +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Native/MarshalMethods.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Native/MarshalMethods.cs new file mode 100644 index 00000000000..a3fc064cc1d --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Native/MarshalMethods.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections.Generic; +using System.IO; + +using ELFSharp.ELF.Sections; + +namespace Microsoft.Android.AppTools.Native; + +sealed class MarshalMethodsManagedClass_V2 +{ + public readonly uint token; + public readonly IntPtr klass = IntPtr.Zero; + + public MarshalMethodsManagedClass_V2 (ILogger log, BinaryReader reader, AnELF elf, ISymbolEntry symbolEntry) + { + bool is64Bit = elf.Is64Bit; + ulong sizeSoFar = 0; + + // Each entry is: + // - 32-bit managed token + // - pointer to class instance + sizeSoFar += NativeHelpers.ReadField (reader, ref token, sizeSoFar, is64Bit); + sizeSoFar += NativeHelpers.ReadField (reader, ref klass, sizeSoFar, is64Bit); + } +} + +sealed class MarshalMethodName_V2 +{ + public readonly ulong id; + public readonly string name; + + public uint AssemblyIndex => (uint)((id & 0xFFFFFFFF00000000) >> 32); + public uint MethodToken => (uint)((id & 0x00000000FFFFFFFF)); + + public MarshalMethodName_V2 (ILogger log, BinaryReader reader, AnELF elf, ISymbolEntry symbolEntry) + { + bool is64Bit = elf.Is64Bit; + ulong sizeSoFar = 0; + ulong entryOffset = (ulong)reader.BaseStream.Position; + + // Each entry is: + // - 64-bit internal method ID + // - pointer to name + sizeSoFar += NativeHelpers.ReadField (reader, ref id, sizeSoFar, is64Bit); + ulong pointerOffset = NativeHelpers.GetPadding (sizeSoFar, is64Bit) + sizeSoFar + entryOffset; + name = elf.GetStringFromPointerField (symbolEntry, pointerOffset) ?? Constants.UnableToLoadDataForPointer; + sizeSoFar += NativeHelpers.ReadField (reader, ref name, sizeSoFar, is64Bit); + } +} + +abstract class MarshalMethods +{ + protected const string LogTag = "Marshal methods:"; + + public abstract string FormatName { get; } + public abstract ulong FormatTag { get; } + + protected ILogger Log { get; } + + protected MarshalMethods (ILogger log) + { + Log = log; + } + + public static MarshalMethods? Create (ILogger log, AnELF elf, ulong format_tag) + { + switch (format_tag) { + case Constants.FormatTag_V1: + case Constants.FormatTag_V2: + return new MarshalMethods_V2 (log, elf, format_tag); + + default: + WarnNotSupported (elf, format_tag); + return null; + } + } + + public static bool Supported (AnELF elf, ulong format_tag) + { + Utils.Log?.DebugLine ($"{LogTag} checking whether {elf.FilePath} contains supported marshal methods structures"); + // V1 is the same format as V2 + switch (format_tag) { + case Constants.FormatTag_V1: + case Constants.FormatTag_V2: + return LogAndReturn (MarshalMethods_V2.CompatibleBinary (elf, format_tag)); + + default: + WarnNotSupported (elf, format_tag); + break; + } + + return LogAndReturn (false); + + bool LogAndReturn (bool retval) + { + Utils.Log?.DebugLine ($"Marshal methods {Utils.AreOrNot (retval)} supported in {elf.FilePath}"); + return retval; + } + } + + protected static bool HasRequiredSymbols (AnELF elf, List symbolNames, string formatName) + { + Utils.Log?.DebugLine ($"{LogTag} checking for required {formatName} symbols in {elf.FilePath}"); + bool allGood = true; + foreach (string symbol in symbolNames) { + Utils.Log?.Debug ($" '{symbol}'"); + if (!elf.HasSymbol (symbol)) { + Utils.Log?.DebugLine (" [missing]"); + allGood = false; + } else { + Utils.Log?.DebugLine (" [found]"); + } + } + + return allGood; + } + + protected ISymbolEntry GetRequiredSymbol (AnELF elf, string symbolName) + { + ISymbolEntry? symbolEntry = elf.GetSymbol (symbolName); + if (symbolEntry == null) { + throw new InvalidOperationException ($"Internal error: symbol not found '{symbolName}', use CompatibleBinary to check validity before instantiating the class"); + } + + return symbolEntry; + } + + static void WarnNotSupported (AnELF elf, ulong format_tag) + { + Utils.Log?.WarningLine ($"{LogTag} reader does not support libxamarin-app.so format version 0x{format_tag:x}"); + } +} + +class MarshalMethods_V2 : MarshalMethods +{ + const string formatName = "V2"; + + static readonly List RequiredSymbols_V2 = new List { + Constants.SymbolNames.MarshalMethodsClassCache, + Constants.SymbolNames.MarshalMethodsClassNames, + Constants.SymbolNames.MarshalMethodsMethodNames, + Constants.SymbolNames.MarshalMethodsNumberOfClasses, + Constants.SymbolNames.MarshalMethodsXamarinAppInit, + }; + + public override string FormatName => formatName; + public override ulong FormatTag => Constants.FormatTag_V2; + + public ulong NumberOfClasses { get; protected set; } + public bool XamarinAppInitFuncValid { get; protected set; } + public List ClassCache { get; protected set; } + public List ClassNames { get; protected set; } + public List MethodNames { get; protected set; } + + public MarshalMethods_V2 (ILogger log, AnELF elf, ulong format_tag) + : base (log) + { + NumberOfClasses = elf.GetUInt32 (Constants.SymbolNames.MarshalMethodsNumberOfClasses); + + ISymbolEntry? xamarinAppInit = elf.GetSymbol (Constants.SymbolNames.MarshalMethodsXamarinAppInit); + if (xamarinAppInit != null) { + XamarinAppInitFuncValid = true; + if (xamarinAppInit.Type != SymbolType.Function) { + Log.WarningLine ($"{LogTag} symbol {Constants.SymbolNames.MarshalMethodsXamarinAppInit} is not a function"); + XamarinAppInitFuncValid = false; + } + + if (xamarinAppInit.Visibility != SymbolVisibility.Default) { + Log.WarningLine ($"{LogTag} symbol {Constants.SymbolNames.MarshalMethodsXamarinAppInit} is not exported"); + XamarinAppInitFuncValid = false; + } + } + + ClassCache = new List (); + ReadClassCache (elf, ClassCache); + + ClassNames = new List (); + ReadClassNames (elf, ClassNames); + + MethodNames = new List (); + ReadMethodNames (elf, MethodNames); + } + + public static bool CompatibleBinary (AnELF elf, ulong format_tag) + { + return HasRequiredSymbols (elf, RequiredSymbols_V2, formatName); + } + + void ReadMethodNames (AnELF elf, List names) + { + ISymbolEntry symbolEntry = GetRequiredSymbol (elf, Constants.SymbolNames.MarshalMethodsMethodNames); + byte[] data = elf.GetData (Constants.SymbolNames.MarshalMethodsMethodNames); + if (data.Length == 0) { + Log.DebugLine ($"{LogTag} {elf.FilePath} class names symbol {Constants.SymbolNames.MarshalMethodsMethodNames} is empty"); + return; + } + + using var stream = new MemoryStream (data); + using var reader = new BinaryReader (stream); + + bool seenLast = false; + while (stream.Position < stream.Length) { + var methodName = new MarshalMethodName_V2 (Log, reader, elf, symbolEntry); + if (methodName.id == 0 && String.IsNullOrEmpty (methodName.name)) { + // terminating entry, ignore + seenLast = true; + continue; + } + + if (seenLast) { + Log.WarningLine ($"{LogTag} method names array may be broken, contains termination entry in the middle of array"); + } + + names.Add (methodName); + } + } + + void ReadClassNames (AnELF elf, List names) + { + ISymbolEntry symbolEntry = GetRequiredSymbol (elf, Constants.SymbolNames.MarshalMethodsClassNames); + byte[] data = elf.GetData (Constants.SymbolNames.MarshalMethodsClassNames); + if (data.Length == 0) { + Log.DebugLine ($"{LogTag} {elf.FilePath} class names symbol {Constants.SymbolNames.MarshalMethodsClassNames} is empty"); + return; + } + + using var stream = new MemoryStream (data); + using var reader = new BinaryReader (stream); + + bool is64Bit = elf.Is64Bit; + ulong sizeSoFar = 0; + while (stream.Position < stream.Length) { + ulong entryOffset = (ulong)reader.BaseStream.Position; + + ulong pointerOffset = NativeHelpers.GetPadding (sizeSoFar, is64Bit) + entryOffset; + string name = elf.GetStringFromPointerField (symbolEntry, pointerOffset) ?? Constants.UnableToLoadDataForPointer; + sizeSoFar += NativeHelpers.ReadField (reader, ref name, sizeSoFar, is64Bit); + + names.Add (name); + } + } + + void ReadClassCache (AnELF elf, List cache) + { + ISymbolEntry symbolEntry = GetRequiredSymbol (elf, Constants.SymbolNames.MarshalMethodsClassCache); + byte[] data = elf.GetData (Constants.SymbolNames.MarshalMethodsClassCache); + if (data.Length == 0) { + Log.DebugLine ($"{LogTag} {elf.FilePath} class cache symbol {Constants.SymbolNames.MarshalMethodsClassCache} is empty"); + return; + } + + using var stream = new MemoryStream (data); + using var reader = new BinaryReader (stream); + + while (stream.Position < stream.Length) { + cache.Add (new MarshalMethodsManagedClass_V2 (Log, reader, elf, symbolEntry)); + } + } +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Native/NativeHelpers.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Native/NativeHelpers.cs new file mode 100644 index 00000000000..2c9d9f7175c --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Native/NativeHelpers.cs @@ -0,0 +1,342 @@ +using System; +using System.Buffers; +using System.IO; +using System.Text; + +using K4os.Compression.LZ4; + +namespace Microsoft.Android.AppTools.Native; + +static class NativeHelpers +{ + // + // See src/monodroid/jni/xamarin-app.hh; struct CompressedAssemblyHeader + // + // LZ4 compressed assembly header format (12 bytes): + // uint magic; // 0x5A4C4158; 'XALZ', little-endian + // uint descriptor_index; // Index into an internal assembly descriptor table + // uint uncompressed_length; // Size of assembly, uncompressed + // + const int CompressionHeaderLength = 12; + + public static readonly ArrayPool BytePool = ArrayPool.Shared; + public static ILogger? Log; + + /// + /// + /// Checks whether assembly data in is compressed. The method checks whether there + /// is enough data available in the stream, but it REQUIRES that the stream is positioned at the start of the + /// potentially compressed data. This is required since sometimes (e.g. in the assembly store reader), the + /// stream contains more than one entry and we mustn't assume anything about positions there. + /// + /// + /// Stream position is unmodified after this method returns, but stream must support seeking. + /// + /// + public static bool IsCompressedAssembly (Stream stream) + { + if (!EnoughDataInStreamForCompressedAssembly (stream, nameof (IsCompressedAssembly))) { + return false; + } + + long pos = stream.Position; + try { + foreach (byte b in Constants.CompressedDataMagic) { + int rval = stream.ReadByte (); + if (rval == -1) { + return false; + } + + var r = (byte)rval; + if (r != b) { + return false; + } + } + } finally { + stream.Seek (pos, SeekOrigin.Begin); + } + + return true; + } + + /// + /// + /// Attempt to decompress assembly data from . The method checks whether there + /// is enough data available in the stream, and that the data has a valid compression signature, but it + /// REQUIRES that the stream is positioned at the start of the potentially compressed data. This is + /// required since sometimes (e.g. in the assembly store reader), the stream contains more than one + /// entry and we mustn't assume anything about positions there. + /// + /// + /// The parameter may be set to a value equal to or less than zero to indicate that + /// all the data available in the stream from its current position to the end is to + /// be read. + /// + /// + /// Data is decompressed into , overwriting any data that might have been previously + /// found in it. On return, is positioned at the start of decompressed data. + /// + /// + /// Both streams must support seeking. + /// + /// + /// true upon successful completion, false otherwise + public static bool DecompressAssembly (Stream input, Stream output, long size = 0) + { + if (!EnoughDataInStreamForCompressedAssembly (input, nameof (DecompressAssembly))) { + return false; + } + + // Compressed header format is at the top of this class + using var reader = new BinaryReader (input, Encoding.UTF8, leaveOpen: true); + + // magic + uint magic = reader.ReadUInt32 (); + if (magic != Constants.CompressedDataMagicInt) { + Log?.DebugLine ($"Stream cannot be compressed, invalid magic number. Expected 0x{Constants.CompressedDataMagicInt:x}, got 0x{magic:x}"); + return false; + } + + // descriptor_index, ignore + reader.ReadUInt32 (); + + if (size <= 0) { + size = input.Length - input.Position; + } + + output.SetLength (0); + + int uncompressedLength = (int)reader.ReadUInt32 (); + int dataSize = (int)size - CompressionHeaderLength; // subtract the compression header size + byte[] inputBytes = BytePool.Rent (dataSize); + byte[]? outputBytes = null; + + try { + int nread = reader.Read (inputBytes, 0, dataSize); + if (nread < dataSize) { + Log?.DebugLine ($"{nameof(DecompressAssembly)}: read less data from stream ({nread} than expected ({inputBytes.Length})"); + return false; + } + + outputBytes = BytePool.Rent (uncompressedLength); + int decoded = LZ4Codec.Decode (inputBytes, 0, dataSize, outputBytes, 0, uncompressedLength); + if (decoded < uncompressedLength) { + Log?.DebugLine ($"{nameof(DecompressAssembly)}: LZ4 decoded less bytes ({decoded}) than expected ({uncompressedLength})"); + return false; + } + + output.Write (outputBytes, 0, decoded); + output.Flush (); + output.Seek (0, SeekOrigin.Begin); + } finally { + BytePool.Return (inputBytes); + if (outputBytes != null) { + BytePool.Return (outputBytes); + } + } + + return true; + } + + // Check whether there's enough data for compression header + at least one byte of data + static bool EnoughDataInStreamForCompressedAssembly (Stream stream, string where) + { + bool enough = EnoughDataInStream (stream, CompressionHeaderLength + 1); + if (!enough) { + Log?.DebugLine ($"{where}: stream cannot be compressed, not enough data"); + } + + return enough; + } + + static bool EnoughDataInStream (Stream stream, long needed) + { + if (needed < 0) { + return false; + } + + if (needed == 0) { + return true; + } + + return stream.Length - stream.Position >= needed; + } + + public static void CreateFileDirectory (string filePath) + { + string fileDir = Path.GetDirectoryName (filePath) ?? String.Empty; + if (fileDir.Length > 0) { + Directory.CreateDirectory (fileDir); + } + } + + static ulong GetPadding (ulong sizeSoFar, bool is64Bit, out ulong typeSize) + { + typeSize = GetNativeTypeSize (is64Bit); + if (typeSize == 1) { + return 0; + } + + ulong modulo; + if (is64Bit) { + modulo = typeSize < 8 ? 4u : 8u; + } else { + modulo = 4u; + } + + ulong alignment = sizeSoFar % modulo; + if (alignment == 0) { + return 0; + } + + return modulo - alignment; + } + + public static ulong GetPadding (ulong sizeSoFar, bool is64Bit) + { + return GetPadding (sizeSoFar, is64Bit, out ulong _); + } + + public static ulong GetPaddedSize (ulong sizeSoFar, bool is64Bit) + { + ulong padding = GetPadding (sizeSoFar, is64Bit, out ulong typeSize); + + if (padding == 0) { + return typeSize; + } + + return typeSize + padding; + } + + public static ulong GetNativeTypeSize (bool is64Bit) + { + Type type = typeof(S); + + if (type == typeof(string) || type == typeof(IntPtr)) { + // We treat `string` as a generic pointer + return is64Bit ? 8u : 4u; + } + + if (type == typeof(byte)) { + return 1u; + } + + if (type == typeof(bool)) { + return 1u; + } + + if (type == typeof(Int32) || type == typeof(UInt32)) { + return 4u; + } + + if (type == typeof(Int64) || type == typeof(UInt64)) { + return 8u; + } + + throw new InvalidOperationException ($"Unable to map managed type {type} to native assembler type"); + } + + /// + /// When reading binary data for C++ structures from ELF images, we need to account for field alignment. + /// This method calculates the number of actual bytes read from the data stream, as well as properly + /// adjusts the stream position to read the next field correctly. + /// + public static ulong GetSizeAndAdjustPosition (BinaryReader reader, ulong sizeSoFar, bool is64Bit) + { + ulong typeSize = GetNativeTypeSize (is64Bit); + ulong paddedSize = GetPaddedSize (sizeSoFar, is64Bit); + + if (paddedSize == 0) { + throw new InvalidOperationException ("Padded size must not be 0"); + } + + if (paddedSize < typeSize) { + throw new InvalidOperationException ("Padded size must not be smaller than type size"); + } + + if (paddedSize == typeSize) { + return typeSize; + } + + ulong seekOffset = paddedSize - typeSize; + reader.BaseStream.Seek ((long)seekOffset, SeekOrigin.Current); + + return paddedSize; + } + + /// + /// Read data from a C++ structure bool field and adjust stream position accordingly. + /// + /// + /// Number of actual bytes read (including padding) + public static ulong ReadField (BinaryReader reader, ref bool field, ulong sizeSoFar, bool is64Bit) + { + ulong ret = GetSizeAndAdjustPosition (reader, sizeSoFar, is64Bit); + field = reader.ReadBoolean (); + return ret; + } + + /// + /// Read data from a C++ structure byte field and adjust stream position accordingly. + /// + /// + /// Number of actual bytes read (including padding) + public static ulong ReadField (BinaryReader reader, ref byte field, ulong sizeSoFar, bool is64Bit) + { + ulong ret = GetSizeAndAdjustPosition (reader, sizeSoFar, is64Bit); + field = reader.ReadByte (); + return ret; + } + + /// + /// Read data from a C++ structure uint field and adjust stream position accordingly. + /// + /// + /// Number of actual bytes read (including padding) + public static ulong ReadField (BinaryReader reader, ref uint field, ulong sizeSoFar, bool is64Bit) + { + ulong ret = GetSizeAndAdjustPosition (reader, sizeSoFar, is64Bit); + field = reader.ReadUInt32 (); + return ret; + } + + /// + /// Read data from a C++ structure ulong field and adjust stream position accordingly. + /// + /// + /// Number of actual bytes read (including padding) + public static ulong ReadField (BinaryReader reader, ref ulong field, ulong sizeSoFar, bool is64Bit) + { + ulong ret = GetSizeAndAdjustPosition (reader, sizeSoFar, is64Bit); + field = reader.ReadUInt64 (); + return ret; + } + + /// + /// Read data from a C++ structure pointer field and adjust stream position accordingly. Nothing is + /// actually stored in the parameter, as the pointer will have a value of 0. Instead, + /// appropriate number of bytes is skipped in the data stream. + /// + /// + /// Number of actual bytes read (including padding) + public static ulong ReadField (BinaryReader reader, ref string field, ulong sizeSoFar, bool is64Bit) + { + ulong ret = GetSizeAndAdjustPosition (reader, sizeSoFar, is64Bit); + var _ = is64Bit ? reader.ReadUInt64 () : reader.ReadUInt32 (); + return ret; + } + + /// + /// Read data from a C++ structure pointer field and adjust stream position accordingly. Nothing is + /// actually stored in the parameter, as the pointer will have a value of 0. Instead, + /// appropriate number of bytes is skipped in the data stream. + /// + /// + /// Number of actual bytes read (including padding) + public static ulong ReadField (BinaryReader reader, ref IntPtr field, ulong sizeSoFar, bool is64Bit) + { + ulong ret = GetSizeAndAdjustPosition (reader, sizeSoFar, is64Bit); + var _ = is64Bit ? reader.ReadUInt64 () : reader.ReadUInt32 (); + return ret; + } +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Native/RelocationSection.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Native/RelocationSection.cs new file mode 100644 index 00000000000..04206d4198f --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Native/RelocationSection.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.IO; + +using ELFSharp.ELF.Sections; + +namespace Microsoft.Android.AppTools.Native; + +abstract class RelocationSectionAddend + where TUnsigned: notnull + where TRela: notnull, ELF_Rela +{ + public abstract Dictionary Entries { get; } +} + +class RelocationSectionAddend64 : RelocationSectionAddend +{ + public override Dictionary Entries { get; } = new Dictionary (); + + public RelocationSectionAddend64 (Section relaDynSection) + { + byte[] data = relaDynSection.GetContents (); + using var stream = new MemoryStream (data); + using var reader = new BinaryReader (stream); + + while (stream.Position < stream.Length) { + var entry = new ELF64_Rela (reader, (ulong)stream.Position); + Entries.Add (entry.r_offset, entry); + } + } +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Native/RelocationTypes.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Native/RelocationTypes.cs new file mode 100644 index 00000000000..5d737a3e56e --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Native/RelocationTypes.cs @@ -0,0 +1,118 @@ +namespace Microsoft.Android.AppTools.Native; + +enum RelocationTypeARM64 +{ + R_AARCH64_TLSGD_MOVW_G1 = 515, // GOT-rel. MOV{N,Z} 31:16. + R_AARCH64_TLSGD_MOVW_G0_NC = 516, // GOT-rel. MOVK imm. 15:0. + R_AARCH64_TLSLD_ADR_PREL21 = 517, // Like 512; local dynamic model. + R_AARCH64_TLSLD_ADR_PAGE21 = 518, // Like 513; local dynamic model. + R_AARCH64_TLSLD_ADD_LO12_NC = 519, // Like 514; local dynamic model. + R_AARCH64_TLSLD_MOVW_G1 = 520, // Like 515; local dynamic model. + R_AARCH64_TLSLD_MOVW_G0_NC = 521, // Like 516; local dynamic model. + R_AARCH64_TLSLD_LD_PREL19 = 522, // TLS PC-rel. load imm. 20:2. + R_AARCH64_TLSLD_MOVW_DTPREL_G2 = 523, // TLS DTP-rel. MOV{N,Z} 47:32. + R_AARCH64_TLSLD_MOVW_DTPREL_G1 = 524, // TLS DTP-rel. MOV{N,Z} 31:16. + R_AARCH64_TLSLD_MOVW_DTPREL_G1_NC = 525, // Likewise; MOVK; no check. + R_AARCH64_TLSLD_MOVW_DTPREL_G0 = 526, // TLS DTP-rel. MOV{N,Z} 15:0. + R_AARCH64_TLSLD_MOVW_DTPREL_G0_NC = 527, // Likewise; MOVK; no check. + R_AARCH64_TLSLD_ADD_DTPREL_HI12 = 528, // DTP-rel. ADD imm. from 23:12. + R_AARCH64_TLSLD_ADD_DTPREL_LO12 = 529, // DTP-rel. ADD imm. from 11:0. + R_AARCH64_TLSLD_ADD_DTPREL_LO12_NC = 530, // Likewise; no ovfl. check. + R_AARCH64_TLSLD_LDST8_DTPREL_LO12 = 531, // DTP-rel. LD/ST imm. 11:0. + R_AARCH64_TLSLD_LDST8_DTPREL_LO12_NC = 532, // Likewise; no check. + R_AARCH64_TLSLD_LDST16_DTPREL_LO12 = 533, // DTP-rel. LD/ST imm. 11:1. + R_AARCH64_TLSLD_LDST16_DTPREL_LO12_NC = 534, // Likewise; no check. + R_AARCH64_TLSLD_LDST32_DTPREL_LO12 = 535, // DTP-rel. LD/ST imm. 11:2. + R_AARCH64_TLSLD_LDST32_DTPREL_LO12_NC = 536, // Likewise; no check. + R_AARCH64_TLSLD_LDST64_DTPREL_LO12 = 537, // DTP-rel. LD/ST imm. 11:3. + R_AARCH64_TLSLD_LDST64_DTPREL_LO12_NC = 538, // Likewise; no check. + R_AARCH64_TLSIE_MOVW_GOTTPREL_G1 = 539, // GOT-rel. MOV{N,Z} 31:16. + R_AARCH64_TLSIE_MOVW_GOTTPREL_G0_NC = 540, // GOT-rel. MOVK 15:0. + R_AARCH64_TLSIE_ADR_GOTTPREL_PAGE21 = 541, // Page-rel. ADRP 32:12. + R_AARCH64_TLSIE_LD64_GOTTPREL_LO12_NC = 542, // Direct LD off. 11:3. + R_AARCH64_TLSIE_LD_GOTTPREL_PREL19 = 543, // PC-rel. load imm. 20:2. + R_AARCH64_TLSLE_MOVW_TPREL_G2 = 544, // TLS TP-rel. MOV{N,Z} 47:32. + R_AARCH64_TLSLE_MOVW_TPREL_G1 = 545, // TLS TP-rel. MOV{N,Z} 31:16. + R_AARCH64_TLSLE_MOVW_TPREL_G1_NC = 546, // Likewise; MOVK; no check. + R_AARCH64_TLSLE_MOVW_TPREL_G0 = 547, // TLS TP-rel. MOV{N,Z} 15:0. + R_AARCH64_TLSLE_MOVW_TPREL_G0_NC = 548, // Likewise; MOVK; no check. + R_AARCH64_TLSLE_ADD_TPREL_HI12 = 549, // TP-rel. ADD imm. 23:12. + R_AARCH64_TLSLE_ADD_TPREL_LO12 = 550, // TP-rel. ADD imm. 11:0. + R_AARCH64_TLSLE_ADD_TPREL_LO12_NC = 551, // Likewise; no ovfl. check. + R_AARCH64_TLSLE_LDST8_TPREL_LO12 = 552, // TP-rel. LD/ST off. 11:0. + R_AARCH64_TLSLE_LDST8_TPREL_LO12_NC = 553, // Likewise; no ovfl. check. + R_AARCH64_TLSLE_LDST16_TPREL_LO12 = 554, // TP-rel. LD/ST off. 11:1. + R_AARCH64_TLSLE_LDST16_TPREL_LO12_NC = 555, // Likewise; no check. + R_AARCH64_TLSLE_LDST32_TPREL_LO12 = 556, // TP-rel. LD/ST off. 11:2. + R_AARCH64_TLSLE_LDST32_TPREL_LO12_NC = 557, // Likewise; no check. + R_AARCH64_TLSLE_LDST64_TPREL_LO12 = 558, // TP-rel. LD/ST off. 11:3. + R_AARCH64_TLSLE_LDST64_TPREL_LO12_NC = 559, // Likewise; no check. + R_AARCH64_TLSDESC_LD_PREL19 = 560, // PC-rel. load immediate 20:2. + R_AARCH64_TLSDESC_ADR_PREL21 = 561, // PC-rel. ADR immediate 20:0. + R_AARCH64_TLSDESC_ADR_PAGE21 = 562, // Page-rel. ADRP imm. 32:12. + R_AARCH64_TLSDESC_LD64_LO12 = 563, // Direct LD off. from 11:3. + R_AARCH64_TLSDESC_ADD_LO12 = 564, // Direct ADD imm. from 11:0. + R_AARCH64_TLSDESC_OFF_G1 = 565, // GOT-rel. MOV{N,Z} imm. 31:16. + R_AARCH64_TLSDESC_OFF_G0_NC = 566, // GOT-rel. MOVK imm. 15:0; no ck. + R_AARCH64_TLSDESC_LDR = 567, // Relax LDR. + R_AARCH64_TLSDESC_ADD = 568, // Relax ADD. + R_AARCH64_TLSDESC_CALL = 569, // Relax BLR. + R_AARCH64_TLSLE_LDST128_TPREL_LO12 = 570, // TP-rel. LD/ST off. 11:4. + R_AARCH64_TLSLE_LDST128_TPREL_LO12_NC = 571, // Likewise; no check. + R_AARCH64_TLSLD_LDST128_DTPREL_LO12 = 572, // DTP-rel. LD/ST imm. 11:4. + R_AARCH64_TLSLD_LDST128_DTPREL_LO12_NC = 573, // Likewise; no check. + R_AARCH64_COPY = 1024, // Copy symbol at runtime. + R_AARCH64_GLOB_DAT = 1025, // Create GOT entry. + R_AARCH64_JUMP_SLOT = 1026, // Create PLT entry. + R_AARCH64_RELATIVE = 1027, // Adjust by program base. + R_AARCH64_TLS_DTPMOD = 1028, // Module number, 64 bit. + R_AARCH64_TLS_DTPREL = 1029, // Module-relative offset, 64 bit. + R_AARCH64_TLS_TPREL = 1030, // TP-relative offset, 64 bit. + R_AARCH64_TLSDESC = 1031, // TLS Descriptor. + R_AARCH64_IRELATIVE = 1032, // STT_GNU_IFUNC relocation. +} + +enum RelocationTypeX64 +{ + R_X86_64_NONE = 0, // No reloc + R_X86_64_64 = 1, // Direct 64 bit + R_X86_64_PC32 = 2, // PC relative 32 bit signed + R_X86_64_GOT32 = 3, // 32 bit GOT entry + R_X86_64_PLT32 = 4, // 32 bit PLT address + R_X86_64_COPY = 5, // Copy symbol at runtime + R_X86_64_GLOB_DAT = 6, // Create GOT entry + R_X86_64_JUMP_SLOT = 7, // Create PLT entry + R_X86_64_RELATIVE = 8, // Adjust by program base + R_X86_64_GOTPCREL = 9, // 32 bit signed PC relative offset to GOT + R_X86_64_32 = 10, // Direct 32 bit zero extended + R_X86_64_32S = 11, // Direct 32 bit sign extended + R_X86_64_16 = 12, // Direct 16 bit zero extended + R_X86_64_PC16 = 13, // 16 bit sign extended pc relative + R_X86_64_8 = 14, // Direct 8 bit sign extended + R_X86_64_PC8 = 15, // 8 bit sign extended pc relative + R_X86_64_DTPMOD64 = 16, // ID of module containing symbol + R_X86_64_DTPOFF64 = 17, // Offset in module's TLS block + R_X86_64_TPOFF64 = 18, // Offset in initial TLS block + R_X86_64_TLSGD = 19, // 32 bit signed PC relative offset to two GOT entries for GD symbol + R_X86_64_TLSLD = 20, // 32 bit signed PC relative offset to two GOT entries for LD symbol + R_X86_64_DTPOFF32 = 21, // Offset in TLS block + R_X86_64_GOTTPOFF = 22, // 32 bit signed PC relative offset to GOT entry for IE symbol + R_X86_64_TPOFF32 = 23, // Offset in initial TLS block + R_X86_64_PC64 = 24, // PC relative 64 bit + R_X86_64_GOTOFF64 = 25, // 64 bit offset to GOT + R_X86_64_GOTPC32 = 26, // 32 bit signed pc relative offset to GOT + R_X86_64_GOT64 = 27, // 64-bit GOT entry offset + R_X86_64_GOTPCREL64 = 28, // 64-bit PC relative offset to GOT entry + R_X86_64_GOTPC64 = 29, // 64-bit PC relative offset to GOT + R_X86_64_GOTPLT64 = 30, // like GOT64, says PLT entry needed + R_X86_64_PLTOFF64 = 31, // 64-bit GOT relative offset to PLT entry + R_X86_64_SIZE32 = 32, // Size of symbol plus 32-bit addend + R_X86_64_SIZE64 = 33, // Size of symbol plus 64-bit addend + R_X86_64_GOTPC32_TLSDESC = 34, // GOT offset for TLS descriptor. + R_X86_64_TLSDESC_CALL = 35, // Marker for call through TLS descriptor. + R_X86_64_TLSDESC = 36, // TLS descriptor. + R_X86_64_IRELATIVE = 37, // Adjust indirectly by program base + R_X86_64_RELATIVE64 = 38, // 64-bit adjust by program base + R_X86_64_GOTPCRELX = 41, // Load from 32 bit signed pc relative offset to GOT entry without REX prefix, relaxable. + R_X86_64_REX_GOTPCRELX = 42, // Load from 32 bit signed pc relative offset to GOT entry with REX prefix, relaxable. +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Native/SharedLibrary.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Native/SharedLibrary.cs new file mode 100644 index 00000000000..304c7698e49 --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Native/SharedLibrary.cs @@ -0,0 +1,10 @@ +using Xamarin.Android.Tools; + +namespace Microsoft.Android.AppTools; + +public class SharedLibrary +{ + public SharedLibraryKind Kind { get; private set; } = SharedLibraryKind.Other; + public bool IsXamarinDataContainer { get; private set; } + public AndroidTargetArch TargetArchitecture { get; private set; } = AndroidTargetArch.None; +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Native/SharedLibraryKind.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Native/SharedLibraryKind.cs new file mode 100644 index 00000000000..52cbcb1ed25 --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.Native/SharedLibraryKind.cs @@ -0,0 +1,10 @@ +namespace Microsoft.Android.AppTools; + +public enum SharedLibraryKind +{ + XamarinApp, + MonoRuntime, + CoreClrRuntime, + BCL, + Other, +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.csproj b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.csproj new file mode 100644 index 00000000000..b76d93f93fa --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools.csproj @@ -0,0 +1,24 @@ + + + + $(DotNetStableTargetFramework) + enable + IN_APPTOOLS;$(DefineConstants) + + + + + + + + + + + + + + + + + + diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/AXMLParser.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/AXMLParser.cs new file mode 100644 index 00000000000..f3556d09d0d --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/AXMLParser.cs @@ -0,0 +1,686 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Xml; + +namespace Microsoft.Android.AppTools; + +enum ChunkType : ushort +{ + Null = 0x0000, + StringPool = 0x0001, + Table = 0x0002, + Xml = 0x0003, + + XmlFirstChunk = 0x0100, + XmlStartNamespace = 0x0100, + XmlEndNamespace = 0x0101, + XmlStartElement = 0x0102, + XmlEndElement = 0x0103, + XmlCData = 0x0104, + XmlLastChunk = 0x017f, + XmlResourceMap = 0x0180, + + TablePackage = 0x0200, + TableType = 0x0201, + TableTypeSpec = 0x0202, + TableLibrary = 0x0203, +} + +enum AttributeType : uint +{ + // The 'data' field is either 0 or 1, specifying this resource is either undefined or empty, respectively. + Null = 0x00, + + // The 'data' field holds a ResTable_ref, a reference to another resource + Reference = 0x01, + + // The 'data' field holds an attribute resource identifier. + Attribute = 0x02, + + // The 'data' field holds an index into the containing resource table's global value string pool. + String = 0x03, + + // The 'data' field holds a single-precision floating point number. + Float = 0x04, + + // The 'data' holds a complex number encoding a dimension value such as "100in". + Dimension = 0x05, + + // The 'data' holds a complex number encoding a fraction of a container. + Fraction = 0x06, + + // The 'data' holds a dynamic ResTable_ref, which needs to be resolved before it can be used like a Reference + DynamicReference = 0x07, + + // The 'data' holds an attribute resource identifier, which needs to be resolved before it can be used like a Attribute. + DynamicAttribute = 0x08, + + // The 'data' is a raw integer value of the form n..n. + IntDec = 0x10, + + // The 'data' is a raw integer value of the form 0xn..n. + IntHex = 0x11, + + // The 'data' is either 0 or 1, for input "false" or "true" respectively. + IntBoolean = 0x12, + + // The 'data' is a raw integer value of the form #aarrggbb. + IntColorARGB8 = 0x1c, + + // The 'data' is a raw integer value of the form #rrggbb. + IntColorRGB8 = 0x1d, + + // The 'data' is a raw integer value of the form #argb. + IntColorARGB4 = 0x1e, + + // The 'data' is a raw integer value of the form #rgb. + IntColorRGB4 = 0x1f, +} + +// +// Based on https://github.com/androguard/androguard/tree/832104db3eb5dc3cc66b30883fa8ce8712dfa200/androguard/core/axml +// +class AXMLParser +{ + // Position of fields inside an attribute + const int ATTRIBUTE_IX_NAMESPACE_URI = 0; + const int ATTRIBUTE_IX_NAME = 1; + const int ATTRIBUTE_IX_VALUE_STRING = 2; + const int ATTRIBUTE_IX_VALUE_TYPE = 3; + const int ATTRIBUTE_IX_VALUE_DATA = 4; + const int ATTRIBUTE_LENGHT = 5; + + const long MinimumDataSize = 8; + const long MaximumDataSize = (long)UInt32.MaxValue; + + const uint ComplexUnitMask = 0x0f; + + static readonly float[] RadixMultipliers = { + 0.00390625f, + 3.051758E-005f, + 1.192093E-007f, + 4.656613E-010f, + }; + + static readonly string[] DimensionUnits = { + "px", + "dip", + "sp", + "pt", + "in", + "mm", + }; + + static readonly string[] FractionUnits = { + "%", + "%p", + }; + + readonly ILogger log; + + Stream data; + long dataSize; + ARSCHeader axmlHeader; + uint fileSize; + StringBlock stringPool; + bool valid = true; + long initialPosition; + + public bool IsValid => valid; + + public AXMLParser (Stream data, ILogger logger) + { + log = logger; + + this.data = data; + dataSize = data.Length; + + // Minimum is a single ARSCHeader, which would be a strange edge case... + if (dataSize < MinimumDataSize) { + throw new InvalidDataException ($"Input data size too small for it to be valid AXML content ({dataSize} < {MinimumDataSize})"); + } + + // This would be even stranger, if an AXML file is larger than 4GB... + // But this is not possible as the maximum chunk size is a unsigned 4 byte int. + if (dataSize > MaximumDataSize) { + throw new InvalidDataException ($"Input data size too large for it to be a valid AXML content ({dataSize} > {MaximumDataSize})"); + } + + try { + axmlHeader = new ARSCHeader (data); + } catch (Exception) { + log.ErrorLine ("Error parsing the first data header"); + throw; + } + + if (axmlHeader.HeaderSize != 8) { + throw new InvalidDataException ($"This does not look like AXML data. header size does not equal 8. header size = {axmlHeader.Size}"); + } + + fileSize = axmlHeader.Size; + if (fileSize > dataSize) { + throw new InvalidDataException ($"This does not look like AXML data. Declared data size does not match real size: {fileSize} vs {dataSize}"); + } + + if (fileSize < dataSize) { + log.WarningLine ($"Declared data size ({fileSize}) is smaller than total data size ({dataSize}). Was something appended to the file? Trying to parse it anyways."); + } + + if (axmlHeader.Type != ChunkType.Xml) { + log.WarningLine ($"AXML file has an unusual resource type, trying to parse it anyways. Resource Type: 0x{(ushort)axmlHeader.Type:04x}"); + } + + ARSCHeader stringPoolHeader = new ARSCHeader (data, ChunkType.StringPool); + if (stringPoolHeader.HeaderSize != 28) { + throw new InvalidDataException ($"This does not look like an AXML file. String chunk header size does not equal 28. Header size = {stringPoolHeader.Size}"); + } + + stringPool = new StringBlock (logger, data, stringPoolHeader); + initialPosition = data.Position; + } + + public XmlDocument? Parse () + { + // Reset position in case we're called more than once, for whatever reason + data.Seek (initialPosition, SeekOrigin.Begin); + valid = true; + + XmlDocument ret = new XmlDocument (); + XmlDeclaration declaration = ret.CreateXmlDeclaration ("1.0", stringPool.IsUTF8 ? "UTF-8" : "UTF-16", null); + ret.InsertBefore (declaration, ret.DocumentElement); + + using var reader = new BinaryReader (data, Encoding.UTF8, leaveOpen: true); + ARSCHeader? header; + string? nsPrefix = null; + string? nsUri = null; + uint prefixIndex = 0; + uint uriIndex = 0; + var nsUriToPrefix = new Dictionary (StringComparer.Ordinal); + XmlNode? currentNode = ret.DocumentElement; + + while (data.Position < dataSize) { + header = new ARSCHeader (data); + + // Special chunk: Resource Map. This chunk might follow the string pool. + if (header.Type == ChunkType.XmlResourceMap) { + if (!SkipOverResourceMap (header, reader)) { + valid = false; + break; + } + continue; + } + + // XML chunks + + // Skip over unknown types + if (!Enum.IsDefined (typeof(ChunkType), header.TypeRaw)) { + log.WarningLine ($"Unknown chunk type 0x{header.TypeRaw:x} at offset {data.Position}. Skipping over {header.Size} bytes"); + data.Seek (header.Size, SeekOrigin.Current); + continue; + } + + // Check that we read a correct header + if (header.HeaderSize != 16) { + log.WarningLine ($"XML chunk header size is not 16. Chunk type {header.Type} (0x{header.TypeRaw:x}), chunk size {header.Size}"); + data.Seek (header.Size, SeekOrigin.Current); + continue; + } + + // Line Number of the source file, only used as meta information + uint lineNumber = reader.ReadUInt32 (); + + // Comment_Index (usually 0xffffffff) + uint commentIndex = reader.ReadUInt32 (); + + if (commentIndex != 0xffffffff && (header.Type == ChunkType.XmlStartNamespace || header.Type == ChunkType.XmlEndNamespace)) { + log.WarningLine ($"Unhandled Comment at namespace chunk: {commentIndex}"); + } + + if (header.Type == ChunkType.XmlStartNamespace) { + prefixIndex = reader.ReadUInt32 (); + uriIndex = reader.ReadUInt32 (); + + nsPrefix = stringPool.GetString (prefixIndex); + nsUri = stringPool.GetString (uriIndex); + + if (!String.IsNullOrEmpty (nsUri)) { + nsUriToPrefix[nsUri] = nsPrefix ?? String.Empty; + } + + log.VerboseLine ($"Start of Namespace mapping: prefix {prefixIndex}: '{nsPrefix}' --> uri {uriIndex}: '{nsUri}'"); + + if (String.IsNullOrEmpty (nsUri)) { + log.WarningLine ($"Namespace prefix '{nsPrefix}' resolves to empty URI."); + } + + continue; + } + + if (header.Type == ChunkType.XmlEndNamespace) { + // Namespace handling is **really** simplified, since we expect to deal only with AndroidManifest.xml which should have just one namespace. + // There should be no problems with that. Famous last words. + uint endPrefixIndex = reader.ReadUInt32 (); + uint endUriIndex = reader.ReadUInt32 (); + + log.VerboseLine ($"End of Namespace mapping: prefix {endPrefixIndex}, uri {endUriIndex}"); + if (endPrefixIndex != prefixIndex) { + log.WarningLine ($"Prefix index of Namespace end doesn't match the last Namespace prefix index: {prefixIndex} != {endPrefixIndex}"); + } + + if (endUriIndex != uriIndex) { + log.WarningLine ($"URI index of Namespace end doesn't match the last Namespace URI index: {uriIndex} != {endUriIndex}"); + } + + string? endUri = stringPool.GetString (endUriIndex); + if (!String.IsNullOrEmpty (endUri) && nsUriToPrefix.ContainsKey (endUri)) { + nsUriToPrefix.Remove (endUri); + } + + nsPrefix = null; + nsUri = null; + prefixIndex = 0; + uriIndex = 0; + + continue; + } + + uint tagNsUriIndex; + uint tagNameIndex; + string? tagName; +// string? tagNs; // TODO: implement + + if (header.Type == ChunkType.XmlStartElement) { + // The TAG consists of some fields: + // * (chunk_size, line_number, comment_index - we read before) + // * namespace_uri + // * name + // * flags + // * attribute_count + // * class_attribute + // After that, there are two lists of attributes, 20 bytes each + tagNsUriIndex = reader.ReadUInt32 (); + tagNameIndex = reader.ReadUInt32 (); + uint tagFlags = reader.ReadUInt32 (); + uint attributeCount = reader.ReadUInt32 () & 0xffff; + uint classAttribute = reader.ReadUInt32 (); + + // Tag name is, of course, required but instead of throwing an exception should we find none, we use a fake name in hope that we can still salvage + // the document. + tagName = stringPool.GetString (tagNameIndex) ?? "unnamedTag"; + log.VerboseLine ($"Start of tag '{tagName}', NS URI index {tagNsUriIndex}"); + log.VerboseLine ($"Reading tag attributes ({attributeCount}):"); + + string? tagNsUri = tagNsUriIndex != 0xffffffff ? stringPool.GetString (tagNsUriIndex) : null; + string? tagNsPrefix; + + if (String.IsNullOrEmpty (tagNsUri) || !nsUriToPrefix.TryGetValue (tagNsUri, out tagNsPrefix)) { + tagNsPrefix = null; + } + + XmlElement element = ret.CreateElement (tagNsPrefix, tagName, tagNsUri); + if (currentNode == null) { + ret.AppendChild (element); + if (!String.IsNullOrEmpty (nsPrefix) && !String.IsNullOrEmpty (nsUri)) { + ret.DocumentElement!.SetAttribute ($"xmlns:{nsPrefix}", nsUri); + } + } else { + currentNode.AppendChild (element); + } + currentNode = element; + + for (uint i = 0; i < attributeCount; i++) { + uint attrNsIdx = reader.ReadUInt32 (); // string index + uint attrNameIdx = reader.ReadUInt32 (); // string index + uint attrValue = reader.ReadUInt32 (); + uint attrType = reader.ReadUInt32 () >> 24; + uint attrData = reader.ReadUInt32 (); + + string? attrNs = attrNsIdx != 0xffffffff ? stringPool.GetString (attrNsIdx) : String.Empty; + string? attrName = stringPool.GetString (attrNameIdx); + + if (String.IsNullOrEmpty (attrName)) { + log.WarningLine ($"Attribute without name, ignoring. Offset: {data.Position}"); + continue; + } + + log.VerboseLine ($" '{attrName}': ns == '{attrNs}'; value == 0x{attrValue:x}; type == 0x{attrType:x}; data == 0x{attrData:x}"); + XmlAttribute attr; + + if (!String.IsNullOrEmpty (attrNs)) { + attr = ret.CreateAttribute (nsUriToPrefix[attrNs], attrName, attrNs); + } else { + attr = ret.CreateAttribute (attrName!); + } + attr.Value = GetAttributeValue (attrValue, attrType, attrData); + element.SetAttributeNode (attr); + } + continue; + } + + if (header.Type == ChunkType.XmlEndElement) { + tagNsUriIndex = reader.ReadUInt32 (); + tagNameIndex = reader.ReadUInt32 (); + + tagName = stringPool.GetString (tagNameIndex); + log.VerboseLine ($"End of tag '{tagName}', NS URI index {tagNsUriIndex}"); + currentNode = currentNode?.ParentNode!; + continue; + } + + // TODO: add support for CDATA + } + + return ret; + } + + string GetAttributeValue (uint attrValue, uint attrType, uint attrData) + { + if (!Enum.IsDefined (typeof(AttributeType), attrType)) { + log.WarningLine ($"Unknown attribute type value 0x{attrType:x}, returning empty attribute value (data == 0x{attrData:x}). Offset: {data.Position}"); + return String.Empty; + } + + switch ((AttributeType)attrType) { + case AttributeType.Null: + return attrData == 0 ? "?NULL?" : String.Empty; + + case AttributeType.Reference: + return $"@{MaybePrefix()}{attrData:x08}"; + + case AttributeType.Attribute: + return $"?{MaybePrefix()}{attrData:x08}"; + + case AttributeType.String: + return stringPool.GetString (attrData) ?? String.Empty; + + case AttributeType.Float: + return $"{(float)attrData}"; + + case AttributeType.Dimension: + return $"{ComplexToFloat(attrData)}{DimensionUnits[attrData & ComplexUnitMask]}"; + + case AttributeType.Fraction: + return $"{ComplexToFloat(attrData) * 100.0f}{FractionUnits[attrData & ComplexUnitMask]}"; + + case AttributeType.IntDec: + return attrData.ToString (); + + case AttributeType.IntHex: + return $"0x{attrData:X08}"; + + case AttributeType.IntBoolean: + return attrData == 0 ? "false" : "true"; + + case AttributeType.IntColorARGB8: + case AttributeType.IntColorRGB8: + case AttributeType.IntColorARGB4: + case AttributeType.IntColorRGB4: + return $"#{attrData:X08}"; + } + + return String.Empty; + + string MaybePrefix () + { + if (attrData >> 24 == 1) { + return "android:"; + } + return String.Empty; + } + + float ComplexToFloat (uint value) + { + return (float)(value & 0xffffff00) * RadixMultipliers[(value >> 4) & 3]; + } + } + + bool SkipOverResourceMap (ARSCHeader header, BinaryReader reader) + { + log.VerboseLine ("AXML contains a resource map"); + + // Check size: < 8 bytes mean that the chunk is not complete + // Should be aligned to 4 bytes. + if (header.Size < 8 || (header.Size % 4) != 0) { + log.ErrorLine ("Invalid chunk size in chunk XML_RESOURCE_MAP"); + return false; + } + + // Since our main interest is in reading AndroidManifest.xml, we're going to skip over the table + for (int i = 0; i < (header.Size - header.HeaderSize) / 4; i++) { + reader.ReadUInt32 (); + } + + return true; + } +} + +class StringBlock +{ + const uint FlagSorted = 1 << 0; + const uint FlagUTF8 = 1 << 0; + + readonly ILogger log; + ARSCHeader header; + uint stringCount; + uint stringsOffset; + uint flags; + bool isUTF8; + List stringOffsets; + byte[] chars; + Dictionary stringCache; + + public uint StringCount => stringCount; + public bool IsUTF8 => isUTF8; + + public StringBlock (ILogger logger, Stream data, ARSCHeader stringPoolHeader) + { + log = logger; + header = stringPoolHeader; + + using var reader = new BinaryReader (data, Encoding.UTF8, leaveOpen: true); + + stringCount = reader.ReadUInt32 (); + uint styleCount = reader.ReadUInt32 (); + + flags = reader.ReadUInt32 (); + isUTF8 = (flags & FlagUTF8) == FlagUTF8; + + stringsOffset = reader.ReadUInt32 (); + uint stylesOffset = reader.ReadUInt32 (); + + if (styleCount == 0 && stylesOffset > 0) { + log.InfoLine ("Styles Offset given, but styleCount is zero. This is not a problem but could indicate packers."); + } + + stringOffsets = new List (); + + for (uint i = 0; i < stringCount; i++) { + stringOffsets.Add (reader.ReadUInt32 ()); + } + + // We're not interested in styles, skip over their offsets + for (uint i = 0; i < styleCount; i++) { + reader.ReadUInt32 (); + } + + bool haveStyles = stylesOffset != 0 && styleCount != 0; + uint size = header.Size - stringsOffset; + if (haveStyles) { + size = stylesOffset - stringsOffset; + } + + if (size % 4 != 0) { + log.WarningLine ("Size of strings is not aligned on four bytes."); + } + + chars = new byte[size]; + reader.Read (chars, 0, (int)size); + + if (haveStyles) { + size = header.Size - stylesOffset; + + if (size % 4 != 0) { + log.WarningLine ("Size of styles is not aligned on four bytes."); + } + + // Not interested in them, skip + for (uint i = 0; i < size / 4; i++) { + reader.ReadUInt32 (); + } + } + + stringCache = new Dictionary (); + } + + public string? GetString (uint idx) + { + if (stringCache.TryGetValue (idx, out string? ret)) { + return ret; + } + + if (idx < 0 || idx > stringOffsets.Count || stringOffsets.Count == 0) { + return null; + } + + uint offset = stringOffsets[(int)idx]; + if (isUTF8) { + ret = DecodeUTF8 (offset); + } else { + ret = DecodeUTF16 (offset); + } + stringCache[idx] = ret; + + return ret; + } + + string DecodeUTF8 (uint offset) + { + // UTF-8 Strings contain two lengths, as they might differ: + // 1) the string length in characters + (uint length, uint nbytes) = DecodeLength (offset, sizeOfChar: 1); + offset += nbytes; + + // 2) the number of bytes the encoded string occupies + (uint encodedBytes, nbytes) = DecodeLength (offset, sizeOfChar: 1); + offset += nbytes; + + if (chars[offset + encodedBytes] != 0) { + throw new InvalidDataException ($"UTF-8 string is not NUL-terminated. Offset: offset"); + } + + return Encoding.UTF8.GetString (chars, (int)offset, (int)encodedBytes); + } + + string DecodeUTF16 (uint offset) + { + (uint length, uint nbytes) = DecodeLength (offset, sizeOfChar: 2); + offset += nbytes; + + uint encodedBytes = length * 2; + if (chars[offset + encodedBytes] != 0 && chars[offset + encodedBytes + 1] != 0) { + throw new InvalidDataException ($"UTF-16 string is not NUL-terminated. Offset: offset"); + } + + return Encoding.Unicode.GetString (chars, (int)offset, (int)encodedBytes); + } + + (uint length, uint nbytes) DecodeLength (uint offset, uint sizeOfChar) + { + uint sizeOfTwoChars = sizeOfChar << 1; + uint highBit = 0x80u << (8 * ((int)sizeOfChar - 1)); + uint length1, length2; + + // Length is tored as 1 or 2 characters of `sizeofChar` size + if (sizeOfChar == 1) { + // UTF-8 encoding, each character is a byte + length1 = chars[offset]; + length2 = chars[offset + 1]; + } else { + // UTF-16 encoding, each character is a short + length1 = (uint)((chars[offset]) | (chars[offset + 1] << 8)); + length2 = (uint)((chars[offset + 2]) | (chars[offset + 3] << 8)); + } + + uint length; + uint nbytes; + if ((length1 & highBit) != 0) { + length = ((length1 & ~highBit) << (8 * (int)sizeOfChar)) | length2; + nbytes = sizeOfTwoChars; + } else { + length = length1; + nbytes = sizeOfChar; + } + + // 8 bit strings: maximum of 0x7FFF bytes, http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/ResourceTypes.cpp#692 + // 16 bit strings: maximum of 0x7FFFFFF bytes, http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/ResourceTypes.cpp#670 + if (sizeOfChar == 1) { + if (length > 0x7fff) { + throw new InvalidDataException ("UTF-8 string is too long. Offset: {offset}"); + } + } else { + if (length > 0x7fffffff) { + throw new InvalidDataException ("UTF-16 string is too long. Offset: {offset}"); + } + } + + return (length, nbytes); + } +} + +class ARSCHeader +{ + // This is the minimal size such a header must have. There might be other header data too! + const long MinimumSize = 2 + 2 + 4; + + long start; + uint size; + ushort type; + ushort headerSize; + bool unknownType; + + public ChunkType Type => unknownType ? ChunkType.Null : (ChunkType)type; + public ushort TypeRaw => type; + public ushort HeaderSize => headerSize; + public uint Size => size; + public long End => start + (long)size; + + public ARSCHeader (Stream data, ChunkType? expectedType = null) + { + start = data.Position; + if (data.Length < start + MinimumSize) { + throw new InvalidDataException ($"Input data not large enough. Offset: {start}"); + } + + // Data in AXML is little-endian, which is fortuitous as that's the only format BinaryReader understands. + using BinaryReader reader = new BinaryReader (data, Encoding.UTF8, leaveOpen: true); + + // ushort: type + // ushort: header_size + // uint: size + type = reader.ReadUInt16 (); + headerSize = reader.ReadUInt16 (); + + // Total size of the chunk, including the header + size = reader.ReadUInt32 (); + + if (expectedType != null && type != (ushort)expectedType) { + throw new InvalidOperationException ($"Header type is not equal to the expected type ({expectedType}): got 0x{type:x}, expected 0x{(ushort)expectedType:x}"); + } + + unknownType = !Enum.IsDefined (typeof(ChunkType), type); + + if (headerSize < MinimumSize) { + throw new InvalidDataException ($"Declared header size is smaller than required size of {MinimumSize}. Offset: {start}"); + } + + if (size < MinimumSize) { + throw new InvalidDataException ($"Declared chunk size is smaller than required size of {MinimumSize}. Offset: {start}"); + } + + if (size < headerSize) { + throw new InvalidDataException ($"Declared chunk size ({size}) is smaller than header size ({headerSize})! Offset: {start}"); + } + } +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/ApplicationConfigShim.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/ApplicationConfigShim.cs new file mode 100644 index 00000000000..7084e86af79 --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/ApplicationConfigShim.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace Microsoft.Android.AppTools; + +sealed class ApplicationConfigShim +{ + // Must be kept in sync with eponymous structure in src/monodroid/jni/xamarin-app.hh + [Flags] + enum MonoComponent + { + None = 0x00, + Debugger = 0x01, + HotReload = 0x02, + Tracing = 0x04, + }; + + const string NotSupportedInThisVersion = "Not supported in this version"; + + public string UsesMonoLlvm { get; } = NotSupportedInThisVersion; + public string UsesMonoAot { get; } = NotSupportedInThisVersion; + public string AotLazyLoad { get; } = NotSupportedInThisVersion; + public string UsesAssemblyPreload { get; } = NotSupportedInThisVersion; + public string BrokenExceptionTransitions { get; } = NotSupportedInThisVersion; + public string InstantRunEnabled { get; } = NotSupportedInThisVersion; + public string JniAddNativeMethodRegistrationAttributePresent { get; } = NotSupportedInThisVersion; + public string HaveRuntimeConfigBlob { get; } = NotSupportedInThisVersion; + public string HaveAssembliesBlob { get; } = NotSupportedInThisVersion; + public string MarshalMethodsEnabled { get; } = NotSupportedInThisVersion; + public string BoundStreamIoExceptionType { get; } = NotSupportedInThisVersion; + public string PackageNamingPolicy { get; } = NotSupportedInThisVersion; + public string EnvironmentVariableCount { get; } = NotSupportedInThisVersion; + public string SystemPropertyCount { get; } = NotSupportedInThisVersion; + public string NumberOfAssembliesInApk { get; } = NotSupportedInThisVersion; + public string BundledAssemblyNameWidth { get; } = NotSupportedInThisVersion; + public string NumberOfAssemblyStoreFiles { get; } = NotSupportedInThisVersion; + public string NumberOfDsoCacheEntries { get; } = NotSupportedInThisVersion; + public string AndroidRuntimeJnienvClassToken { get; } = NotSupportedInThisVersion; + public string JnienvInitializeMethodToken { get; } = NotSupportedInThisVersion; + public string JnienvRegisterjninativesMethodToken { get; } = NotSupportedInThisVersion; + public string JniRemappingReplacementTypeCount { get; } = NotSupportedInThisVersion; + public string JniRemappingReplacementMethodIndexEntryCount { get; } = NotSupportedInThisVersion; + public string MonoComponentsMask { get; } = NotSupportedInThisVersion; + public string AndroidPackageName { get; } = NotSupportedInThisVersion; + + public ApplicationConfigCommon NativeAppConfig { get; } + public ulong AppConfigFormatTag { get; } + + public ApplicationConfigShim (ApplicationConfig_V1 appConfig) + { + NativeAppConfig = appConfig; + AppConfigFormatTag = Constants.FormatTag_V1; + } + + public ApplicationConfigShim (ApplicationConfig_V2 appConfig) + { + NativeAppConfig = appConfig; + AppConfigFormatTag = Constants.FormatTag_V2; + + UsesMonoLlvm = Utils.YesNo (appConfig.uses_mono_llvm); + UsesMonoAot = Utils.YesNo (appConfig.uses_mono_aot); + AotLazyLoad = Utils.YesNo (appConfig.aot_lazy_load); + UsesAssemblyPreload = Utils.YesNo (appConfig.uses_assembly_preload); + BrokenExceptionTransitions = Utils.YesNo (appConfig.broken_exception_transitions); + InstantRunEnabled = Utils.YesNo (appConfig.instant_run_enabled ); + JniAddNativeMethodRegistrationAttributePresent = Utils.YesNo (appConfig.jni_add_native_method_registration_attribute_present); + HaveRuntimeConfigBlob = Utils.YesNo (appConfig.have_runtime_config_blob); + HaveAssembliesBlob = Utils.YesNo (appConfig.have_assemblies_blob); + MarshalMethodsEnabled = Utils.YesNo (appConfig.marshal_methods_enabled); + BoundStreamIoExceptionType = FormatInt (appConfig.bound_stream_io_exception_type); + PackageNamingPolicy = FormatInt (appConfig.package_naming_policy); + EnvironmentVariableCount = FormatInt (appConfig.environment_variable_count); + SystemPropertyCount = FormatInt (appConfig.system_property_count); + NumberOfAssembliesInApk = FormatInt (appConfig.number_of_assemblies_in_apk); + BundledAssemblyNameWidth = FormatInt (appConfig.bundled_assembly_name_width); + NumberOfAssemblyStoreFiles = FormatInt (appConfig.number_of_assembly_store_files); + NumberOfDsoCacheEntries = FormatInt (appConfig.number_of_dso_cache_entries); + AndroidRuntimeJnienvClassToken = FormatToken (appConfig.android_runtime_jnienv_class_token); + JnienvInitializeMethodToken = FormatToken (appConfig.jnienv_initialize_method_token); + JnienvRegisterjninativesMethodToken = FormatToken (appConfig.jnienv_registerjninatives_method_token); + JniRemappingReplacementTypeCount = FormatInt (appConfig.jni_remapping_replacement_type_count); + JniRemappingReplacementMethodIndexEntryCount = FormatInt (appConfig.jni_remapping_replacement_method_index_entry_count); + MonoComponentsMask = $"{FormatMonoComponentMask((MonoComponent)appConfig.mono_components_mask)} 0x{appConfig.mono_components_mask:x}"; + AndroidPackageName = appConfig.android_package_name; + } + + static string FormatInt (uint v) => v.ToString (CultureInfo.InvariantCulture); + + static string FormatToken (uint token) => $"0x{token:x}"; + + static string FormatMonoComponentMask (MonoComponent mask) + { + var items = new List (); + + if (mask.HasFlag (MonoComponent.Debugger)) { + items.Add ("Debugger"); + } + + if (mask.HasFlag (MonoComponent.HotReload)) { + items.Add ("HotReload"); + } + + if (mask.HasFlag (MonoComponent.Tracing)) { + items.Add ("Tracing"); + } + + return String.Join (", ", items); + } +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/ApplicationConfigs.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/ApplicationConfigs.cs new file mode 100644 index 00000000000..316c63eefa7 --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/ApplicationConfigs.cs @@ -0,0 +1,95 @@ +using System; +using System.IO; + +using ELFSharp.ELF.Sections; +using Microsoft.Android.AppTools.Native; + +namespace Microsoft.Android.AppTools; + +#pragma warning disable 0649 + +abstract class ApplicationConfigCommon +{ + protected bool Is64Bit { get; } + + protected Stream GetStream (byte[] data) => new MemoryStream (data); + + protected ApplicationConfigCommon (bool is64Bit) + { + Is64Bit = is64Bit; + } +} + +sealed class ApplicationConfig_V1 : ApplicationConfigCommon +{ + public ApplicationConfig_V1 (byte[] data, AnELF elf, ISymbolEntry symbolEntry) + : base (elf.Is64Bit) + {} +} + +sealed class ApplicationConfig_V2 : ApplicationConfigCommon +{ + public readonly bool uses_mono_llvm; + public readonly bool uses_mono_aot; + public readonly bool aot_lazy_load; + public readonly bool uses_assembly_preload; + public readonly bool broken_exception_transitions; + public readonly bool instant_run_enabled ; + public readonly bool jni_add_native_method_registration_attribute_present; + public readonly bool have_runtime_config_blob; + public readonly bool have_assemblies_blob; + public readonly bool marshal_methods_enabled; + public readonly byte bound_stream_io_exception_type; + public readonly uint package_naming_policy; + public readonly uint environment_variable_count; + public readonly uint system_property_count; + public readonly uint number_of_assemblies_in_apk; + public readonly uint bundled_assembly_name_width; + public readonly uint number_of_assembly_store_files; + public readonly uint number_of_dso_cache_entries; + public readonly uint android_runtime_jnienv_class_token; + public readonly uint jnienv_initialize_method_token; + public readonly uint jnienv_registerjninatives_method_token; + public readonly uint jni_remapping_replacement_type_count; + public readonly uint jni_remapping_replacement_method_index_entry_count; + public readonly uint mono_components_mask; + public readonly string android_package_name = String.Empty; + + public ApplicationConfig_V2 (byte[] data, AnELF elf, ISymbolEntry symbolEntry) + : base (elf.Is64Bit) + { + using Stream stream = GetStream (data); + using var reader = new BinaryReader (stream); + + ulong sizeSoFar = 0; + + sizeSoFar += NativeHelpers.ReadField (reader, ref uses_mono_llvm, sizeSoFar, Is64Bit); + sizeSoFar += NativeHelpers.ReadField (reader, ref uses_mono_aot, sizeSoFar, Is64Bit); + sizeSoFar += NativeHelpers.ReadField (reader, ref aot_lazy_load, sizeSoFar, Is64Bit); + sizeSoFar += NativeHelpers.ReadField (reader, ref uses_assembly_preload, sizeSoFar, Is64Bit); + sizeSoFar += NativeHelpers.ReadField (reader, ref broken_exception_transitions, sizeSoFar, Is64Bit); + sizeSoFar += NativeHelpers.ReadField (reader, ref instant_run_enabled, sizeSoFar, Is64Bit); + sizeSoFar += NativeHelpers.ReadField (reader, ref jni_add_native_method_registration_attribute_present, sizeSoFar, Is64Bit); + sizeSoFar += NativeHelpers.ReadField (reader, ref have_runtime_config_blob, sizeSoFar, Is64Bit); + sizeSoFar += NativeHelpers.ReadField (reader, ref have_assemblies_blob, sizeSoFar, Is64Bit); + sizeSoFar += NativeHelpers.ReadField (reader, ref marshal_methods_enabled, sizeSoFar, Is64Bit); + sizeSoFar += NativeHelpers.ReadField (reader, ref bound_stream_io_exception_type, sizeSoFar, Is64Bit); + sizeSoFar += NativeHelpers.ReadField (reader, ref package_naming_policy, sizeSoFar, Is64Bit); + sizeSoFar += NativeHelpers.ReadField (reader, ref environment_variable_count, sizeSoFar, Is64Bit); + sizeSoFar += NativeHelpers.ReadField (reader, ref system_property_count, sizeSoFar, Is64Bit); + sizeSoFar += NativeHelpers.ReadField (reader, ref number_of_assemblies_in_apk, sizeSoFar, Is64Bit); + sizeSoFar += NativeHelpers.ReadField (reader, ref bundled_assembly_name_width, sizeSoFar, Is64Bit); + sizeSoFar += NativeHelpers.ReadField (reader, ref number_of_assembly_store_files, sizeSoFar, Is64Bit); + sizeSoFar += NativeHelpers.ReadField (reader, ref number_of_dso_cache_entries, sizeSoFar, Is64Bit); + sizeSoFar += NativeHelpers.ReadField (reader, ref android_runtime_jnienv_class_token, sizeSoFar, Is64Bit); + sizeSoFar += NativeHelpers.ReadField (reader, ref jnienv_initialize_method_token, sizeSoFar, Is64Bit); + sizeSoFar += NativeHelpers.ReadField (reader, ref jnienv_registerjninatives_method_token, sizeSoFar, Is64Bit); + sizeSoFar += NativeHelpers.ReadField (reader, ref jni_remapping_replacement_type_count, sizeSoFar, Is64Bit); + sizeSoFar += NativeHelpers.ReadField (reader, ref jni_remapping_replacement_method_index_entry_count, sizeSoFar, Is64Bit); + sizeSoFar += NativeHelpers.ReadField (reader, ref mono_components_mask, sizeSoFar, Is64Bit); + + android_package_name = elf.GetStringFromPointerField (symbolEntry, sizeSoFar) ?? "FAILED TO READ STRING FROM BINARY"; + } +} + +#pragma warning restore 0649 diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/ApplicationInfo.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/ApplicationInfo.cs new file mode 100644 index 00000000000..a21f357617c --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/ApplicationInfo.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.IO; + +using Microsoft.Android.AppTools.Assemblies; +using Xamarin.Android.Tools; + +namespace Microsoft.Android.AppTools; + +/// +/// Main application information class. Gathers information about the application, or components thereof, +/// regardless of whether the data is gathered from an application archive or just a single file (e.g. +/// the assembly store) that makes part of the application. +/// +public class ApplicationInfo +{ + readonly ILogger log; + + /// + /// If information was obtained from an application archive (`.apk`, `.aab` or `.zip`), this + /// property will be set accordingly to a value different than + /// + public ArchiveKind ArchiveKind { get; private set; } = ArchiveKind.None; + + public ApplicationRuntime RuntimeKind { get; private set; } = ApplicationRuntime.Unknown; + + /// + /// If assembly stores were read, either from the archive or directly from a file on the filesystem, + /// this property will contain instances of the class describing the + /// stores in detail. If the collection isn't `null`, it is also guaranteed not to be empty. + /// + public ICollection? AssemblyStores { get; private set; } + + /// + /// If application info was obtained from an application archive (`.apk`, `.aab` or `.zip`) or + /// from `AndroidManifest.xml`, this property will contain the application's package name (if + /// found in the manifest). + /// + public string? PackageName { get; private set; } + + /// + /// If application info was obtained from an application archive (`.apk`, `.aab` or `.zip`) or + /// by directly reading a `.so` shared library, this collection will contain information about + /// all the shared libraries for all the target architectures supported by the application. + /// + public ICollection? SharedLibraries { get; private set; } + + /// + /// If application info was obtained from an application archive (`.apk`, `.aab` or `.zip`) or + /// by directly reading a `.so` shared library, this collection will contain all the architectures + /// targeted by the application. + /// + public ICollection? TargetArchitectures { get; private set; } + + public ApplicationInfo (ILogger log) + { + this.log = log; + } + + /// + /// Reads application information from the file passed in the `inputFilePath` parameter. The file + /// doesn't have to be application .apk or .aab archive, it can be any file that is (or is not) + /// part of the application. If the file is unrecognized, unsupported etc, the constructor will + /// make a note of it and initialize the class accordingly. All the issues will be reported via + /// calls to members of the `ILogger` interface, passed in the `log` parameter to the class + /// constructor. Returns `true` if there was any valid information read, `false` otherwise. + /// + public bool Read (string inputFilePath) + { + (ApplicationRuntime runtimeKind, FileFormat format, FileInfo? info) = Utils.DetectFileFormat (log, inputFilePath); + if (info == null || format == FileFormat.Unknown) { + return false; + } + + RuntimeKind = runtimeKind; + ArchiveKind = format switch { + FileFormat.Aab => ArchiveKind.AAB, + FileFormat.Apk => ArchiveKind.APK, + FileFormat.Zip => ArchiveKind.ZIP, + _ => ArchiveKind.None + }; + + IList? explorers = AssemblyStoreExplorer.Open (log, inputFilePath, format, info); + if (explorers != null) { + var stores = new List (); + foreach (AssemblyStoreExplorer exp in explorers) { + stores.Add (new AssemblyStore (log, exp)); + } + AssemblyStores = stores; + } + + return true; + } +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/ApplicationRuntime.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/ApplicationRuntime.cs new file mode 100644 index 00000000000..a73c5910846 --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/ApplicationRuntime.cs @@ -0,0 +1,9 @@ +namespace Microsoft.Android.AppTools; + +public enum ApplicationRuntime +{ + Unknown, + CoreCLR, + MonoVM, + NativeAOT, +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/ArchiveKind.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/ArchiveKind.cs new file mode 100644 index 00000000000..c1ca4f850b8 --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/ArchiveKind.cs @@ -0,0 +1,9 @@ +namespace Microsoft.Android.AppTools; + +public enum ArchiveKind +{ + None, + APK, + AAB, + ZIP, +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/Constants.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/Constants.cs new file mode 100644 index 00000000000..27bbbd90749 --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/Constants.cs @@ -0,0 +1,30 @@ +namespace Microsoft.Android.AppTools; + +sealed class Constants +{ + // Symbols in libxamarin-app.so + public sealed class SymbolNames + { + public const string ApplicationConfig = "application_config"; + public const string DSOCache = "dso_cache"; + public const string EnvironmentVariables = "app_environment_variables"; + public const string FormatTag = "format_tag"; + public const string MarshalMethodsClassCache = "marshal_methods_class_cache"; + public const string MarshalMethodsClassNames = "mm_class_names"; + public const string MarshalMethodsMethodNames = "mm_method_names"; + public const string MarshalMethodsNumberOfClasses = "marshal_methods_number_of_classes"; + public const string MarshalMethodsXamarinAppInit = "xamarin_app_init"; + public const string MonoAotModeName = "mono_aot_mode_name"; + public const string SystemProperties = "app_system_properties"; + } + + // Correspond to the `FORMAT_TAG` constant in src/monodroid/xamarin-app.hh + public const ulong FormatTag_V1 = 0x015E6972616D58; + public const ulong FormatTag_V2 = 0x00026E69726D6158; + + public const uint CompressedDataMagicInt = 0x5A4C4158; // 'XALZ', little-endian + public static readonly byte[] CompressedDataMagic = { 0x58, 0x41, 0x4c, 0x5a }; // 'XALZ', little-endian + + public const string UnableToLoadDataForPointer = "[unable to load data a pointer indicates]"; + public const string ItemUnsupported = "unsupported"; +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/DataProvider.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/DataProvider.cs new file mode 100644 index 00000000000..7c180b68b7b --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/DataProvider.cs @@ -0,0 +1,32 @@ +using System.IO; + +namespace Microsoft.Android.AppTools; + +interface IDataProvider +{ + public string? InputPath { get; } +} + +abstract class DataProvider : IDataProvider +{ + protected ILogger Log { get; } + protected Stream? InputStream { get; } + public string? InputPath { get; } + + protected DataProvider (ILogger log) + { + Log = log; + } + + protected DataProvider (string inputPath, ILogger log) + : this (log) + { + InputPath = inputPath; + } + + protected DataProvider (Stream inputStream, string? inputPath, ILogger log) + : this (inputPath ?? "[STREAM]", log) + { + InputStream = inputStream; + } +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/DataProviderAppInfo.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/DataProviderAppInfo.cs new file mode 100644 index 00000000000..44ffcfdb6ab --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/DataProviderAppInfo.cs @@ -0,0 +1,226 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Xml; + +using Microsoft.Android.AppTools.Assemblies; +using Xamarin.Tools.Zip; + +namespace Microsoft.Android.AppTools; + +class DataProviderAppInfo : DataProvider +{ + const string SignatureDirPath = "META-INF"; + + InputReaderZip reader; + + public string ApplicationName { get; private set; } = String.Empty; + public string ApplicationLabel { get; private set; } = String.Empty; + public string ArchiveType => reader.ArchiveType; + public bool ExtractsNativeLibs { get; private set; } + public bool HasRuntimeConfigBlob { get; private set; } + public bool HasAssemblyStoresManifest { get; private set; } + public bool IsClassicXA { get; private set; } + public bool IsDebug { get; private set; } + public bool IsProfileable { get; private set; } + public bool IsSigned { get; private set; } + public bool IsTestOnly { get; private set; } + public string MainActivityName { get; private set; } = String.Empty; + public string MinSdkVersion { get; private set; } = String.Empty; + public uint NumberOfAbiAssemblyStores { get; private set; } = 0; + public string PackageName { get; private set; } = String.Empty; + public string[] SupportedAbis { get; private set; } + public string TargetSdkVersion { get; private set; } = String.Empty; + public uint TotalNumberOfAssemblyStores { get; private set; } = 0; + public bool UsesAOT { get; private set; } + public bool UsesAssemblyStores { get; private set; } + public ICollection UsesPermissions { get; private set; } = new List (); + + public DataProviderAppInfo (InputReaderZip reader, ILogger log) + : base (reader.ArchivePath, log) + { + this.reader = reader; + + HasRuntimeConfigBlob = HasFile (reader.AssembliesDirPath, "rc.bin"); + // TODO: full IsClassicXA detection + if (!HasRuntimeConfigBlob) { + } + + XmlDocument? manifest = LoadManifest (log, out XmlNamespaceManager? nsManager); + if (manifest == null) { + log.WarningLine ("Unable to parse Android manifest from the apk"); + } else { + if (nsManager == null) { + throw new InvalidOperationException ("Internal error: nsManager must not be null"); + } + + // TODO: some strings can refer to resources, parse them + XmlNode? node = manifest.SelectSingleNode ("//manifest", nsManager); + PackageName = GetAttributeValue (node, "package") ?? String.Empty; + + node = manifest.SelectSingleNode ("//manifest/uses-sdk", nsManager); + MinSdkVersion = GetAttributeValue (node, "android:minSdkVersion") ?? String.Empty; + TargetSdkVersion = GetAttributeValue (node, "android:targetSdkVersion") ?? String.Empty; + + node = manifest.SelectSingleNode ("//manifest/application", nsManager); + IsDebug = GetBoolAttributeValue (node, "android:debuggable"); + IsTestOnly = GetBoolAttributeValue (node, "android:testOnly"); + ExtractsNativeLibs = GetBoolAttributeValue (node, "android:extractNativeLibs"); + ApplicationName = GetAttributeValue (node, "android:name") ?? String.Empty; + ApplicationLabel = GetAttributeValue (node, "android:label") ?? String.Empty; + + node = manifest.SelectSingleNode ("//manifest/application/profileable", nsManager); + IsProfileable = GetBoolAttributeValue (node, "android:enabled"); + + node = manifest.SelectSingleNode ("//manifest/application/activity[./intent-filter/action[@android:name='android.intent.action.MAIN']]", nsManager); + MainActivityName = GetAttributeValue (node, "android:name") ?? String.Empty; + + CollectPermissions (manifest, nsManager, UsesPermissions); + } + + IsSigned = HasFile (SignatureDirPath, "ANDROIDD.RSA") || HasFile (SignatureDirPath, "BNDLTOOL.RSA"); + UsesAssemblyStores = HasFile (reader.AssembliesDirPath, "assemblies.blob"); + if (UsesAssemblyStores) { + TotalNumberOfAssemblyStores = 1; + HasAssemblyStoresManifest = HasFile (reader.AssembliesDirPath, "assemblies.manifest"); + } + + string libDirLead = $"{reader.NativeLibsDirPath}/"; + string assembliesDirLead = $"{reader.AssembliesDirPath}/"; + + var abis = new HashSet (StringComparer.Ordinal); + + foreach (ZipEntry entry in reader.Archive) { + if (UsesAssemblyStores) { + if (entry.FullName.StartsWith (assembliesDirLead, StringComparison.Ordinal) && IsAssemblyAbiBlob (entry.FullName)) { + TotalNumberOfAssemblyStores++; + NumberOfAbiAssemblyStores++; + } + } + + if (!entry.FullName.StartsWith (libDirLead, StringComparison.Ordinal)) { + continue; + } + + string? dir = Path.GetDirectoryName (entry.FullName); + string? file = Path.GetFileName (entry.FullName); + + if (String.IsNullOrEmpty (dir) || String.IsNullOrEmpty (file)) { + continue; + } + + string? abi = Path.GetFileName (dir); + if (!abis.Contains (abi!)) { + abis.Add (abi!); + } + + if (!UsesAOT && file.StartsWith ("libaot-", StringComparison.Ordinal)) { + UsesAOT = true; + } + } + + SupportedAbis = new string[abis.Count]; + abis.CopyTo (SupportedAbis); + } + + XmlDocument? LoadManifest (ILogger log, out XmlNamespaceManager? nsManager) + { + nsManager = null; + + // TODO: implement support for AAB AndroidManifest.xml, it's not in the same binary XML format as in the APK. It can be dumped with: bundletool dump manifest --bundle + string manifestPath = MakeZipPath (reader.ManifestDirPath, InputReaderZip.AndroidManifestName); + log.DebugLine ($"Trying to load and parse Android manifest from the archive: {manifestPath}"); + + ZipEntry manifestEntry = reader.Archive.ReadEntry (manifestPath); + using var manifestData = new MemoryStream (); + manifestEntry.Extract (manifestData); + manifestData.Seek (0, SeekOrigin.Begin); + + XmlDocument? manifest; + try { + var axml = new AXMLParser (manifestData, log); + manifest = axml.Parse (); + if (manifest == null) { + return null; + } + } catch (Exception ex) { + log.DebugLine ("Failed to parse Android manifest."); + log.DebugLine (ex.ToString ()); + return null; + } + + XmlNode? node = manifest.SelectSingleNode ("//manifest"); + if (node == null) { + log.ErrorLine ("Unable to find root element 'manifest' of AndroidManifest.xml"); + return null; + } + + nsManager = new XmlNamespaceManager (manifest.NameTable); + if (node.Attributes != null) { + const string nsPrefix = "xmlns:"; + + foreach (XmlAttribute attr in node.Attributes) { + if (!attr.Name.StartsWith (nsPrefix, StringComparison.Ordinal)) { + continue; + } + + nsManager.AddNamespace (attr.Name.Substring (nsPrefix.Length), attr.Value); + } + } + + return manifest; + } + + static void CollectPermissions (XmlNode manifest, XmlNamespaceManager nsManager, ICollection permissions) + { + XmlNodeList? nodes = manifest.SelectNodes ("//manifest/uses-permission", nsManager); + if (nodes == null) { + return; + } + + foreach (XmlNode node in nodes) { + string? permission = GetAttributeValue (node, "android:name"); + if (String.IsNullOrEmpty (permission)) { + continue; + } + + permissions.Add (permission); + } + } + + static bool GetBoolAttributeValue (XmlNode? node, string prefixedAttributeName, bool defaultValue = false) + { + string? val = GetAttributeValue (node, prefixedAttributeName)?.ToLowerInvariant (); + if (String.IsNullOrEmpty (val) || !Boolean.TryParse (val, out bool ret)) { + return defaultValue; + } + + return ret; + } + + static string? GetAttributeValue (XmlNode? node, string prefixedAttributeName) + { + if (node?.Attributes == null) { + return null; + } + + foreach (XmlAttribute attr in node.Attributes) { + if (String.Compare (prefixedAttributeName, attr.Name, StringComparison.Ordinal) == 0) { + return attr.Value; + } + } + + return null; + } + + bool IsAssemblyAbiBlob (string entryName) => false /*!String.IsNullOrEmpty (AssemblyStoreReader.GetBlobArchitecture (entryName))*/; + + bool HasFile (string path, string fileName, bool caseSensitive = true) => reader.Archive.ContainsEntry (MakeZipPath (path, fileName), caseSensitive); + + static string MakeZipPath (string dirPath, string fileName) + { + if (String.IsNullOrEmpty (dirPath)) + return fileName; + return $"{dirPath}/{fileName}"; + } +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/DataProviderAssemblyStore.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/DataProviderAssemblyStore.cs new file mode 100644 index 00000000000..1b08295e197 --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/DataProviderAssemblyStore.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.IO; + +using Microsoft.Android.AppTools.Assemblies; + +namespace Microsoft.Android.AppTools; + +class DataProviderAssemblyStore : DataProvider +{ +// public List Blobs { get; } = new List (); +// public AssemblyStoreManifestReader? Manifest { get; private set; } + + public DataProviderAssemblyStore (Stream inputStream, string? inputPath, ILogger log) + : base (inputStream, inputPath, log) + { +// Blobs.Add (new AssemblyStoreReader (inputStream, inputPath, keepStoreInMemory: true)); + } + + public bool ExtractAssembly (string assemblyNameRegex, string outputDirectory, bool decompress) + { + return false; + } + + public void EnsureFullAssemblyInformation () + { +// foreach (AssemblyStoreReader blob in Blobs) { +// blob.EnsureAssemblyNames (Manifest); +// } + } + + public ICollection GetAssemblyNames () + { + var ret = new HashSet (); + +// foreach (AssemblyStoreReader blob in Blobs) { +// blob.EnsureAssemblyNames (Manifest); + +// foreach (AssemblyStoreAssembly asm in blob.Assemblies) { +// if (asm.Name.Length == 0 || ret.Contains (asm.Name)) { +// continue; +// } + +// ret.Add (asm.Name); +// } +// } + + return ret; + } +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/DataProviderTypemaps.MonoVM.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/DataProviderTypemaps.MonoVM.cs new file mode 100644 index 00000000000..abce3764080 --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/DataProviderTypemaps.MonoVM.cs @@ -0,0 +1,41 @@ +using System.IO; + +namespace Microsoft.Android.AppTools; + +interface IDataProviderTypemapsMonoVM : IDataProviderTypemaps +{ +} + +abstract class DataProviderTypemapsXamarinAppMonoVM : DataProviderXamarinAppMonoVM, IDataProviderTypemapsMonoVM +{ + public DataProviderTypemapsXamarinAppMonoVM (Stream inputStream, string? inputPath, ILogger log) + : base (inputStream, inputPath, log) + { + } + + public static IDataProviderTypemapsMonoVM? Create (Stream inputStream, string? inputPath, ILogger log) + { + // TODO: load ELF here, for detection + return null; + + // switch (format_tag) { + // case Constants.FormatTag_V1: + // return null; + + // case Constants.FormatTag_V2: + // return null; + + // default: + // //WarnNotSupported (elf, format_tag); + // return null; + // } + } +} + +class DataProviderTypemapsFastDev : DataProvider, IDataProviderTypemaps +{ + public DataProviderTypemapsFastDev (Stream inputStream, string? inputPath, ILogger log) + : base (inputStream, inputPath, log) + { + } +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/DataProviderXamarinApp.MonoVM.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/DataProviderXamarinApp.MonoVM.cs new file mode 100644 index 00000000000..ac9c3a78355 --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/DataProviderXamarinApp.MonoVM.cs @@ -0,0 +1,240 @@ +using System; +using System.Collections.Generic; +using System.IO; + +using ELFSharp.ELF; +using ELFSharp.ELF.Sections; +using Microsoft.Android.AppTools.Native; +using Xamarin.Android.Tasks; + +namespace Microsoft.Android.AppTools; + +class DataProviderXamarinAppMonoVM : DataProvider +{ + readonly AnELF elf; + readonly ulong format_tag; + + public string MachineArchitecture { get; } + + public DataProviderXamarinAppMonoVM (Stream inputStream, string? inputPath, ILogger log) + : base (inputStream, inputPath, log) + { + string filePath = inputPath ?? "[memory]"; + if (!AnELF.TryLoad (Log, inputStream, filePath, out AnELF? maybeELF) || maybeELF == null) { + throw new InvalidOperationException ($"Failed to load ELF image from '{filePath}'"); + } + + elf = maybeELF; + format_tag = GetFormatTag (elf); + + MachineArchitecture = elf.AnyELF.Machine switch { + Machine.AArch64 => "ARM64 (AArch64)", + Machine.AMD64 => "X86_64", + Machine.Intel386 => "X86 (i386)", + Machine.ARM => "ARM32 (ARM)", + _ => $"[unsupported] {elf.AnyELF.Machine}" + }; + } + + public MarshalMethods? GetMarshalMethods () + { + if (!MarshalMethods.Supported (elf, format_tag)) { + return null; + } + + return MarshalMethods.Create (Log, elf, format_tag); + } + + public string? GetAOTMode () + { + if (!elf.HasSymbol (Constants.SymbolNames.MonoAotModeName)) { + return null; + } + + byte[]? data = GetSymbolData (Constants.SymbolNames.MonoAotModeName, out ISymbolEntry? symbolEntry); + if (data == null || symbolEntry == null) { + return null; + } + + return elf.GetStringFromPointer (symbolEntry) ?? Constants.UnableToLoadDataForPointer; + } + + public DSOCacheMonoVM? GetDSOCache () + { + if (!elf.HasSymbol (Constants.SymbolNames.DSOCache)) { + return null; + } + + byte[]? data = GetSymbolData (Constants.SymbolNames.DSOCache, out ISymbolEntry? symbolEntry); + if (data == null || data.Length == 0 || symbolEntry == null) { + return null; + } + + return new DSOCacheMonoVM (Log, data, elf, symbolEntry); + } + + public IDictionary? GetSystemProperties () + { + return GetKeyValuePairs (Constants.SymbolNames.SystemProperties, "System properties"); + } + + public IDictionary? GetEnvironmentVariables () + { + return GetKeyValuePairs (Constants.SymbolNames.EnvironmentVariables, "Environment variables"); + } + + IDictionary? GetKeyValuePairs (string symbolName, string description) + { + if (!elf.HasSymbol (symbolName)) { + return null; + } + + byte[]? data = GetSymbolData (symbolName, out ISymbolEntry? symbolEntry); + if (data == null || data.Length == 0 || symbolEntry == null) { + return null; + } + + ulong pointerSize = (ulong)elf.PointerSize; + ulong nEntries = (ulong)data.Length / pointerSize; + bool oddNumberOfEntries = nEntries % 2 != 0; + + if (oddNumberOfEntries) { + Log.WarningLine ($" {description} array doesn't have an even number of elements"); + } + + ulong currentOffset = 0; + string? name; + string? value; + var dict = new SortedDictionary (); + + while (nEntries > 0) { + name = GetNextEntry (symbolEntry); + value = GetNextEntry (symbolEntry); + + if (dict.ContainsKey (name)) { + Log.WarningLine ($"Duplicate array entry '{name}' (value: '{value}')"); + continue; + } + dict.Add (name, value); + + string GetNextEntry (ISymbolEntry symbol) + { + string ret = elf.GetStringFromPointerField (symbol, currentOffset) ?? Constants.UnableToLoadDataForPointer; + currentOffset += pointerSize; + nEntries--; + + return ret; + } + } + + return dict; + } + + public ApplicationConfigShim? GetApplicationConfig () + { + if (!elf.HasSymbol (Constants.SymbolNames.ApplicationConfig)) { + return null; + } + + var applicationConfig = new ApplicationConfig (); + ulong size = 0; + + size += elf.GetPaddedSize (size, applicationConfig.uses_mono_llvm); + size += elf.GetPaddedSize (size, applicationConfig.uses_mono_aot); + size += elf.GetPaddedSize (size, applicationConfig.aot_lazy_load); + size += elf.GetPaddedSize (size, applicationConfig.uses_assembly_preload); + size += elf.GetPaddedSize (size, applicationConfig.broken_exception_transitions); + size += elf.GetPaddedSize (size, applicationConfig.jni_add_native_method_registration_attribute_present); + size += elf.GetPaddedSize (size, applicationConfig.have_runtime_config_blob); + size += elf.GetPaddedSize (size, applicationConfig.have_assemblies_blob); + size += elf.GetPaddedSize (size, applicationConfig.marshal_methods_enabled); + size += elf.GetPaddedSize (size, applicationConfig.bound_stream_io_exception_type); + size += elf.GetPaddedSize (size, applicationConfig.package_naming_policy); + size += elf.GetPaddedSize (size, applicationConfig.environment_variable_count); + size += elf.GetPaddedSize (size, applicationConfig.system_property_count); + size += elf.GetPaddedSize (size, applicationConfig.number_of_assemblies_in_apk); + size += elf.GetPaddedSize (size, applicationConfig.bundled_assembly_name_width); + size += elf.GetPaddedSize (size, applicationConfig.number_of_dso_cache_entries); + size += elf.GetPaddedSize (size, applicationConfig.android_runtime_jnienv_class_token); + size += elf.GetPaddedSize (size, applicationConfig.jnienv_initialize_method_token); + size += elf.GetPaddedSize (size, applicationConfig.jnienv_registerjninatives_method_token); + size += elf.GetPaddedSize (size, applicationConfig.jni_remapping_replacement_type_count); + size += elf.GetPaddedSize (size, applicationConfig.jni_remapping_replacement_method_index_entry_count); + size += elf.GetPaddedSize (size, applicationConfig.mono_components_mask); + size += elf.GetPaddedSize (size, applicationConfig.android_package_name); + + byte[]? data = GetSymbolData (Constants.SymbolNames.ApplicationConfig, out ISymbolEntry? symbolEntry); + if (data == null || symbolEntry == null) { + return null; + } + + switch (format_tag) { + case Constants.FormatTag_V1: + return GetApplicationConfig_V1 (size, data, symbolEntry); + + case Constants.FormatTag_V2: + return GetApplicationConfig_V2 (size, data, symbolEntry); + + default: + Log.WarningLine ($"libxamarin-app.so format 0x{format_tag:x} is not supported"); + return null; + } + } + + ApplicationConfigShim? GetApplicationConfig_V1 (ulong currentApplicationConfigSize, byte[] data, ISymbolEntry symbolEntry) + { + // Due to lack of consistent versioning, the latest "v1" binaries since commit 8bc7a3e84f95e70fe12790ac31ecd97957771cb2 are the same + // as the first V2 binaries. Earlier versions had different structure sizes, so if we find these sizes below, we can instead use + // the V2 loader safely. + const int ExpectedSize32_V2 = 68; + const int ExpectedSize64_V2 = 72; + + if (data.Length == ExpectedSize32_V2 || data.Length == ExpectedSize64_V2) { + Log.DebugLine ("Application config V1 with V2 structure size, forwarding to the V2 reader"); + return GetApplicationConfig_V2 (currentApplicationConfigSize, data, symbolEntry); + } + + Log.DebugLine ("Reading application config V1"); + var appConfig = new ApplicationConfig_V1 (data, elf, symbolEntry); + + return new ApplicationConfigShim (appConfig); + } + + ApplicationConfigShim? GetApplicationConfig_V2 (ulong currentApplicationConfigSize, byte[] data, ISymbolEntry symbolEntry) + { + const int ExpectedSize32 = 68; + const int ExpectedSize64 = 72; + + Log.DebugLine ("Reading application config V2"); + var appConfig = new ApplicationConfig_V2 (data, elf, symbolEntry); + + int expectedSize = elf.Is64Bit ? ExpectedSize64 : ExpectedSize32; + if (data.Length != expectedSize) { + Log.WarningLine ($"Failed to read '{Constants.SymbolNames.ApplicationConfig}' data from {InputPath} (expected {expectedSize}, got {data.Length})"); + return null; + } + + return new ApplicationConfigShim (appConfig); + } + + byte[]? GetSymbolData (string symbolName, out ISymbolEntry? symbolEntry) + { + byte[] data = elf.GetData (symbolName, out symbolEntry); + if (data.Length == 0 || symbolEntry == null) { + string reason = symbolEntry == null ? "not found" : "is empty"; + Log.DebugLine ($"Application config symbol '{symbolName}' {reason} in {InputPath}"); + return null; + } + + return data; + } + + ulong GetFormatTag (AnELF elfBinary) + { + if (!elfBinary.HasSymbol (Constants.SymbolNames.FormatTag)) { + return 0; + } + + return elf.GetUInt64 (Constants.SymbolNames.FormatTag); + } +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/ELFPayloadError.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/ELFPayloadError.cs new file mode 100644 index 00000000000..fb7ab23fa7d --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/ELFPayloadError.cs @@ -0,0 +1,11 @@ +namespace Microsoft.Android.AppTools; + +enum ELFPayloadError +{ + None, + NotELF, + LoadFailed, + NotSharedLibrary, + NotLittleEndian, + NoPayloadSection, +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/FileFormat.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/FileFormat.cs new file mode 100644 index 00000000000..14ba75c2e3b --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/FileFormat.cs @@ -0,0 +1,12 @@ +namespace Microsoft.Android.AppTools; + +enum FileFormat +{ + Aab, + AabBase, + Apk, + AssemblyStore, + ELF, + Zip, + Unknown, +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/IDataProviderTypemaps.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/IDataProviderTypemaps.cs new file mode 100644 index 00000000000..9473536c80d --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/IDataProviderTypemaps.cs @@ -0,0 +1,5 @@ +namespace Microsoft.Android.AppTools; + +interface IDataProviderTypemaps : IDataProvider +{ +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/ILogger.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/ILogger.cs new file mode 100644 index 00000000000..be2efcd4789 --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/ILogger.cs @@ -0,0 +1,27 @@ +using System; + +namespace Microsoft.Android.AppTools; + +public interface ILogger +{ + LogLevel Level { get; set; } + + void Message (string? message); + void MessageLine (string? message = null); + void Warning (string? message); + void WarningLine (string? message = null); + void Error (string? message); + void ErrorLine (string? message = null); + void Info (string? message); + void InfoLine (string? message = null); + void Debug (string? message); + void DebugLine (string? message = null); + void Verbose (string? message); + void VerboseLine (string? message = null); + void Status (string label, string text); + void StatusLine (string label, string text); + void Log (LogLevel level, string? message); + void LogLine (LogLevel level, string? message, ConsoleColor color); + void Log (LogLevel level, string? message, ConsoleColor color); + void ExceptionError (string desc, Exception ex); +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/InputReader.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/InputReader.cs new file mode 100644 index 00000000000..5cbc1827d09 --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/InputReader.cs @@ -0,0 +1,113 @@ +using System; +using System.IO; + +namespace Microsoft.Android.AppTools; + +abstract class InputReader +{ + readonly object providerCreatorLock = new object(); + + public abstract bool SupportsAssemblyExtraction { get; } + public abstract bool SupportsAssemblyStore { get; } + public abstract bool SupportsXamarinApp { get; } + public abstract bool SupportsTypemaps { get; } + public abstract bool SupportsAppInfo { get; } + + protected ILogger Log { get; } + + protected InputReader (ILogger log) + { + Log = log; + } + + protected virtual DataProviderAppInfo? ReadAppInfo () + { + throw new NotImplementedException ("Not implemented by this input reader"); + } + + protected virtual DataProviderXamarinAppMonoVM? ReadXamarinAppMonoVM () + { + throw new NotImplementedException ("Not implemented by this input reader"); + } + + protected virtual IDataProviderTypemaps? ReadTypemaps () + { + throw new NotImplementedException ("Not implemented by this input reader"); + } + + protected virtual DataProviderAssemblyStore? ReadAssemblyStore () + { + throw new NotImplementedException ("Not implemented by this input reader"); + } + + protected virtual bool DoExtractAssembly (string assemblyNameRegex, string outputDirectory, bool decompress) + { + throw new NotImplementedException ("Not implemented by this input reader"); + } + + public bool ExtractAssembly (string assemblyNameRegex, string outputDirectory, bool decompress = true) + { + if (!SupportsAssemblyExtraction) { + throw new NotSupportedException ("Assemlby extraction is not supported by this input reader"); + } + + return DoExtractAssembly (assemblyNameRegex, outputDirectory, decompress); + } + + public DataProviderAppInfo? GetAppInfo () + { + if (!SupportsAppInfo) { + throw new NotSupportedException ("Application information is not supported by this input reader"); + } + + return ReadAppInfo (); + } + + public DataProviderXamarinAppMonoVM? GetXamarinApp () + { + if (!SupportsXamarinApp) { + throw new NotSupportedException ("libxamarin-app.so is not supported by this input reader"); + } + + return ReadXamarinAppMonoVM (); + } + + public IDataProviderTypemaps? GetTypemaps () + { + if (!SupportsTypemaps) { + throw new NotSupportedException ("Type maps are not supported by this input reader"); + } + + return ReadTypemaps (); + } + + public DataProviderAssemblyStore? GetAssemblyStore () + { + if (!SupportsAssemblyStore) { + throw new NotSupportedException ("Assembly stores are not supported by this input reader"); + } + + return ReadAssemblyStore (); + } + + protected T? CreateProvider (string? filePath, ref Stream? stream, ref T? instance, Func createInstance) where T: class, IDataProvider + { + lock (providerCreatorLock) { + if (instance != null) { + return instance; + } + + if (stream == null) { + if (String.IsNullOrEmpty (filePath)) { + // TODO: log + return null; + } + + stream = File.OpenRead (filePath); + } + + instance = createInstance (stream, filePath, Log); + return instance; + } + } +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/InputReaderAab.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/InputReaderAab.cs new file mode 100644 index 00000000000..4563eeb162a --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/InputReaderAab.cs @@ -0,0 +1,17 @@ +using Xamarin.Tools.Zip; + +namespace Microsoft.Android.AppTools; + +class InputReaderAab : InputReaderZip +{ + public override string DexDirPath => "base/dex"; + public override string InternalAssetsDirPath => "base/assets/xa-internal"; + public override string ManifestDirPath => "base/manifest"; + public override string NativeLibsDirPath => "base/lib"; + public override string AssembliesDirPath => "base/root/assemblies"; + public override string ArchiveType =>"AAB"; + + public InputReaderAab (ZipArchive archive, string archivePath, ILogger log) + : base (archive, archivePath, log) + {} +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/InputReaderApk.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/InputReaderApk.cs new file mode 100644 index 00000000000..16b6e57636d --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/InputReaderApk.cs @@ -0,0 +1,19 @@ +using System; + +using Xamarin.Tools.Zip; + +namespace Microsoft.Android.AppTools; + +class InputReaderApk : InputReaderZip +{ + public override string DexDirPath => String.Empty; + public override string InternalAssetsDirPath => "assets/xa-internal"; + public override string ManifestDirPath => String.Empty; + public override string NativeLibsDirPath => "lib"; + public override string AssembliesDirPath => "assemblies"; + public override string ArchiveType =>"APK"; + + public InputReaderApk (ZipArchive archive, string archivePath, ILogger log) + : base (archive, archivePath, log) + {} +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/InputReaderAssemblyStore.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/InputReaderAssemblyStore.cs new file mode 100644 index 00000000000..6458c8f023d --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/InputReaderAssemblyStore.cs @@ -0,0 +1,51 @@ +using System; +using System.IO; + +namespace Microsoft.Android.AppTools; + +class InputReaderAssemblyStore : InputReader +{ + Stream? inputStream; + string? filePath; + DataProviderAssemblyStore? store; + + public override bool SupportsAssemblyExtraction => true; + public override bool SupportsAssemblyStore => true; + public override bool SupportsXamarinApp => false; + public override bool SupportsTypemaps => false; + public override bool SupportsAppInfo => false; + + public InputReaderAssemblyStore (string filePath, ILogger log) + : base (log) + { + this.filePath = filePath; + inputStream = null; + } + + public InputReaderAssemblyStore (Stream inputStream, string? filePath, ILogger log) + : base (log) + { + this.inputStream = inputStream; + filePath = null; + } + + protected override bool DoExtractAssembly (string assemblyNameRegex, string outputDirectory, bool decompress) + { + DataProviderAssemblyStore? assemblyStore = ReadAssemblyStore (); + if (assemblyStore == null) { + return false; + } + + throw new NotImplementedException (); + } + + protected override DataProviderAssemblyStore? ReadAssemblyStore () + { + return CreateProvider ( + filePath, + ref inputStream, + ref store, + (Stream s, string? path, ILogger logger) => new DataProviderAssemblyStore (s, path, logger) + ); + } +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/InputReaderXamarinApp.MonoVM.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/InputReaderXamarinApp.MonoVM.cs new file mode 100644 index 00000000000..e41b11ec299 --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/InputReaderXamarinApp.MonoVM.cs @@ -0,0 +1,51 @@ +using System.IO; + +namespace Microsoft.Android.AppTools; + +class InputReaderXamarinAppMonoVM : InputReader +{ + Stream? inputStream; + string? filePath; + DataProviderXamarinAppMonoVM? xamarinApp; + IDataProviderTypemapsMonoVM? typeMaps; + + public override bool SupportsAssemblyExtraction => false; + public override bool SupportsAssemblyStore => false; + public override bool SupportsXamarinApp => true; + public override bool SupportsTypemaps => true; + public override bool SupportsAppInfo => false; + + public InputReaderXamarinAppMonoVM (string filePath, ILogger log) + : base (log) + { + this.filePath = filePath; + inputStream = null; + } + + public InputReaderXamarinAppMonoVM (Stream inputStream, string? filePath, ILogger log) + : base (log) + { + this.inputStream = inputStream; + filePath = null; + } + + protected override DataProviderXamarinAppMonoVM? ReadXamarinAppMonoVM () + { + return CreateProvider ( + filePath, + ref inputStream, + ref xamarinApp, + (Stream s, string? path, ILogger logger) => new DataProviderXamarinAppMonoVM (s, path, logger) + ); + } + + protected override IDataProviderTypemaps? ReadTypemaps () + { + return CreateProvider ( + filePath, + ref inputStream, + ref typeMaps, + (Stream s, string? path, ILogger logger) => DataProviderTypemapsXamarinAppMonoVM.Create (s, path, logger) + ); + } +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/InputReaderZip.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/InputReaderZip.cs new file mode 100644 index 00000000000..581ec7155de --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/InputReaderZip.cs @@ -0,0 +1,58 @@ +using System; + +using Xamarin.Tools.Zip; + +namespace Microsoft.Android.AppTools; + +abstract class InputReaderZip : InputReader +{ + public const string AndroidManifestName = "AndroidManifest.xml"; + + public override bool SupportsAssemblyExtraction => true; + public override bool SupportsAssemblyStore => true; + public override bool SupportsXamarinApp => true; + public override bool SupportsTypemaps => true; + public override bool SupportsAppInfo => true; + + public ZipArchive Archive { get; } + public string ArchivePath { get; } + + public abstract string DexDirPath { get; } + public abstract string InternalAssetsDirPath { get; } + public abstract string ManifestDirPath { get; } + public abstract string NativeLibsDirPath { get; } + public abstract string AssembliesDirPath { get; } + public abstract string ArchiveType { get; } + + protected InputReaderZip (ZipArchive archive, string archivePath, ILogger log) + : base (log) + { + Archive = archive; + ArchivePath = archivePath; + } + + protected override bool DoExtractAssembly (string assemblyNameRegex, string outputDirectory, bool decompress ) + { + throw new NotImplementedException (); + } + + protected override DataProviderAppInfo? ReadAppInfo () + { + return new DataProviderAppInfo (this, Log); + } + + protected override DataProviderXamarinAppMonoVM? ReadXamarinAppMonoVM () + { + throw new NotImplementedException (); + } + + protected override IDataProviderTypemaps? ReadTypemaps () + { + throw new NotImplementedException (); + } + + protected override DataProviderAssemblyStore? ReadAssemblyStore () + { + return null; + } +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/LogLevel.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/LogLevel.cs new file mode 100644 index 00000000000..c463481365b --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/LogLevel.cs @@ -0,0 +1,11 @@ +namespace Microsoft.Android.AppTools; + +public enum LogLevel +{ + Error, + Warning, + Info, + Message, + Debug, + Verbose, +} diff --git a/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/Utils.cs b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/Utils.cs new file mode 100644 index 00000000000..1986c5e131b --- /dev/null +++ b/tools/Microsoft.Android.AppTools/Microsoft.Android.AppTools/Utils.cs @@ -0,0 +1,268 @@ +using System; +using System.IO; +using System.Buffers; + +using ELFSharp.ELF; +using ELFSharp.ELF.Sections; +using Xamarin.Android.Tasks; +using Xamarin.Tools.Zip; + +namespace Microsoft.Android.AppTools; + +static class Utils +{ + static readonly string[] aabZipEntries = { + "base/manifest/AndroidManifest.xml", + "BundleConfig.pb", + }; + + static readonly string[] aabBaseZipEntries = { + "manifest/AndroidManifest.xml", + }; + + static readonly string[] apkZipEntries = { + "AndroidManifest.xml", + }; + + static readonly string[] apkMonoRuntimeEntries = { + $"lib/{MonoAndroidHelper.AndroidAbi.Arm64}/libmonosgen-2.0.so", + $"lib/{MonoAndroidHelper.AndroidAbi.Arm32}/libmonosgen-2.0.so", + "lib/{MonoAndroidHelper.AndroidAbi.X86}/libmonosgen-2.0.so", + "lib/{MonoAndroidHelper.AndroidAbi.X64}/libmonosgen-2.0.so", + }; + + static readonly string[] aabMonoRuntimeEntries = { + $"lib/{MonoAndroidHelper.AndroidAbi.Arm64}/libmonosgen-2.0.so", + $"lib/{MonoAndroidHelper.AndroidAbi.Arm32}/libmonosgen-2.0.so", + "lib/{MonoAndroidHelper.AndroidAbi.X86}/libmonosgen-2.0.so", + "lib/{MonoAndroidHelper.AndroidAbi.X64}/libmonosgen-2.0.so", + }; + + static readonly string[] aabBaseMonoRuntimeEntries = { + $"lib/{MonoAndroidHelper.AndroidAbi.Arm64}/libmonosgen-2.0.so", + $"lib/{MonoAndroidHelper.AndroidAbi.Arm32}/libmonosgen-2.0.so", + "lib/{MonoAndroidHelper.AndroidAbi.X86}/libmonosgen-2.0.so", + "lib/{MonoAndroidHelper.AndroidAbi.X64}/libmonosgen-2.0.so", + }; + + static readonly string[] apkCoreCLRRuntimeEntries = { + $"lib/{MonoAndroidHelper.AndroidAbi.Arm64}/libcoreclr.so", + $"lib/{MonoAndroidHelper.AndroidAbi.Arm32}/libcoreclr.so", + "lib/{MonoAndroidHelper.AndroidAbi.X86}/libcoreclr.so", + "lib/{MonoAndroidHelper.AndroidAbi.X64}/libcoreclr.so", + }; + + static readonly string[] aabCoreCLRRuntimeEntries = { + $"lib/{MonoAndroidHelper.AndroidAbi.Arm64}/libcoreclr.so", + $"lib/{MonoAndroidHelper.AndroidAbi.Arm32}/libcoreclr.so", + "lib/{MonoAndroidHelper.AndroidAbi.X86}/libcoreclr.so", + "lib/{MonoAndroidHelper.AndroidAbi.X64}/libcoreclr.so", + }; + + static readonly string[] aabBaseCoreCLRRuntimeEntries = { + $"lib/{MonoAndroidHelper.AndroidAbi.Arm64}/libcoreclr.so", + $"lib/{MonoAndroidHelper.AndroidAbi.Arm32}/libcoreclr.so", + "lib/{MonoAndroidHelper.AndroidAbi.X86}/libcoreclr.so", + "lib/{MonoAndroidHelper.AndroidAbi.X64}/libcoreclr.so", + }; + + public const uint ZIP_MAGIC = 0x4034b50; + public const uint ASSEMBLY_STORE_MAGIC = 0x41424158; + public const uint ELF_MAGIC = 0x464c457f; + + public static readonly ArrayPool BytePool = ArrayPool.Shared; + public static ILogger? Log; + + public static (ulong offset, ulong size, ELFPayloadError error) FindELFPayloadSectionOffsetAndSize (Stream stream) + { + stream.Seek (0, SeekOrigin.Begin); + Class elfClass = ELFReader.CheckELFType (stream); + if (elfClass == Class.NotELF) { + return ReturnError (null, ELFPayloadError.NotELF); + } + + if (!ELFReader.TryLoad (stream, shouldOwnStream: false, out IELF? elf)) { + return ReturnError (elf, ELFPayloadError.LoadFailed); + } + + if (elf.Type != FileType.SharedObject) { + return ReturnError (elf, ELFPayloadError.NotSharedLibrary); + } + + if (elf.Endianess != ELFSharp.Endianess.LittleEndian) { + return ReturnError (elf, ELFPayloadError.NotLittleEndian); + } + + if (!elf.TryGetSection ("payload", out ISection? payloadSection)) { + return ReturnError (elf, ELFPayloadError.NoPayloadSection); + } + + bool is64 = elf.Machine switch { + Machine.ARM => false, + Machine.Intel386 => false, + + Machine.AArch64 => true, + Machine.AMD64 => true, + + _ => throw new NotSupportedException ($"Unsupported ELF architecture '{elf.Machine}'") + }; + + ulong offset; + ulong size; + + if (is64) { + (offset, size) = GetOffsetAndSize64 ((Section)payloadSection); + } else { + (offset, size) = GetOffsetAndSize32 ((Section)payloadSection); + } + + elf.Dispose (); + return (offset, size, ELFPayloadError.None); + + (ulong offset, ulong size) GetOffsetAndSize64 (Section payload) + { + return (payload.Offset, payload.Size); + } + + (ulong offset, ulong size) GetOffsetAndSize32 (Section payload) + { + return ((ulong)payload.Offset, (ulong)payload.Size); + } + + (ulong offset, ulong size, ELFPayloadError error) ReturnError (IELF? elf, ELFPayloadError error) + { + elf?.Dispose (); + + return (0, 0, error); + } + } + + public static (ApplicationRuntime runtimeKind, FileFormat format, FileInfo? info) DetectFileFormat (ILogger log, string path) + { + if (String.IsNullOrEmpty (path)) { + return (ApplicationRuntime.Unknown, FileFormat.Unknown, null); + } + + var info = new FileInfo (path); + if (!info.Exists) { + return (ApplicationRuntime.Unknown, FileFormat.Unknown, null); + } + + using var reader = new BinaryReader (info.OpenRead ()); + + // ATM, all formats we recognize have 4-byte magic at the start + FileFormat format = reader.ReadUInt32 () switch { + Utils.ZIP_MAGIC => FileFormat.Zip, + Utils.ELF_MAGIC => FileFormat.ELF, + Utils.ASSEMBLY_STORE_MAGIC => FileFormat.AssemblyStore, + _ => FileFormat.Unknown + }; + + if (format == FileFormat.Unknown || format != FileFormat.Zip) { + return (ApplicationRuntime.Unknown, format, info); + } + + (ApplicationRuntime runtimeKind, format) = DetectAndroidArchive (info, format); + return (runtimeKind, format, info); + } + + static (ApplicationRuntime runtimeKind, FileFormat format) DetectAndroidArchive (FileInfo info, FileFormat defaultFormat) + { + using var zip = ZipArchive.Open (info.FullName, FileMode.Open); + + if (HasAllEntries (zip, aabZipEntries)) { + return DetectRuntimeKind (zip, FileFormat.Aab); + } + + if (HasAllEntries (zip, apkZipEntries)) { + return DetectRuntimeKind (zip, FileFormat.Apk); + } + + if (HasAllEntries (zip, aabBaseZipEntries)) { + return DetectRuntimeKind (zip, FileFormat.AabBase); + } + + return (ApplicationRuntime.Unknown, defaultFormat); + } + + static (ApplicationRuntime runtimeKind, FileFormat format) DetectRuntimeKind (ZipArchive zip, FileFormat format) + { + ApplicationRuntime runtimeKind = format switch { + FileFormat.Aab => DetectAabRuntime (), + FileFormat.AabBase => DetectAabBaseRuntime (), + FileFormat.Apk => DetectApkRuntime (), + _ => ApplicationRuntime.Unknown + }; + + // TODO: detect statically linked libmonodroid.so + return (runtimeKind, format); + + ApplicationRuntime DetectApkRuntime () + { + if (HasAnyEntry (zip, apkMonoRuntimeEntries)) { + return ApplicationRuntime.MonoVM; + } + + if (HasAnyEntry (zip, apkCoreCLRRuntimeEntries)) { + return ApplicationRuntime.MonoVM; + } + + // TODO: NativeAOT + return ApplicationRuntime.Unknown; + } + + ApplicationRuntime DetectAabRuntime () + { + if (HasAnyEntry (zip, aabMonoRuntimeEntries)) { + return ApplicationRuntime.MonoVM; + } + + if (HasAnyEntry (zip, aabCoreCLRRuntimeEntries)) { + return ApplicationRuntime.MonoVM; + } + + // TODO: NativeAOT + return ApplicationRuntime.Unknown; + } + + ApplicationRuntime DetectAabBaseRuntime () + { + if (HasAnyEntry (zip, aabBaseMonoRuntimeEntries)) { + return ApplicationRuntime.MonoVM; + } + + if (HasAnyEntry (zip, aabBaseCoreCLRRuntimeEntries)) { + return ApplicationRuntime.MonoVM; + } + + // TODO: NativeAOT + return ApplicationRuntime.Unknown; + } + } + + static bool HasAnyEntry (ZipArchive zip, string[] entries) + { + foreach (string entry in entries) { + if (zip.ContainsEntry (entry, caseSensitive: true)) { + return true; + } + } + + return false; + } + + static bool HasAllEntries (ZipArchive zip, string[] entries) + { + foreach (string entry in entries) { + if (!zip.ContainsEntry (entry, caseSensitive: true)) { + return false; + } + } + + return true; + } + + public static string ToStringOrNull (T? reference) => reference == null ? "" : reference.ToString () ?? "[unknown]"; + public static string YesNo (bool yes) => yes ? "yes" : "no"; + public static string AreOrNot (bool are) => are ? "are" : "are not"; +} diff --git a/tools/xapp/ConsoleLogger.cs b/tools/xapp/ConsoleLogger.cs new file mode 100644 index 00000000000..d5508619d61 --- /dev/null +++ b/tools/xapp/ConsoleLogger.cs @@ -0,0 +1,214 @@ +using System; +using System.IO; + +using Microsoft.Android.AppTools; + +namespace Microsoft.Android.AppTools.XAPP; + +class ConsoleLogger : ILogger +{ + static readonly object consoleLock = new object (); + string? logFilePath = null; + string? logFileDir = null; + + public const ConsoleColor ErrorColor = ConsoleColor.Red; + public const ConsoleColor DebugColor = ConsoleColor.DarkGray; + public const ConsoleColor InfoColor = ConsoleColor.Green; + public const ConsoleColor MessageColor = ConsoleColor.Gray; + public const ConsoleColor WarningColor = ConsoleColor.Yellow; + public const ConsoleColor StatusLabel = ConsoleColor.White; + public const ConsoleColor StatusText = ConsoleColor.Cyan; + public const ConsoleColor StatusYes = ConsoleColor.Green; + public const ConsoleColor StatusNo = ConsoleColor.Red; + + public LogLevel Level { get; set; } = LogLevel.Message; + + public string? LogFilePath { + get => logFilePath; + set { + if (!String.IsNullOrEmpty (value)) { + string? dir = Path.GetDirectoryName (value); + if (!String.IsNullOrEmpty (dir)) { + Directory.CreateDirectory (dir); + } + } + + logFilePath = value; + logFileDir = Path.GetDirectoryName (value); + } + } + + public void Message (string? message) + { + Log (LogLevel.Message, message); + } + + public void MessageLine (string? message = null) + { + Message ($"{message ?? String.Empty}{Environment.NewLine}"); + } + + public void Warning (string? message) + { + Log (LogLevel.Warning, message); + } + + public void WarningLine (string? message = null) + { + Warning ($"{message ?? String.Empty}{Environment.NewLine}"); + } + + public void Error (string? message) + { + Log (LogLevel.Error, message); + } + + public void ErrorLine (string? message = null) + { + Error ($"{message ?? String.Empty}{Environment.NewLine}"); + } + + public void Info (string? message) + { + Log (LogLevel.Info, message); + } + + public void InfoLine (string? message = null) + { + Info ($"{message ?? String.Empty}{Environment.NewLine}"); + } + + public void Debug (string? message) + { + Log (LogLevel.Debug, message); + } + + public void DebugLine (string? message = null) + { + Debug ($"{message ?? String.Empty}{Environment.NewLine}"); + } + + public void Verbose (string? message) + { + Log (LogLevel.Verbose, message); + } + + public void VerboseLine (string? message = null) + { + Verbose ($"{message ?? String.Empty}{Environment.NewLine}"); + } + + void Status (string label, string text, ConsoleColor color) + { + Log (LogLevel.Info, $"{label}: ", StatusLabel); + Log (LogLevel.Info, text, color); + } + + public void Status (string label, string text) + { + Status (label, text, StatusText); + } + + public void Status (string label, IFormattable? val, string missingText = "missing") + { + Status (label, val?.ToString () ?? missingText); + } + + public void StatusLine () + { + Log (LogLevel.Info, Environment.NewLine); + } + + public void StatusLine (string label, string text) + { + Status (label, text); + StatusLine (); + } + + public void StatusLine (string label, IFormattable? val, string missingText = "missing") + { + Status (label, val); + StatusLine (); + } + + public void StatusYesNo (string label, bool yes) + { + Status (label, YesNo (yes), yes ? StatusYes : StatusNo); + } + + public void StatusYesNoLine (string label, bool yes) + { + StatusYesNo (label, yes); + StatusLine (); + } + + static string YesNo (bool yes) => yes ? "yes" : "no"; + + public void Log (LogLevel level, string? message) + { + if (level > Level) { + LogToFile (message); + return; + } + + Log (level, message, ForegroundColor (level)); + } + + public void LogLine (LogLevel level, string? message, ConsoleColor color) + { + Log (level, message, color); + Log (level, Environment.NewLine, color); + } + + public void Log (LogLevel level, string? message, ConsoleColor color) + { + LogToFile (message); + + if (level > Level) { + return; + } + + TextWriter writer = level == LogLevel.Error ? Console.Error : Console.Out; + message = message ?? String.Empty; + + ConsoleColor fg = ConsoleColor.Gray; + try { + lock (consoleLock) { + fg = Console.ForegroundColor; + Console.ForegroundColor = color; + } + + writer.Write (message); + } finally { + Console.ForegroundColor = fg; + } + } + + public void ExceptionError (string desc, Exception ex) + { + LogLine (LogLevel.Error, desc, ErrorColor); + LogLine (LogLevel.Error, ex.ToString (), ErrorColor); + } + + void LogToFile (string? message) + { + if (String.IsNullOrEmpty (LogFilePath)) { + return; + } + + if (!String.IsNullOrEmpty (logFileDir) && !Directory.Exists (logFileDir)) { + Directory.CreateDirectory (logFileDir); + } + + File.AppendAllText (LogFilePath, message); + } + + ConsoleColor ForegroundColor (LogLevel level) => level switch { + LogLevel.Error => ErrorColor, + LogLevel.Warning => WarningColor, + LogLevel.Info => InfoColor, + LogLevel.Debug => DebugColor, + LogLevel.Message => MessageColor, + _ => MessageColor, + }; +} diff --git a/tools/xapp/main.cs b/tools/xapp/main.cs new file mode 100644 index 00000000000..405d5ef7c91 --- /dev/null +++ b/tools/xapp/main.cs @@ -0,0 +1,106 @@ +using System.Collections.Generic; + +using Microsoft.Android.AppTools.Assemblies; +using Mono.Options; + +namespace Microsoft.Android.AppTools.XAPP; + +sealed class ParsedOptions +{ + public bool TypeMaps; + public bool AssemblyStoreContent; + public bool AssemblyDetails; + public bool ExtractAssemblies; +} + +class Xapp +{ + static readonly ConsoleLogger log = new (); + static readonly ParsedOptions parsedOptions = new (); + + static int Main (string[] args) + { + log.Level = LogLevel.Debug; + + var opts = new OptionSet { + "Usage: xapp [OPTIONS] [path/to/file ..]", + "", + "OPTIONS are:", + "", + { "t|typemaps", "Show detailed typemap information", v => parsedOptions.TypeMaps = true }, + { "s|assembly-store-content", "List names of all assemblies in assembly stores", v => parsedOptions.AssemblyStoreContent = true }, + { "a|assembly-details", "List details for each assembly", v => parsedOptions.AssemblyDetails = true }, + { "e|extract-assemblies", "Extract assemblies from the APK/AAB archive or assembly store blob", v => parsedOptions.ExtractAssemblies = true }, + }; + List rest = opts.Parse (args); + bool haveErrors = false; + + foreach (string inputFile in rest) { + var appInfo = new ApplicationInfo (log); + if (!appInfo.Read (inputFile)) { + haveErrors = true; + continue; + } + + log.StatusLine ("Input file path", inputFile); + log.StatusLine ("Archive kind", appInfo.ArchiveKind.ToString ()); + if (appInfo.ArchiveKind != ArchiveKind.None) { + log.StatusLine ("Runtime kind", appInfo.RuntimeKind); + } + + bool haveAssemblyStores = appInfo.AssemblyStores != null; + log.StatusYesNoLine ("Assembly store", haveAssemblyStores); + if (haveAssemblyStores) { + log.StatusLine ("Assembly store count", appInfo.AssemblyStores!.Count); + foreach (AssemblyStore store in appInfo.AssemblyStores!) { + log.InfoLine ($" {store.TargetArchitecture}"); + log.StatusLine ($" Assembly count", store.NumberOfAssemblies); + if (parsedOptions.AssemblyStoreContent) { + ListAssemblies (" ", store); + } + } + } + log.StatusLine (); + } + + return haveErrors ? 1 : 0; + } + + static void ListAssemblies (string indent, AssemblyStore store) + { + foreach (AssemblyStoreItem asm in store.Assemblies) { + log.MessageLine ($"{indent}{asm.Name}"); + if (!parsedOptions.AssemblyDetails) { + continue; + } + + log.StatusYesNoLine ($"{indent} 64-bit", asm.Is64Bit); + log.StatusLine ($"{indent} In-store size", asm.DataSize); + log.StatusLine ($"{indent} Assembly image offset", asm.DataOffset); + + if (asm.DebugSize == 0 && asm.ConfigSize == 0) { + log.MessageLine ($"{indent} Debug data and config file absent"); + } else { + if (asm.DebugSize == 0) { + log.MessageLine ($"{indent} Debug data absent"); + } else { + log.StatusLine ($"{indent} Debug data size", asm.DebugSize); + log.StatusLine ($"{indent} Debug data offset", asm.DebugOffset); + } + + if (asm.ConfigSize == 0) { + log.MessageLine ($"{indent} Config file absent"); + } else { + log.StatusLine ($"{indent} Config file size", asm.ConfigSize); + log.StatusLine ($"{indent} Config file offset", asm.ConfigOffset); + } + } + + log.InfoLine ($"{indent} Name hashes"); + foreach (ulong hash in asm.Hashes) { + log.MessageLine ($"{indent} 0x{hash:x}"); + } + log.MessageLine (); + } + } +} diff --git a/tools/xapp/utilities.cs b/tools/xapp/utilities.cs new file mode 100644 index 00000000000..df3ecbe4ad7 --- /dev/null +++ b/tools/xapp/utilities.cs @@ -0,0 +1,6 @@ +namespace Microsoft.Android.AppTools.XAPP; + +static class Utilities +{ + public static string YesNo (bool yes) => yes ? "yes" : "no"; +} diff --git a/tools/xapp/xapp.csproj b/tools/xapp/xapp.csproj new file mode 100644 index 00000000000..bb605dd637e --- /dev/null +++ b/tools/xapp/xapp.csproj @@ -0,0 +1,21 @@ + + + + Exe + $(DotNetStableTargetFramework) + ../../bin/$(Configuration)/bin/xapp + enable + + $(ProductVersion) + Microsoft Corporation + 2025 Microsoft Corporation + + + + + + + + + +