diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs index 458f843..bb42882 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs @@ -587,6 +587,9 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I { "_init_env" or "atexit" or + "__cxa_guard_acquire" or + "__cxa_guard_release" or + "__cxa_guard_abort" or "strlen" or "strnlen" or "strcmp" or diff --git a/src/SharpEmu.Core/Loader/ImportedSymbolRelocation.cs b/src/SharpEmu.Core/Loader/ImportedSymbolRelocation.cs new file mode 100644 index 0000000..a8239e5 --- /dev/null +++ b/src/SharpEmu.Core/Loader/ImportedSymbolRelocation.cs @@ -0,0 +1,10 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +namespace SharpEmu.Core.Loader; + +public readonly record struct ImportedSymbolRelocation( + ulong TargetAddress, + long Addend, + string Nid, + bool IsData); diff --git a/src/SharpEmu.Core/Loader/SelfImage.cs b/src/SharpEmu.Core/Loader/SelfImage.cs index 6c648c2..293012d 100644 --- a/src/SharpEmu.Core/Loader/SelfImage.cs +++ b/src/SharpEmu.Core/Loader/SelfImage.cs @@ -16,6 +16,7 @@ public sealed class SelfImage IReadOnlyList mappedRegions, IReadOnlyDictionary? importStubs = null, IReadOnlyDictionary? runtimeSymbols = null, + IReadOnlyList? importedRelocations = null, ulong imageBase = 0, ulong procParamAddress = 0) { @@ -28,6 +29,7 @@ public sealed class SelfImage MappedRegions = mappedRegions; ImportStubs = importStubs ?? new Dictionary(); RuntimeSymbols = runtimeSymbols ?? new Dictionary(StringComparer.Ordinal); + ImportedRelocations = importedRelocations ?? Array.Empty(); _imageBase = imageBase; ProcParamAddress = procParamAddress; } @@ -44,6 +46,8 @@ public sealed class SelfImage public IReadOnlyDictionary RuntimeSymbols { get; } + public IReadOnlyList ImportedRelocations { get; } + public ulong EntryPoint => ElfHeader.EntryPoint + _imageBase; public ulong ProcParamAddress { get; } diff --git a/src/SharpEmu.Core/Loader/SelfLoader.cs b/src/SharpEmu.Core/Loader/SelfLoader.cs index fe12adb..f26590d 100644 --- a/src/SharpEmu.Core/Loader/SelfLoader.cs +++ b/src/SharpEmu.Core/Loader/SelfLoader.cs @@ -29,6 +29,7 @@ public sealed class SelfLoader : ISelfLoader private const int ElfRelocationSize = 24; private const int ElfSectionHeaderSize = 64; private const uint SectionTypeSymbolTable = 2; + private const uint SectionTypeRela = 4; private const long DtNull = 0; private const long DtPltRelSize = 0x02; @@ -65,6 +66,7 @@ public sealed class SelfLoader : ISelfLoader private const byte SymbolBindLocal = 0; private const byte SymbolBindGlobal = 1; private const byte SymbolBindWeak = 2; + private const byte SymbolTypeObject = 1; private IModuleManager? _moduleManager; private uint _nextTlsModuleId = 1; @@ -190,11 +192,13 @@ public sealed class SelfLoader : ISelfLoader var importStubs = ResolveAndPatchImportStubs( imageData, loadContext, + elfHeader, programHeaders, virtualMemory, imageBase, _moduleManager, - tlsModuleId); + tlsModuleId, + out var importedRelocations); var effectiveImportStubs = importStubs.Count == 0 ? new Dictionary() : new Dictionary(importStubs); @@ -246,6 +250,7 @@ public sealed class SelfLoader : ISelfLoader virtualMemory.SnapshotRegions(), finalizedImportStubs, finalizedRuntimeSymbols, + importedRelocations, imageBase, procParamAddress); } @@ -426,12 +431,15 @@ public sealed class SelfLoader : ISelfLoader private static IReadOnlyDictionary ResolveAndPatchImportStubs( ReadOnlySpan imageData, LoadContext loadContext, + ElfHeader elfHeader, IReadOnlyList programHeaders, IVirtualMemory virtualMemory, ulong imageBase, IModuleManager? moduleManager, - uint tlsModuleId) + uint tlsModuleId, + out IReadOnlyList importedRelocations) { + importedRelocations = Array.Empty(); if (!TryGetProgramHeader(programHeaders, ProgramHeaderType.Dynamic, out var dynamicHeader, out var dynamicHeaderIndex)) { return EmptyImportStubs; @@ -447,10 +455,18 @@ public sealed class SelfLoader : ISelfLoader throw new NotSupportedException("Dynamic metadata segments larger than 2 GB are not currently supported."); } - var dynamicOffset = ResolvePhysicalSegmentOffset(imageData.Length, loadContext, dynamicHeader, dynamicHeaderIndex); - EnsureRange(imageData.Length, dynamicOffset, dynamicHeader.FileSize); + if (!TryLoadDynamicTableBytes( + imageData, + loadContext, + virtualMemory, + imageBase, + dynamicHeader, + dynamicHeaderIndex, + out var dynamicTable)) + { + return EmptyImportStubs; + } - var dynamicTable = imageData.Slice((int)dynamicOffset, (int)dynamicHeader.FileSize); var elfData = imageData; var dynamicInfo = ParseDynamicInfo(dynamicTable); @@ -463,13 +479,6 @@ public sealed class SelfLoader : ISelfLoader Console.WriteLine($"[LOADER] TLS module id: {tlsModuleId}"); Console.WriteLine($"[LOADER] HasImportMetadata: {dynamicInfo.HasImportMetadata}"); - if (!dynamicInfo.HasImportMetadata) - { - Console.WriteLine($"[LOADER] No import metadata found in ELF!"); - return EmptyImportStubs; - } - - Console.WriteLine($"[LOADER] ImageBase runtime: 0x{imageBase:X16}"); var relocations = new List(512); if (dynamicInfo.RelaSize != 0 && @@ -484,13 +493,16 @@ public sealed class SelfLoader : ISelfLoader CollectRelocations(jmpRelBytes, relocations); } - if (relocations.Count == 0) + if (!dynamicInfo.HasImportMetadata) { - Console.WriteLine($"[LOADER] No relocations found!"); - return EmptyImportStubs; + Console.WriteLine($"[LOADER] No import metadata found in ELF!"); } - Console.WriteLine($"[LOADER] Processing {relocations.Count} relocations..."); + if (relocations.Count != 0) + { + Console.WriteLine($"[LOADER] ImageBase runtime: 0x{imageBase:X16}"); + Console.WriteLine($"[LOADER] Processing {relocations.Count} relocations..."); + } uint maxSymbolIndex = 0; foreach (var relocation in relocations) @@ -544,164 +556,37 @@ public sealed class SelfLoader : ISelfLoader var descriptors = new List(256); var orderedImportNids = new List(128); var seenImportNids = new HashSet(StringComparer.Ordinal); - var skippedUnsupported = 0; - var skippedUnmapped = 0; - var skippedSymbolRead = 0; - var skippedNonImportBind = 0; - var skippedNameRead = 0; - var skippedEmptyNid = 0; - foreach (var relocation in relocations) + AppendRelocationDescriptors( + relocations, + symbolTable, + stringTable, + virtualMemory, + imageBase, + tlsModuleId, + descriptors, + orderedImportNids, + seenImportNids); + + if (descriptors.Count == 0) { - if (IsFocusRelocationOffset(relocation.Offset, imageBase)) + var sectionFallbackRelocCount = AppendSectionRelocationDescriptors( + imageData, + loadContext, + elfHeader, + virtualMemory, + imageBase, + tlsModuleId, + descriptors, + orderedImportNids, + seenImportNids); + if (sectionFallbackRelocCount != 0) { - Console.Error.WriteLine( - $"[LOADER][FOCUS][SCAN] off=0x{relocation.Offset:X16} type={relocation.Type} sym={relocation.SymbolIndex} addend=0x{relocation.Addend:X}"); + Console.WriteLine( + $"[LOADER] Section relocation fallback recovered {sectionFallbackRelocCount} relocation entries, {orderedImportNids.Count} unique NIDs, {descriptors.Count} descriptors"); } - - if (!IsSupportedRelocationType(relocation.Type)) - { - skippedUnsupported++; - if (IsFocusRelocationOffset(relocation.Offset, imageBase)) - { - Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] unsupported type={relocation.Type}"); - } - continue; - } - - if (!TryResolveMappedAddress(virtualMemory, relocation.Offset, imageBase, sizeof(ulong), out var targetAddress)) - { - skippedUnmapped++; - if (IsFocusRelocationOffset(relocation.Offset, imageBase)) - { - Console.Error.WriteLine("[LOADER][FOCUS][SKIP] target address not mapped"); - } - continue; - } - - if (relocation.Type == RelocationTypeRelative) - { - descriptors.Add(new RelocationDescriptor( - targetAddress, - relocation.Addend, - null, - imageBase, - RelocationValueKind.Pointer)); - continue; - } - - if (relocation.Type == RelocationTypeTlsModuleId) - { - var dtpmodValue = tlsModuleId == 0 ? 1u : tlsModuleId; - descriptors.Add(new RelocationDescriptor( - targetAddress, - 0, - null, - dtpmodValue, - RelocationValueKind.TlsModuleId)); - continue; - } - - var symbolIndex = relocation.SymbolIndex; - ElfSymbol symbol; - if (symbolIndex == 0) - { - symbol = default; - } - else if (!TryReadSymbol(symbolTable, symbolIndex, out symbol)) - { - skippedSymbolRead++; - if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd) - { - Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] symbol read failed index={symbolIndex}"); - } - continue; - } - - var addend = relocation.Type is RelocationTypeGlobalData or RelocationTypeJumpSlot ? 0 : relocation.Addend; - var symbolBind = GetSymbolBind(symbol.Info); - if (symbolBind == SymbolBindLocal) - { - var symbolAddress = ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase); - if (symbolAddress == 0) - { - Console.Error.WriteLine( - $"[LOADER] Skipping local relocation with invalid symbol value 0x{symbol.Value:X} " + - $"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}"); - continue; - } - - descriptors.Add(new RelocationDescriptor( - targetAddress, - addend, - null, - symbolAddress, - RelocationValueKind.Pointer)); - continue; - } - - if (symbol.Value != 0) - { - var symbolAddress = ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase); - if (symbolAddress == 0) - { - Console.Error.WriteLine( - $"[LOADER] Skipping relocation with invalid symbol value 0x{symbol.Value:X} " + - $"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}"); - continue; - } - - descriptors.Add(new RelocationDescriptor( - targetAddress, - addend, - null, - symbolAddress, - RelocationValueKind.Pointer)); - continue; - } - - if (symbolBind is not (SymbolBindGlobal or SymbolBindWeak)) - { - skippedNonImportBind++; - if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd) - { - Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] bind={symbolBind} not importable"); - } - continue; - } - - if (!TryReadNullTerminatedAscii(stringTable, symbol.NameOffset, out var symbolName)) - { - skippedNameRead++; - if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd) - { - Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] symbol name read failed offset={symbol.NameOffset}"); - } - continue; - } - - var nid = ExtractNid(symbolName); - if (string.IsNullOrWhiteSpace(nid)) - { - skippedEmptyNid++; - continue; - } - - if (seenImportNids.Add(nid)) - { - orderedImportNids.Add(nid); - } - - descriptors.Add(new RelocationDescriptor( - targetAddress, - addend, - nid, - 0, - RelocationValueKind.Pointer)); } Console.WriteLine($"[LOADER] Found {orderedImportNids.Count} unique NIDs, {descriptors.Count} descriptors"); - Console.WriteLine( - $"[LOADER] Reloc skip stats: unsupported={skippedUnsupported}, unmapped={skippedUnmapped}, symbolRead={skippedSymbolRead}, nonImportBind={skippedNonImportBind}, nameRead={skippedNameRead}, emptyNid={skippedEmptyNid}"); if (descriptors.Count == 0) { @@ -709,6 +594,8 @@ public sealed class SelfLoader : ISelfLoader return EmptyImportStubs; } + importedRelocations = BuildImportedRelocations(descriptors); + var stubsByAddress = CreateImportStubMapping(virtualMemory, orderedImportNids); Console.WriteLine($"[LOADER] Created {stubsByAddress.Count} import stubs"); @@ -784,6 +671,237 @@ public sealed class SelfLoader : ISelfLoader return stubsByAddress; } + private static int AppendSectionRelocationDescriptors( + ReadOnlySpan imageData, + LoadContext loadContext, + ElfHeader elfHeader, + IVirtualMemory virtualMemory, + ulong imageBase, + uint tlsModuleId, + ICollection descriptors, + IList orderedImportNids, + ISet seenImportNids) + { + if (elfHeader.SectionHeaderOffset == 0 || + elfHeader.SectionHeaderCount == 0 || + elfHeader.SectionHeaderEntrySize < ElfSectionHeaderSize) + { + return 0; + } + + var appendedRelocations = 0; + for (var sectionIndex = 0; sectionIndex < elfHeader.SectionHeaderCount; sectionIndex++) + { + if (!TryReadSectionHeader(imageData, loadContext, elfHeader, sectionIndex, out var relocationHeader) || + relocationHeader.Type != SectionTypeRela || + relocationHeader.Size == 0 || + relocationHeader.EntrySize < ElfRelocationSize) + { + continue; + } + + if (!TryReadElfRelativeSlice(imageData, loadContext, relocationHeader.Offset, relocationHeader.Size, out var relocationTable)) + { + continue; + } + + var relocations = new List(checked((int)(relocationHeader.Size / relocationHeader.EntrySize))); + CollectRelocations(relocationTable, relocations); + if (relocations.Count == 0) + { + continue; + } + + ReadOnlySpan symbolTable = ReadOnlySpan.Empty; + ReadOnlySpan stringTable = ReadOnlySpan.Empty; + if (relocationHeader.Link < elfHeader.SectionHeaderCount && + TryReadSectionHeader(imageData, loadContext, elfHeader, (int)relocationHeader.Link, out var symbolHeader) && + symbolHeader.Size != 0 && + symbolHeader.EntrySize >= ElfSymbolSize && + TryReadElfRelativeSlice(imageData, loadContext, symbolHeader.Offset, symbolHeader.Size, out symbolTable) && + symbolHeader.Link < elfHeader.SectionHeaderCount && + TryReadSectionHeader(imageData, loadContext, elfHeader, (int)symbolHeader.Link, out var stringHeader) && + stringHeader.Size != 0 && + TryReadElfRelativeSlice(imageData, loadContext, stringHeader.Offset, stringHeader.Size, out stringTable)) + { + } + + AppendRelocationDescriptors( + relocations, + symbolTable, + stringTable, + virtualMemory, + imageBase, + tlsModuleId, + descriptors, + orderedImportNids, + seenImportNids); + appendedRelocations += relocations.Count; + } + + return appendedRelocations; + } + + private static void AppendRelocationDescriptors( + IReadOnlyList relocations, + ReadOnlySpan symbolTable, + ReadOnlySpan stringTable, + IVirtualMemory virtualMemory, + ulong imageBase, + uint tlsModuleId, + ICollection descriptors, + IList orderedImportNids, + ISet seenImportNids) + { + foreach (var relocation in relocations) + { + if (IsFocusRelocationOffset(relocation.Offset, imageBase)) + { + Console.Error.WriteLine( + $"[LOADER][FOCUS][SCAN] off=0x{relocation.Offset:X16} type={relocation.Type} sym={relocation.SymbolIndex} addend=0x{relocation.Addend:X}"); + } + + if (!IsSupportedRelocationType(relocation.Type)) + { + if (IsFocusRelocationOffset(relocation.Offset, imageBase)) + { + Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] unsupported type={relocation.Type}"); + } + continue; + } + + if (!TryResolveMappedAddress(virtualMemory, relocation.Offset, imageBase, sizeof(ulong), out var targetAddress)) + { + if (IsFocusRelocationOffset(relocation.Offset, imageBase)) + { + Console.Error.WriteLine("[LOADER][FOCUS][SKIP] target address not mapped"); + } + continue; + } + + if (relocation.Type == RelocationTypeRelative) + { + descriptors.Add(new RelocationDescriptor( + targetAddress, + relocation.Addend, + null, + imageBase, + RelocationValueKind.Pointer, + IsDataImport: false)); + continue; + } + + if (relocation.Type == RelocationTypeTlsModuleId) + { + var dtpmodValue = tlsModuleId == 0 ? 1u : tlsModuleId; + descriptors.Add(new RelocationDescriptor( + targetAddress, + 0, + null, + dtpmodValue, + RelocationValueKind.TlsModuleId, + IsDataImport: false)); + continue; + } + + var symbolIndex = relocation.SymbolIndex; + ElfSymbol symbol; + if (symbolIndex == 0) + { + symbol = default; + } + else if (!TryReadSymbol(symbolTable, symbolIndex, out symbol)) + { + if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd) + { + Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] symbol read failed index={symbolIndex}"); + } + continue; + } + + var addend = relocation.Type is RelocationTypeGlobalData or RelocationTypeJumpSlot ? 0 : relocation.Addend; + var symbolBind = GetSymbolBind(symbol.Info); + if (symbolBind == SymbolBindLocal) + { + var symbolAddress = ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase); + if (symbolAddress == 0) + { + Console.Error.WriteLine( + $"[LOADER] Skipping local relocation with invalid symbol value 0x{symbol.Value:X} " + + $"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}"); + continue; + } + + descriptors.Add(new RelocationDescriptor( + targetAddress, + addend, + null, + symbolAddress, + RelocationValueKind.Pointer, + IsDataImport: false)); + continue; + } + + if (symbol.Value != 0) + { + var symbolAddress = ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase); + if (symbolAddress == 0) + { + Console.Error.WriteLine( + $"[LOADER] Skipping relocation with invalid symbol value 0x{symbol.Value:X} " + + $"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}"); + continue; + } + + descriptors.Add(new RelocationDescriptor( + targetAddress, + addend, + null, + symbolAddress, + RelocationValueKind.Pointer, + IsDataImport: false)); + continue; + } + + if (symbolBind is not (SymbolBindGlobal or SymbolBindWeak)) + { + if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd) + { + Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] bind={symbolBind} not importable"); + } + continue; + } + + if (!TryReadNullTerminatedAscii(stringTable, symbol.NameOffset, out var symbolName)) + { + if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd) + { + Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] symbol name read failed offset={symbol.NameOffset}"); + } + continue; + } + + var nid = ExtractNid(symbolName); + if (string.IsNullOrWhiteSpace(nid)) + { + continue; + } + + if (seenImportNids.Add(nid)) + { + orderedImportNids.Add(nid); + } + + descriptors.Add(new RelocationDescriptor( + targetAddress, + addend, + nid, + 0, + RelocationValueKind.Pointer, + IsDataImport: GetSymbolType(symbol.Info) == SymbolTypeObject)); + } + } + private static void RegisterRuntimeSymbolsAndHooks( ReadOnlySpan imageData, LoadContext loadContext, @@ -811,6 +929,34 @@ public sealed class SelfLoader : ISelfLoader } } + private static IReadOnlyList BuildImportedRelocations( + IReadOnlyList descriptors) + { + if (descriptors.Count == 0) + { + return Array.Empty(); + } + + var importedRelocations = new List(descriptors.Count); + foreach (var descriptor in descriptors) + { + if (descriptor.ImportNid is null || descriptor.ValueKind != RelocationValueKind.Pointer) + { + continue; + } + + importedRelocations.Add(new ImportedSymbolRelocation( + descriptor.TargetAddress, + descriptor.Addend, + descriptor.ImportNid, + descriptor.IsDataImport)); + } + + return importedRelocations.Count == 0 + ? Array.Empty() + : importedRelocations; + } + private static int RegisterSectionRuntimeSymbols( ReadOnlySpan imageData, LoadContext loadContext, @@ -909,9 +1055,18 @@ public sealed class SelfLoader : ISelfLoader return 0; } - var dynamicOffset = ResolvePhysicalSegmentOffset(imageData.Length, loadContext, dynamicHeader, dynamicHeaderIndex); - EnsureRange(imageData.Length, dynamicOffset, dynamicHeader.FileSize); - var dynamicTable = imageData.Slice((int)dynamicOffset, (int)dynamicHeader.FileSize); + if (!TryLoadDynamicTableBytes( + imageData, + loadContext, + virtualMemory, + imageBase, + dynamicHeader, + dynamicHeaderIndex, + out var dynamicTable)) + { + return 0; + } + var dynamicInfo = ParseDynamicInfo(dynamicTable); if (dynamicInfo.SymTabOffset == 0 || dynamicInfo.StrTabOffset == 0) { @@ -1044,6 +1199,37 @@ public sealed class SelfLoader : ISelfLoader return true; } + private static bool TryLoadDynamicTableBytes( + ReadOnlySpan imageData, + LoadContext loadContext, + IVirtualMemory virtualMemory, + ulong imageBase, + ProgramHeader dynamicHeader, + int dynamicHeaderIndex, + out ReadOnlySpan dynamicTable) + { + if (TryLoadTableBytes( + imageData, + virtualMemory, + imageBase, + dynamicHeader.VirtualAddress, + dynamicHeader.FileSize, + out var loadedDynamicTable)) + { + dynamicTable = loadedDynamicTable; + return true; + } + + var dynamicOffset = ResolvePhysicalSegmentOffset(imageData.Length, loadContext, dynamicHeader, dynamicHeaderIndex); + if (!TrySlice(imageData, dynamicOffset, dynamicHeader.FileSize, out dynamicTable)) + { + dynamicTable = default; + return false; + } + + return true; + } + private static bool TryReadElfRelativeSlice( ReadOnlySpan imageData, LoadContext loadContext, @@ -1512,6 +1698,11 @@ public sealed class SelfLoader : ISelfLoader return (byte)(info >> 4); } + private static byte GetSymbolType(byte info) + { + return (byte)(info & 0x0F); + } + private static bool IsFocusRelocationOffset(ulong relocationOffset, ulong imageBase) { if (relocationOffset >= FocusRelocGuestStart && relocationOffset <= FocusRelocGuestEnd) @@ -1947,7 +2138,8 @@ public sealed class SelfLoader : ISelfLoader long Addend, string? ImportNid, ulong SymbolValue, - RelocationValueKind ValueKind); + RelocationValueKind ValueKind, + bool IsDataImport); private enum SelfSegmentResolveStatus { diff --git a/src/SharpEmu.Core/Runtime/SharpEmuRuntime.cs b/src/SharpEmu.Core/Runtime/SharpEmuRuntime.cs index 8b7d756..6b4ca13 100644 --- a/src/SharpEmu.Core/Runtime/SharpEmuRuntime.cs +++ b/src/SharpEmu.Core/Runtime/SharpEmuRuntime.cs @@ -138,7 +138,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime var generation = image.ElfHeader.AbiVersion == 2 ? Generation.Gen5 : Generation.Gen4; var activeImportStubs = new Dictionary(image.ImportStubs); var activeRuntimeSymbols = new Dictionary(image.RuntimeSymbols, StringComparer.Ordinal); - LoadAdjacentSceModules(ebootPath, activeImportStubs, activeRuntimeSymbols); + var loadedModuleImages = LoadAdjacentSceModules(ebootPath, activeImportStubs, activeRuntimeSymbols); + RebindImportedDataSymbols(image, loadedModuleImages, activeRuntimeSymbols); var processImageName = Path.GetFileName(ebootPath); if (string.IsNullOrWhiteSpace(processImageName)) { @@ -328,15 +329,16 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime return result; } - private void LoadAdjacentSceModules( + private List LoadAdjacentSceModules( string ebootPath, IDictionary importStubs, IDictionary runtimeSymbols) { + var loadedImages = new List(); var ebootDirectory = Path.GetDirectoryName(ebootPath); if (string.IsNullOrWhiteSpace(ebootDirectory)) { - return; + return loadedImages; } var moduleDirectories = new[] @@ -350,7 +352,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime if (moduleDirectories.Length == 0) { - return; + return loadedImages; } var allModulePaths = moduleDirectories @@ -380,7 +382,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime if (modulePaths.Length == 0) { - return; + return loadedImages; } Console.Error.WriteLine($"[RUNTIME] Module search directories: {string.Join(", ", moduleDirectories)}"); @@ -416,6 +418,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime mergedImportCount += MergeImportStubs(importStubs, moduleImage.ImportStubs, modulePath); mergedSymbolCount += MergeRuntimeSymbols(runtimeSymbols, moduleImage.RuntimeSymbols); RegisterLoadedModule(modulePath, moduleImage, isMain: false, isSystemModule: false); + loadedImages.Add(moduleImage); loadedModules++; Console.Error.WriteLine( @@ -430,6 +433,67 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime Console.Error.WriteLine( $"[RUNTIME] Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}"); + return loadedImages; + } + + private void RebindImportedDataSymbols( + SelfImage mainImage, + IReadOnlyList loadedModuleImages, + IReadOnlyDictionary runtimeSymbols) + { + var rebound = 0; + var unresolved = 0; + + rebound += RebindImportedDataSymbols(mainImage, runtimeSymbols, ref unresolved); + for (var i = 0; i < loadedModuleImages.Count; i++) + { + rebound += RebindImportedDataSymbols(loadedModuleImages[i], runtimeSymbols, ref unresolved); + } + + if (rebound != 0 || unresolved != 0) + { + Console.Error.WriteLine( + $"[RUNTIME] Imported data rebind: rebound={rebound}, unresolved={unresolved}"); + } + } + + private int RebindImportedDataSymbols( + SelfImage image, + IReadOnlyDictionary runtimeSymbols, + ref int unresolved) + { + if (image.ImportedRelocations.Count == 0) + { + return 0; + } + + var rebound = 0; + for (var i = 0; i < image.ImportedRelocations.Count; i++) + { + var relocation = image.ImportedRelocations[i]; + if (!relocation.IsData) + { + continue; + } + + if (!runtimeSymbols.TryGetValue(relocation.Nid, out var symbolAddress) || + !IsUsableRuntimeSymbolAddress(symbolAddress)) + { + unresolved++; + continue; + } + + var reboundValue = AddSigned(symbolAddress, relocation.Addend); + if (!TryWriteUInt64(_virtualMemory, relocation.TargetAddress, reboundValue)) + { + unresolved++; + continue; + } + + rebound++; + } + + return rebound; } private static int MergeImportStubs( @@ -498,6 +562,24 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime return address >= 0x10000 && !IsUnresolvedRuntimeSentinel(address); } + private static bool TryWriteUInt64(IVirtualMemory virtualMemory, ulong address, ulong value) + { + Span bytes = stackalloc byte[sizeof(ulong)]; + BinaryPrimitives.WriteUInt64LittleEndian(bytes, value); + return virtualMemory.TryWrite(address, bytes); + } + + private static ulong AddSigned(ulong value, long addend) + { + if (addend >= 0) + { + return unchecked(value + (ulong)addend); + } + + var magnitude = unchecked((ulong)(-(addend + 1))) + 1; + return unchecked(value - magnitude); + } + private static bool IsUnresolvedRuntimeSentinel(ulong value) { return value == 0xFFFEUL || diff --git a/src/SharpEmu.HLE/OrbisGen2Result.cs b/src/SharpEmu.HLE/OrbisGen2Result.cs index aeb695f..00f6f52 100644 --- a/src/SharpEmu.HLE/OrbisGen2Result.cs +++ b/src/SharpEmu.HLE/OrbisGen2Result.cs @@ -15,6 +15,11 @@ public enum OrbisGen2Result : int /// ORBIS_GEN2_OK = 0, + /// + /// Indicates that the operation is not permitted for the calling thread. + /// + ORBIS_GEN2_ERROR_PERMISSION_DENIED = unchecked((int)0x80020001), + /// /// Indicates that the requested export was not found. /// @@ -30,11 +35,26 @@ public enum OrbisGen2Result : int /// ORBIS_GEN2_ERROR_ALREADY_EXISTS = unchecked((int)0x80020004), + /// + /// Indicates that completing the operation would deadlock. + /// + ORBIS_GEN2_ERROR_DEADLOCK = unchecked((int)0x8002000B), + + /// + /// Indicates that the target resource is busy. + /// + ORBIS_GEN2_ERROR_BUSY = unchecked((int)0x80020010), + /// /// Indicates that behavior is recognized but not implemented yet. /// ORBIS_GEN2_ERROR_NOT_IMPLEMENTED = unchecked((int)0x8002FFFF), + /// + /// Indicates that the operation timed out. + /// + ORBIS_GEN2_ERROR_TIMED_OUT = unchecked((int)0x8002003C), + /// /// Indicates that memory access failed. /// diff --git a/src/SharpEmu.Libs/CxxAbiExports.cs b/src/SharpEmu.Libs/CxxAbiExports.cs index e22664a..94940f7 100644 --- a/src/SharpEmu.Libs/CxxAbiExports.cs +++ b/src/SharpEmu.Libs/CxxAbiExports.cs @@ -13,6 +13,7 @@ public static class CxaGuardExports private sealed class GuardState { public int OwnerThreadId { get; set; } + public int RecursionDepth { get; set; } } private static readonly ConcurrentDictionary _inProgress = new(); @@ -53,6 +54,7 @@ public static class CxaGuardExports var newState = new GuardState { OwnerThreadId = currentThreadId, + RecursionDepth = 1, }; if (_inProgress.TryAdd(guardPtr, newState)) { @@ -101,6 +103,20 @@ public static class CxaGuardExports return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; } + if (state is not null) + { + lock (state) + { + if (state.RecursionDepth > 1) + { + state.RecursionDepth--; + ctx[CpuRegister.Rax] = 0; + LogGuardResult("guard_release", guardPtr, result: 0, initialized: false, inProgress: true, ownerThreadId: state.OwnerThreadId); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + } + } + if (!TryWriteGuardInitialized(ctx, guardPtr, initialized: true)) { ctx[CpuRegister.Rax] = 0; diff --git a/src/SharpEmu.Libs/Kernel/KernelExports.cs b/src/SharpEmu.Libs/Kernel/KernelExports.cs index 73a391c..d3f91f3 100644 --- a/src/SharpEmu.Libs/Kernel/KernelExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelExports.cs @@ -8,7 +8,6 @@ namespace SharpEmu.Libs.Kernel; public static class KernelExports { - private static long _nextThreadId = 1; private static int _nextFileDescriptor = 2; private static readonly object _cxaGate = new(); private static readonly List _cxaDestructors = new(); @@ -175,12 +174,24 @@ public static class KernelExports public static int PthreadCreate(CpuContext ctx) { var threadIdAddress = ctx[CpuRegister.Rdi]; - var nextThreadId = unchecked((ulong)Interlocked.Increment(ref _nextThreadId)); - if (threadIdAddress != 0 && !ctx.TryWriteUInt64(threadIdAddress, nextThreadId)) + var attrAddress = ctx[CpuRegister.Rsi]; + var entryAddress = ctx[CpuRegister.Rdx]; + var argument = ctx[CpuRegister.Rcx]; + var nameAddress = ctx[CpuRegister.R8]; + var name = nameAddress == 0 ? string.Empty : ReadCString(ctx, nameAddress, 256); + var threadHandle = KernelPthreadState.CreateThreadHandle(name); + if (threadIdAddress != 0 && !ctx.TryWriteUInt64(threadIdAddress, threadHandle)) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } + if (ShouldTracePthread()) + { + Console.Error.WriteLine( + $"[LOADER][TRACE] pthread_create: out=0x{threadIdAddress:X16} attr=0x{attrAddress:X16} entry=0x{entryAddress:X16} arg=0x{argument:X16} name_ptr=0x{nameAddress:X16} name='{name}' -> thread=0x{threadHandle:X16}"); + } + + ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -201,12 +212,20 @@ public static class KernelExports LibraryName = "libKernel")] public static int PthreadJoin(CpuContext ctx) { + var threadId = ctx[CpuRegister.Rdi]; var returnValueAddress = ctx[CpuRegister.Rsi]; if (returnValueAddress != 0 && !ctx.TryWriteUInt64(returnValueAddress, 0)) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } + if (ShouldTracePthread()) + { + Console.Error.WriteLine( + $"[LOADER][TRACE] pthread_join: thread=0x{threadId:X16} retval_out=0x{returnValueAddress:X16}"); + } + + ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -370,4 +389,9 @@ public static class KernelExports try { return System.Text.Encoding.UTF8.GetString(buf.Slice(0, len)); } catch { return System.Text.Encoding.ASCII.GetString(buf.Slice(0, len)); } } + + private static bool ShouldTracePthread() + { + return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal); + } } diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index 2ec42f8..bdf6e34 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -35,13 +35,18 @@ public static class KernelMemoryCompatExports private const uint PageExecuteReadWrite = 0x40; private const uint PageExecuteWriteCopy = 0x80; private const uint PageGuard = 0x100; + private const int Enomem = 12; + private const int Einval = 22; + private const nuint DefaultLibcHeapAlignment = 16; private static readonly object _fdGate = new(); private static readonly Dictionary _openFiles = new(); private static readonly Dictionary _openDirectories = new(); + private static readonly object _libcAllocGate = new(); private static readonly object _memoryGate = new(); private static readonly object _tlsGate = new(); private static readonly Dictionary _directAllocations = new(); + private static readonly Dictionary _libcAllocations = new(); private static readonly Dictionary _mappedRegions = new(); private static readonly Dictionary _tlsModuleBlocks = new(); private static long _nextFileDescriptor = 2; @@ -78,7 +83,9 @@ public static class KernelMemoryCompatExports public required string[] Entries { get; init; } public int NextIndex { get; set; } } + private readonly record struct DirectAllocation(ulong Start, ulong Length, int MemoryType); + private readonly record struct LibcHeapAllocation(nint BaseAddress, nuint Size, nuint Alignment); private readonly record struct MappedRegion(ulong Address, ulong Length, int Protection, bool IsFlexible, ulong DirectStart); [SysAbiExport( @@ -310,7 +317,7 @@ public static class KernelMemoryCompatExports var payload = new byte[bytes.Length + 1]; bytes.CopyTo(payload.AsSpan()); - if (!ctx.Memory.TryWrite(destination, payload)) + if (!TryWriteCompat(ctx, destination, payload)) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } @@ -339,7 +346,7 @@ public static class KernelMemoryCompatExports var copied = 0; while (copied < count) { - if (!ctx.Memory.TryRead(source + (ulong)copied, one)) + if (!TryReadCompat(ctx, source + (ulong)copied, one)) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } @@ -352,7 +359,7 @@ public static class KernelMemoryCompatExports } } - if (!ctx.Memory.TryWrite(destination, payload)) + if (!TryWriteCompat(ctx, destination, payload)) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } @@ -377,7 +384,7 @@ public static class KernelMemoryCompatExports } var payload = GC.AllocateUninitializedArray(count); - if (count > 0 && (!ctx.Memory.TryRead(source, payload) || !ctx.Memory.TryWrite(destination, payload))) + if (count > 0 && (!TryReadCompat(ctx, source, payload) || !TryWriteCompat(ctx, destination, payload))) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } @@ -396,6 +403,161 @@ public static class KernelMemoryCompatExports return Memcpy(ctx); } + [SysAbiExport( + Nid = "gQX+4GDQjpM", + ExportName = "malloc", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libc")] + public static int Malloc(CpuContext ctx) + { + ctx[CpuRegister.Rax] = + TryAllocateLibcHeap(ctx[CpuRegister.Rdi], DefaultLibcHeapAlignment, zeroFill: false, out var address) + ? address + : 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "tIhsqj0qsFE", + ExportName = "free", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libc")] + public static int Free(CpuContext ctx) + { + FreeLibcHeap(ctx[CpuRegister.Rdi]); + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "2X5agFjKxMc", + ExportName = "calloc", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libc")] + public static int Calloc(CpuContext ctx) + { + ctx[CpuRegister.Rax] = + TryMultiplyAllocationSize(ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], out var totalSize) && + TryAllocateLibcHeapCore(totalSize, DefaultLibcHeapAlignment, zeroFill: true, out var address) + ? address + : 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "Y7aJ1uydPMo", + ExportName = "realloc", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libc")] + public static int Realloc(CpuContext ctx) + { + var existingAddress = ctx[CpuRegister.Rdi]; + var requestedSize = ctx[CpuRegister.Rsi]; + + if (existingAddress == 0) + { + ctx[CpuRegister.Rax] = + TryAllocateLibcHeap(requestedSize, DefaultLibcHeapAlignment, zeroFill: false, out var freshAddress) + ? freshAddress + : 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + if (requestedSize == 0) + { + FreeLibcHeap(existingAddress); + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + ctx[CpuRegister.Rax] = + TryReallocateLibcHeap(existingAddress, requestedSize, out var resizedAddress) + ? resizedAddress + : 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "Ujf3KzMvRmI", + ExportName = "memalign", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libc")] + public static int Memalign(CpuContext ctx) + { + ctx[CpuRegister.Rax] = + TryAllocateAlignedLibcHeap( + alignmentValue: ctx[CpuRegister.Rdi], + requestedSize: ctx[CpuRegister.Rsi], + requireSizeMultiple: false, + out var address) + ? address + : 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "2Btkg8k24Zg", + ExportName = "aligned_alloc", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libc")] + public static int AlignedAlloc(CpuContext ctx) + { + ctx[CpuRegister.Rax] = + TryAllocateAlignedLibcHeap( + alignmentValue: ctx[CpuRegister.Rdi], + requestedSize: ctx[CpuRegister.Rsi], + requireSizeMultiple: true, + out var address) + ? address + : 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "cVSk9y8URbc", + ExportName = "posix_memalign", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libc")] + public static int PosixMemalign(CpuContext ctx) + { + var outPointerAddress = ctx[CpuRegister.Rdi]; + if (outPointerAddress == 0) + { + ctx[CpuRegister.Rax] = Einval; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + if (!TryValidateAlignedAllocation( + ctx[CpuRegister.Rsi], + ctx[CpuRegister.Rdx], + requireSizeMultiple: false, + requirePointerSizedAlignment: true, + out var alignment, + out var requestedSize)) + { + _ = TryWriteUInt64Compat(ctx, outPointerAddress, 0); + ctx[CpuRegister.Rax] = Einval; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + if (!TryAllocateLibcHeapCore(requestedSize, alignment, zeroFill: false, out var address)) + { + _ = TryWriteUInt64Compat(ctx, outPointerAddress, 0); + ctx[CpuRegister.Rax] = Enomem; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + if (!TryWriteUInt64Compat(ctx, outPointerAddress, address)) + { + FreeLibcHeap(address); + ctx[CpuRegister.Rax] = Einval; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + [SysAbiExport( Nid = "DfivPArhucg", ExportName = "memcmp", @@ -415,8 +577,8 @@ public static class KernelMemoryCompatExports Span rightByte = stackalloc byte[1]; for (var i = 0; i < count; i++) { - if (!ctx.Memory.TryRead(left + (ulong)i, leftByte) || - !ctx.Memory.TryRead(right + (ulong)i, rightByte)) + if (!TryReadCompat(ctx, left + (ulong)i, leftByte) || + !TryReadCompat(ctx, right + (ulong)i, rightByte)) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } @@ -2318,6 +2480,236 @@ public static class KernelMemoryCompatExports } } + private static bool TryAllocateLibcHeap(ulong requestedSize, nuint alignment, bool zeroFill, out ulong address) + { + address = 0; + return TryConvertAllocationSize(requestedSize, out var size) && + TryAllocateLibcHeapCore(size, alignment, zeroFill, out address); + } + + private static unsafe bool TryAllocateLibcHeapCore(nuint requestedSize, nuint alignment, bool zeroFill, out ulong address) + { + address = 0; + alignment = NormalizeLibcAlignment(alignment); + var actualSize = requestedSize == 0 ? 1u : requestedSize; + + nuint totalSize; + try + { + checked + { + totalSize = actualSize + alignment - 1 + (nuint)IntPtr.Size; + } + } + catch (OverflowException) + { + return false; + } + + nint baseAddress; + try + { + baseAddress = Marshal.AllocHGlobal(checked((nint)totalSize)); + } + catch (OutOfMemoryException) + { + return false; + } + catch (OverflowException) + { + return false; + } + + if (baseAddress == 0) + { + return false; + } + + var alignedAddress = AlignUp(unchecked((ulong)baseAddress) + (ulong)IntPtr.Size, (ulong)alignment); + lock (_libcAllocGate) + { + _libcAllocations[alignedAddress] = new LibcHeapAllocation(baseAddress, actualSize, alignment); + } + + try + { + if (zeroFill) + { + NativeMemory.Clear((void*)alignedAddress, actualSize); + } + } + catch + { + FreeLibcHeap(alignedAddress); + return false; + } + + address = alignedAddress; + return true; + } + + private static unsafe bool TryReallocateLibcHeap(ulong existingAddress, ulong requestedSize, out ulong resizedAddress) + { + resizedAddress = 0; + if (existingAddress == 0) + { + return TryAllocateLibcHeap(requestedSize, DefaultLibcHeapAlignment, zeroFill: false, out resizedAddress); + } + + if (requestedSize == 0) + { + FreeLibcHeap(existingAddress); + return true; + } + + LibcHeapAllocation allocation; + lock (_libcAllocGate) + { + if (!_libcAllocations.TryGetValue(existingAddress, out allocation)) + { + return false; + } + } + + if (!TryAllocateLibcHeap(requestedSize, allocation.Alignment, zeroFill: false, out resizedAddress)) + { + return false; + } + + var bytesToCopy = Math.Min(allocation.Size, (nuint)requestedSize); + Buffer.MemoryCopy( + source: (void*)existingAddress, + destination: (void*)resizedAddress, + destinationSizeInBytes: checked((long)Math.Max(bytesToCopy, 1u)), + sourceBytesToCopy: checked((long)bytesToCopy)); + FreeLibcHeap(existingAddress); + return true; + } + + private static bool TryAllocateAlignedLibcHeap(ulong alignmentValue, ulong requestedSize, bool requireSizeMultiple, out ulong address) + { + address = 0; + return TryValidateAlignedAllocation( + alignmentValue, + requestedSize, + requireSizeMultiple, + requirePointerSizedAlignment: false, + out var alignment, + out var size) && + TryAllocateLibcHeapCore(size, alignment, zeroFill: false, out address); + } + + private static bool TryValidateAlignedAllocation( + ulong alignmentValue, + ulong requestedSize, + bool requireSizeMultiple, + bool requirePointerSizedAlignment, + out nuint alignment, + out nuint size) + { + alignment = 0; + size = 0; + if (!TryConvertAllocationSize(requestedSize, out size) || + alignmentValue == 0 || + alignmentValue > (ulong)nint.MaxValue) + { + return false; + } + + alignment = (nuint)alignmentValue; + if (!IsPowerOfTwo(alignment)) + { + return false; + } + + if (requirePointerSizedAlignment && alignment % (nuint)IntPtr.Size != 0) + { + return false; + } + + if (alignment < (nuint)IntPtr.Size) + { + alignment = (nuint)IntPtr.Size; + } + + if (requireSizeMultiple && size % alignment != 0) + { + return false; + } + + return true; + } + + private static void FreeLibcHeap(ulong address) + { + if (address == 0) + { + return; + } + + LibcHeapAllocation allocation; + lock (_libcAllocGate) + { + if (!_libcAllocations.Remove(address, out allocation)) + { + return; + } + } + + Marshal.FreeHGlobal(allocation.BaseAddress); + } + + private static bool TryMultiplyAllocationSize(ulong left, ulong right, out nuint size) + { + size = 0; + if (!TryConvertAllocationSize(left, out var leftSize) || + !TryConvertAllocationSize(right, out var rightSize)) + { + return false; + } + + try + { + checked + { + size = leftSize * rightSize; + } + } + catch (OverflowException) + { + return false; + } + + return true; + } + + private static bool TryConvertAllocationSize(ulong requestedSize, out nuint size) + { + size = 0; + if (requestedSize > (ulong)nint.MaxValue) + { + return false; + } + + size = (nuint)requestedSize; + return true; + } + + private static nuint NormalizeLibcAlignment(nuint alignment) + { + if (alignment < DefaultLibcHeapAlignment) + { + return DefaultLibcHeapAlignment; + } + + return alignment; + } + + private static bool IsPowerOfTwo(nuint value) + { + return value != 0 && (value & (value - 1)) == 0; + } + private static bool TryWriteHostMemory(ulong address, ReadOnlySpan source) { if (source.IsEmpty || !IsHostRangeAccessible(address, (ulong)source.Length, writeAccess: true)) diff --git a/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs index 41b67f4..8132dd5 100644 --- a/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs @@ -8,8 +8,10 @@ namespace SharpEmu.Libs.Kernel; public static class KernelPthreadCompatExports { - private const int MutexTypeNormal = 0; - private const int MutexTypeRecursive = 1; + private const int MutexTypeDefault = 1; + private const int MutexTypeErrorCheck = 1; + private const int MutexTypeRecursive = 2; + private const int MutexTypeNormal = 4; private const ulong SyntheticMutexHandleBase = 0x00006000_0000_0000; private const ulong SyntheticMutexAttrHandleBase = 0x00006001_0000_0000; @@ -18,24 +20,22 @@ public static class KernelPthreadCompatExports private static readonly Dictionary _mutexAttrStates = new(); private static readonly Dictionary _condStates = new(); private static readonly HashSet _condAttrStates = new(); - private static long _nextSyntheticThreadId = 1; private static long _nextSyntheticMutexHandleId = 1; private static long _nextSyntheticMutexAttrHandleId = 1; - [ThreadStatic] - private static ulong _currentThreadId; private sealed class PthreadMutexState { public SemaphoreSlim Semaphore { get; } = new(1, 1); public ulong OwnerThreadId { get; set; } public int RecursionCount { get; set; } - public int Type { get; set; } = MutexTypeNormal; + public int Type { get; set; } = MutexTypeDefault; public int Protocol { get; set; } } private sealed class PthreadCondState { - public int PendingSignals { get; set; } + public object SyncRoot { get; } = new(); + public ulong SignalEpoch { get; set; } public int Waiters { get; set; } } @@ -48,9 +48,9 @@ public static class KernelPthreadCompatExports LibraryName = "libKernel")] public static int PthreadSelf(CpuContext ctx) { - var currentThreadId = GetCurrentThreadId(); - ctx[CpuRegister.Rax] = currentThreadId; - TracePthreadSelf(ctx, currentThreadId); + var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle(); + ctx[CpuRegister.Rax] = currentThreadHandle; + TracePthreadSelf(ctx, currentThreadHandle); return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -86,7 +86,7 @@ public static class KernelPthreadCompatExports LibraryName = "libKernel")] public static int PthreadGetthreadid(CpuContext ctx) { - var currentThreadId = GetCurrentThreadId(); + var currentThreadId = KernelPthreadState.GetCurrentThreadUniqueId(); var outAddress = ctx[CpuRegister.Rdi]; if (outAddress != 0 && !ctx.TryWriteUInt64(outAddress, currentThreadId)) { @@ -216,6 +216,13 @@ public static class KernelPthreadCompatExports LibraryName = "libKernel")] public static int PosixPthreadMutexattrSettype(CpuContext ctx) => PthreadMutexattrSettypeCore(ctx, ctx[CpuRegister.Rdi], unchecked((int)ctx[CpuRegister.Rsi])); + [SysAbiExport( + Nid = "5txKfcMUAok", + ExportName = "pthread_mutexattr_setprotocol", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PosixPthreadMutexattrSetprotocol(CpuContext ctx) => PthreadMutexattrSetprotocolCore(ctx, ctx[CpuRegister.Rdi], unchecked((int)ctx[CpuRegister.Rsi])); + [SysAbiExport( Nid = "2Tb92quprl0", ExportName = "scePthreadCondInit", @@ -242,7 +249,7 @@ public static class KernelPthreadCompatExports ExportName = "scePthreadCondTimedwait", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] - public static int PthreadCondTimedwait(CpuContext ctx) => PthreadCondWaitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], timed: true); + public static int PthreadCondTimedwait(CpuContext ctx) => PthreadCondWaitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], timed: true, timeoutUsec: unchecked((uint)ctx[CpuRegister.Rdx])); [SysAbiExport( Nid = "kDh-NfxgMtE", @@ -390,7 +397,7 @@ public static class KernelPthreadCompatExports } } - var currentThreadId = GetCurrentThreadId(); + var currentThreadId = KernelPthreadState.GetCurrentThreadHandle(); lock (state) { if (state.OwnerThreadId == currentThreadId) @@ -402,8 +409,11 @@ public static class KernelPthreadCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } - TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_ALREADY_EXISTS); - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_ALREADY_EXISTS; + var ownedResult = tryOnly + ? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY + : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK; + TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, ownedResult); + return ownedResult; } } @@ -418,8 +428,8 @@ public static class KernelPthreadCompatExports } if (!acquired) { - TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_ALREADY_EXISTS); - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_ALREADY_EXISTS; + TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY); + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY; } lock (state) @@ -448,11 +458,11 @@ public static class KernelPthreadCompatExports if (state is null) { - TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, null, GetCurrentThreadId(), (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND); + TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, null, KernelPthreadState.GetCurrentThreadHandle(), (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND); return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; } - var currentThreadId = GetCurrentThreadId(); + var currentThreadId = KernelPthreadState.GetCurrentThreadHandle(); var shouldRelease = false; lock (state) { @@ -464,8 +474,8 @@ public static class KernelPthreadCompatExports if (requireOwner && state.OwnerThreadId != currentThreadId) { - TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED); + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED; } state.RecursionCount--; @@ -503,8 +513,8 @@ public static class KernelPthreadCompatExports var syntheticHandle = AllocateSyntheticHandle(SyntheticMutexAttrHandleBase, ref _nextSyntheticMutexAttrHandleId); lock (_stateGate) { - _mutexAttrStates[attrAddress] = new PthreadMutexAttrState(MutexTypeNormal, 0); - _mutexAttrStates[syntheticHandle] = new PthreadMutexAttrState(MutexTypeNormal, 0); + _mutexAttrStates[attrAddress] = new PthreadMutexAttrState(MutexTypeDefault, 0); + _mutexAttrStates[syntheticHandle] = new PthreadMutexAttrState(MutexTypeDefault, 0); } _ = ctx.TryWriteUInt64(attrAddress, syntheticHandle); @@ -543,10 +553,10 @@ public static class KernelPthreadCompatExports { if (!_mutexAttrStates.TryGetValue(resolvedAddress, out var state)) { - state = new PthreadMutexAttrState(MutexTypeNormal, 0); + state = new PthreadMutexAttrState(MutexTypeDefault, 0); } - _mutexAttrStates[resolvedAddress] = state with { Type = type }; + _mutexAttrStates[resolvedAddress] = state with { Type = NormalizeMutexType(type) }; if (resolvedAddress != attrAddress) { _mutexAttrStates[attrAddress] = _mutexAttrStates[resolvedAddress]; @@ -568,7 +578,7 @@ public static class KernelPthreadCompatExports { if (!_mutexAttrStates.TryGetValue(resolvedAddress, out var state)) { - state = new PthreadMutexAttrState(MutexTypeNormal, 0); + state = new PthreadMutexAttrState(MutexTypeDefault, 0); } _mutexAttrStates[resolvedAddress] = state with { Protocol = protocol }; @@ -651,7 +661,7 @@ public static class KernelPthreadCompatExports { return _mutexAttrStates.TryGetValue(resolvedAddress, out var state) ? state - : default; + : new PthreadMutexAttrState(MutexTypeDefault, 0); } } @@ -691,55 +701,66 @@ public static class KernelPthreadCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } - private static int PthreadCondWaitCore(CpuContext ctx, ulong condAddress, ulong mutexAddress, bool timed) + private static int PthreadCondWaitCore(CpuContext ctx, ulong condAddress, ulong mutexAddress, bool timed, uint timeoutUsec = 0) { if (condAddress == 0 || mutexAddress == 0) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; } + PthreadCondState state; lock (_stateGate) { - if (!_condStates.TryGetValue(condAddress, out var state)) + if (!_condStates.TryGetValue(condAddress, out state!)) { state = new PthreadCondState(); _condStates[condAddress] = state; } + } + var waitResult = (int)OrbisGen2Result.ORBIS_GEN2_OK; + lock (state.SyncRoot) + { state.Waiters++; - if (state.PendingSignals > 0) + var observedEpoch = state.SignalEpoch; + TracePthreadCond("wait-enter", condAddress, mutexAddress, state, timed, waitResult); + + var unlockResult = PthreadMutexUnlockCore(ctx, mutexAddress, requireOwner: true); + if (unlockResult != (int)OrbisGen2Result.ORBIS_GEN2_OK) { - state.PendingSignals--; state.Waiters--; - return (int)OrbisGen2Result.ORBIS_GEN2_OK; + TracePthreadCond("wait-unlock-fail", condAddress, mutexAddress, state, timed, unlockResult); + return unlockResult; } - } - var unlockResult = PthreadMutexUnlockCore(ctx, mutexAddress, requireOwner: true); - if (unlockResult != (int)OrbisGen2Result.ORBIS_GEN2_OK) - { - return unlockResult; - } + while (state.SignalEpoch == observedEpoch) + { + if (!timed) + { + Monitor.Wait(state.SyncRoot); + continue; + } - if (timed) - { - Thread.Sleep(1); - } - else - { - Thread.Yield(); + if (!Monitor.Wait(state.SyncRoot, GetCondWaitTimeout(timeoutUsec))) + { + waitResult = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT; + break; + } + } + + state.Waiters = Math.Max(0, state.Waiters - 1); + TracePthreadCond(waitResult == (int)OrbisGen2Result.ORBIS_GEN2_OK ? "wait-wake" : "wait-timeout", condAddress, mutexAddress, state, timed, waitResult); } var lockResult = PthreadMutexLockCore(ctx, mutexAddress, tryOnly: false); - lock (_stateGate) + if (lockResult != (int)OrbisGen2Result.ORBIS_GEN2_OK) { - if (_condStates.TryGetValue(condAddress, out var state)) - { - state.Waiters = Math.Max(0, state.Waiters - 1); - } + TracePthreadCond("wait-relock-fail", condAddress, mutexAddress, state, timed, lockResult); + return lockResult; } - return lockResult; + TracePthreadCond(waitResult == (int)OrbisGen2Result.ORBIS_GEN2_OK ? "wait-exit" : "wait-exit-timeout", condAddress, mutexAddress, state, timed, waitResult); + return waitResult; } private static int PthreadCondSignalCore(ulong condAddress, bool broadcast) @@ -749,47 +770,70 @@ public static class KernelPthreadCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; } + PthreadCondState state; lock (_stateGate) { - if (!_condStates.TryGetValue(condAddress, out var state)) + if (!_condStates.TryGetValue(condAddress, out state!)) { state = new PthreadCondState(); _condStates[condAddress] = state; } + } - if (broadcast) + lock (state.SyncRoot) + { + if (state.Waiters > 0) { - state.PendingSignals += Math.Max(1, state.Waiters); - } - else - { - state.PendingSignals++; + state.SignalEpoch++; + if (broadcast) + { + Monitor.PulseAll(state.SyncRoot); + } + else + { + Monitor.Pulse(state.SyncRoot); + } } + + TracePthreadCond(broadcast ? "broadcast" : "signal", condAddress, mutexAddress: 0, state, timed: false, (int)OrbisGen2Result.ORBIS_GEN2_OK); } return (int)OrbisGen2Result.ORBIS_GEN2_OK; } - private static ulong GetCurrentThreadId() + private static TimeSpan GetCondWaitTimeout(uint timeoutUsec) { - if (_currentThreadId != 0) + if (timeoutUsec == 0) { - return _currentThreadId; + return TimeSpan.Zero; } - _currentThreadId = unchecked((ulong)Interlocked.Increment(ref _nextSyntheticThreadId)); - return _currentThreadId; + return TimeSpan.FromTicks((long)timeoutUsec * 10L); } - private static void TracePthreadSelf(CpuContext ctx, ulong currentThreadId) + private static int NormalizeMutexType(int type) + { + return type switch + { + 0 => MutexTypeDefault, + 1 => MutexTypeErrorCheck, + 2 => MutexTypeRecursive, + 3 => MutexTypeNormal, + 4 => MutexTypeNormal, + _ => MutexTypeDefault, + }; + } + + private static void TracePthreadSelf(CpuContext ctx, ulong currentThreadHandle) { if (!ShouldTracePthread()) { return; } + var currentThreadId = KernelPthreadState.GetCurrentThreadUniqueId(); Console.Error.WriteLine( - $"[LOADER][TRACE] pthread_self: stale_rdi=0x{ctx[CpuRegister.Rdi]:X16} thread=0x{currentThreadId:X16}"); + $"[LOADER][TRACE] pthread_self: stale_rdi=0x{ctx[CpuRegister.Rdi]:X16} thread=0x{currentThreadHandle:X16} tid=0x{currentThreadId:X16}"); } private static void TracePthreadMutex(CpuContext ctx, string operation, ulong mutexAddress, ulong resolvedAddress, PthreadMutexState? state, ulong currentThreadId, int result) @@ -808,6 +852,18 @@ public static class KernelPthreadCompatExports $"recursion={(state?.RecursionCount ?? 0)} type={(state?.Type ?? 0)} result=0x{unchecked((uint)result):X8}"); } + private static void TracePthreadCond(string operation, ulong condAddress, ulong mutexAddress, PthreadCondState? state, bool timed, int result) + { + if (!ShouldTracePthread()) + { + return; + } + + Console.Error.WriteLine( + $"[LOADER][TRACE] pthread_cond_{operation}: cond=0x{condAddress:X16} mutex=0x{mutexAddress:X16} " + + $"waiters={(state?.Waiters ?? 0)} epoch=0x{(state?.SignalEpoch ?? 0):X} timed={timed} result=0x{unchecked((uint)result):X8}"); + } + private static bool ShouldTracePthread() { return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal); diff --git a/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs index 53d862d..908a6bf 100644 --- a/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs @@ -841,9 +841,13 @@ public static class KernelPthreadExtendedCompatExports return state; } + var name = KernelPthreadState.TryGetThreadIdentity(thread, out var identity) + ? identity.Name + : $"Thread-{thread:X}"; + state = new ThreadState { - Name = $"Thread-{thread:X}", + Name = name, Priority = DefaultThreadPriority, AffinityMask = DefaultThreadAffinityMask, DetachState = DefaultDetachState, diff --git a/src/SharpEmu.Libs/Kernel/KernelPthreadState.cs b/src/SharpEmu.Libs/Kernel/KernelPthreadState.cs new file mode 100644 index 0000000..3b65ee1 --- /dev/null +++ b/src/SharpEmu.Libs/Kernel/KernelPthreadState.cs @@ -0,0 +1,78 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Runtime.InteropServices; +using System.Threading; + +namespace SharpEmu.Libs.Kernel; + +internal static class KernelPthreadState +{ + private const int ThreadObjectSize = 0x1000; + + private static readonly object Gate = new(); + private static readonly Dictionary Threads = new(); + private static readonly byte[] ZeroThreadObject = new byte[ThreadObjectSize]; + private static long _nextUniqueThreadId = 1; + + [ThreadStatic] + private static ulong _currentThreadHandle; + + [ThreadStatic] + private static ulong _currentThreadUniqueId; + + internal readonly record struct ThreadIdentity(ulong UniqueId, string Name); + + internal static ulong GetCurrentThreadHandle() + { + EnsureCurrentThreadRegistered(); + return _currentThreadHandle; + } + + internal static ulong GetCurrentThreadUniqueId() + { + EnsureCurrentThreadRegistered(); + return _currentThreadUniqueId; + } + + internal static ulong CreateThreadHandle(string name) + { + var uniqueId = unchecked((ulong)Interlocked.Increment(ref _nextUniqueThreadId)); + return AllocateThreadHandle(uniqueId, name); + } + + internal static bool TryGetThreadIdentity(ulong threadHandle, out ThreadIdentity identity) + { + lock (Gate) + { + return Threads.TryGetValue(threadHandle, out identity); + } + } + + private static void EnsureCurrentThreadRegistered() + { + if (_currentThreadHandle != 0) + { + return; + } + + var uniqueId = unchecked((ulong)Interlocked.Increment(ref _nextUniqueThreadId)); + var name = $"Thread-{uniqueId:X}"; + _currentThreadHandle = AllocateThreadHandle(uniqueId, name); + _currentThreadUniqueId = uniqueId; + } + + private static ulong AllocateThreadHandle(ulong uniqueId, string name) + { + var pointer = Marshal.AllocHGlobal(ThreadObjectSize); + Marshal.Copy(ZeroThreadObject, 0, pointer, ThreadObjectSize); + + var handle = unchecked((ulong)pointer.ToInt64()); + lock (Gate) + { + Threads[handle] = new ThreadIdentity(uniqueId, string.IsNullOrWhiteSpace(name) ? $"Thread-{uniqueId:X}" : name); + } + + return handle; + } +}