mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-09-10 17:16:47 -04:00
add linux option for guest memory usage
This commit is contained in:
@@ -73,8 +73,12 @@ FaultCounters Counters();
|
||||
void LogFaultSummary() noexcept;
|
||||
|
||||
// Returns true when the access violation was a guest-space fault this module
|
||||
// resolved; the caller must then resume execution. `exceptionPointers` is a
|
||||
// Windows EXCEPTION_POINTERS*.
|
||||
bool HandleAccessViolation(void* exceptionPointers) noexcept;
|
||||
// resolved; the caller must then resume execution. `faultAddress` is the raw
|
||||
// host pointer the access violation trapped on (Windows: ExceptionInformation[1];
|
||||
// POSIX: siginfo_t::si_addr) and `isWrite` is whether it was a write access
|
||||
// (Windows: ExceptionInformation[0] != 0; POSIX: derived from the ucontext).
|
||||
// The platform-specific handler that calls this is expected to have already
|
||||
// done that extraction - this function only ever works with the parsed pair.
|
||||
bool HandleAccessViolation(void* faultAddress, bool isWrite) noexcept;
|
||||
|
||||
} // namespace GuestFlat
|
||||
|
||||
@@ -11,10 +11,23 @@
|
||||
|
||||
#include "memory.h"
|
||||
|
||||
// Windows' SehLogger longjmps out of a vectored exception handler, where plain setjmp/longjmp is
|
||||
// the norm. A POSIX signal handler jumping back to here must use the sig-prefixed pair instead:
|
||||
// only sigsetjmp/siglongjmp save and restore the process signal mask, which is what keeps SIGSEGV
|
||||
// from staying blocked (and a second fault during the same ctor loop from escalating instead of
|
||||
// trapping) after the first recovered fault.
|
||||
#if defined(_WIN32)
|
||||
using MkwJmpBuf = jmp_buf;
|
||||
#define MKW_SETJMP(buf) setjmp(buf)
|
||||
#else
|
||||
using MkwJmpBuf = sigjmp_buf;
|
||||
#define MKW_SETJMP(buf) sigsetjmp(buf, 1)
|
||||
#endif
|
||||
|
||||
// Global flag to suppress SEH reporting (caught by system_bridge)
|
||||
extern bool g_suppressSehReporting;
|
||||
// Jump buffer for SEH recovery
|
||||
extern thread_local jmp_buf* g_sehJumpTarget;
|
||||
extern thread_local MkwJmpBuf* g_sehJumpTarget;
|
||||
// SEH details for the most recent trapped exception (used during ctor execution).
|
||||
extern thread_local uint32_t g_sehLastExceptionCode;
|
||||
extern thread_local uintptr_t g_sehLastExceptionAddress;
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "runtime_log.h"
|
||||
#include "system_bridge.h"
|
||||
|
||||
#if defined(_WIN32)
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
@@ -26,28 +27,57 @@
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace GuestFlat {
|
||||
namespace {
|
||||
|
||||
#if defined(_WIN32)
|
||||
// Placeholder / view constants. Declared here so the build does not depend on
|
||||
// the exact Windows SDK version that first shipped them.
|
||||
constexpr DWORD kMemReplacePlaceholder = 0x00004000;
|
||||
constexpr DWORD kMemReservePlaceholder = 0x00040000;
|
||||
constexpr DWORD kMemPreservePlaceholder = 0x00000002;
|
||||
#endif
|
||||
constexpr size_t kAllocationGranularity = 0x10000; // 64 KiB
|
||||
constexpr size_t kHostPageSize = 0x1000;
|
||||
|
||||
// Named, platform-neutral protection modes so every fault-interception call site below (the
|
||||
// MMIO window, the executable-write guard, deferred-EFB-read protection, the on-demand
|
||||
// unmapped-block commit) can stay identical text on both platforms; only ProtectRange() and
|
||||
// CommitPlaceholder() below branch on VirtualProtect vs. mprotect.
|
||||
#if defined(_WIN32)
|
||||
using ProtectionFlags = DWORD;
|
||||
constexpr ProtectionFlags kProtNone = PAGE_NOACCESS;
|
||||
constexpr ProtectionFlags kProtRead = PAGE_READONLY;
|
||||
constexpr ProtectionFlags kProtReadWrite = PAGE_READWRITE;
|
||||
#else
|
||||
using ProtectionFlags = int;
|
||||
constexpr ProtectionFlags kProtNone = PROT_NONE;
|
||||
constexpr ProtectionFlags kProtRead = PROT_READ;
|
||||
constexpr ProtectionFlags kProtReadWrite = PROT_READ | PROT_WRITE;
|
||||
#endif
|
||||
|
||||
#if defined(_WIN32)
|
||||
using VirtualAlloc2Fn = PVOID(WINAPI*)(HANDLE, PVOID, SIZE_T, ULONG, ULONG, void*, ULONG);
|
||||
using MapViewOfFile3Fn = PVOID(WINAPI*)(HANDLE, HANDLE, PVOID, ULONG64, SIZE_T, ULONG, ULONG, void*, ULONG);
|
||||
|
||||
VirtualAlloc2Fn g_virtualAlloc2 = nullptr;
|
||||
MapViewOfFile3Fn g_mapViewOfFile3 = nullptr;
|
||||
#endif
|
||||
|
||||
uint8_t* g_base = nullptr;
|
||||
bool g_initialized = false;
|
||||
std::vector<RegionRequest> g_activeRegions;
|
||||
#if defined(_WIN32)
|
||||
PVOID g_vectoredHandle = nullptr;
|
||||
#endif
|
||||
|
||||
std::mutex& StateMutex() {
|
||||
static std::mutex mutex;
|
||||
@@ -70,7 +100,11 @@ struct SectionKeyHash {
|
||||
};
|
||||
|
||||
struct Section {
|
||||
#if defined(_WIN32)
|
||||
HANDLE handle = nullptr;
|
||||
#else
|
||||
int fd = -1;
|
||||
#endif
|
||||
uint64_t size = 0;
|
||||
uint8_t* hostView = nullptr;
|
||||
};
|
||||
@@ -114,6 +148,18 @@ std::vector<GuardedRange>& DeferredRanges() {
|
||||
return ranges;
|
||||
}
|
||||
|
||||
#if !defined(_WIN32)
|
||||
// Windows disambiguates a racing "unmapped touch" fault via VirtualQuery (did some other thread
|
||||
// already commit this 64 KiB block, and is it actually accessible enough to satisfy this access).
|
||||
// mprotect has no query counterpart, so this tracks the same fact ourselves: one bit per 64 KiB
|
||||
// block, set the first time this module ever commits it, checked-and-set under StateMutex() so
|
||||
// two threads racing on the same never-yet-committed block still report/commit exactly once.
|
||||
std::vector<uint8_t>& UnmappedCommittedBlocks() {
|
||||
static std::vector<uint8_t> blocks(1u << 16, 0); // 2^32 / 64 KiB
|
||||
return blocks;
|
||||
}
|
||||
#endif
|
||||
|
||||
std::atomic<uint32_t> g_countMmio{0};
|
||||
std::atomic<uint32_t> g_countEfb{0};
|
||||
std::atomic<uint32_t> g_countXGuard{0};
|
||||
@@ -133,10 +179,26 @@ uint64_t RoundUp(uint64_t value, uint64_t alignment) {
|
||||
|
||||
std::string LastErrorText(const char* what) {
|
||||
std::ostringstream oss;
|
||||
#if defined(_WIN32)
|
||||
oss << what << " failed (GetLastError=" << GetLastError() << ")";
|
||||
#else
|
||||
oss << what << " failed (" << std::strerror(errno) << ")";
|
||||
#endif
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
// Protects [address, address+size) with `protection`, bridging VirtualProtect (Windows) and
|
||||
// mprotect (POSIX) so every fault-interception call site below can stay platform-neutral.
|
||||
bool ProtectRange(uint8_t* address, uint64_t size, ProtectionFlags protection) {
|
||||
#if defined(_WIN32)
|
||||
DWORD previous = 0;
|
||||
return VirtualProtect(address, static_cast<SIZE_T>(size), protection, &previous) != FALSE;
|
||||
#else
|
||||
return mprotect(address, static_cast<size_t>(size), protection) == 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
void ResolvePlacementApi() {
|
||||
if (g_virtualAlloc2 != nullptr && g_mapViewOfFile3 != nullptr) return;
|
||||
HMODULE kernelBase = GetModuleHandleW(L"kernelbase.dll");
|
||||
@@ -153,9 +215,11 @@ void ResolvePlacementApi() {
|
||||
"are unavailable on this system).");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void EnsureReservation() {
|
||||
if (g_base != nullptr) return;
|
||||
#if defined(_WIN32)
|
||||
ResolvePlacementApi();
|
||||
|
||||
void* requested = reinterpret_cast<void*>(kFixedFlatGuestBase);
|
||||
@@ -178,25 +242,59 @@ void EnsureReservation() {
|
||||
"usual cause.";
|
||||
throw std::runtime_error(oss.str());
|
||||
}
|
||||
|
||||
if (reserved != requested) {
|
||||
throw std::runtime_error(
|
||||
"The flat guest reservation did not land on the fixed base the translated code was "
|
||||
"compiled against.");
|
||||
}
|
||||
#else
|
||||
void* requested = reinterpret_cast<void*>(kFixedFlatGuestBase);
|
||||
|
||||
// No MAP_FIXED here (and deliberately no MAP_FIXED_NOREPLACE, which needs Linux 4.17+ -
|
||||
// this must work on kernels as old as 4.9): `requested` is only a hint. The kernel's
|
||||
// get_unmapped_area honors a page-aligned hint when the whole range is free, so this lands
|
||||
// on the fixed base in the normal case; if anything already occupies part of the range, the
|
||||
// kernel silently picks a different address instead of clobbering it, which the check below
|
||||
// catches - same "something got there first" contract as the Windows path, without needing
|
||||
// a specific kernel version.
|
||||
void* reserved = mmap(requested, kGuestSpaceSize + kAllocationGranularity, kProtNone,
|
||||
MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
|
||||
if (reserved == MAP_FAILED) {
|
||||
std::ostringstream oss;
|
||||
oss << "Unable to reserve the 4 GiB flat guest address space at 0x" << std::hex
|
||||
<< reinterpret_cast<uintptr_t>(requested) << std::dec
|
||||
<< " (" << std::strerror(errno)
|
||||
<< "). The translated code addresses guest memory through this fixed base, so it "
|
||||
"cannot fall back to another one.";
|
||||
throw std::runtime_error(oss.str());
|
||||
}
|
||||
if (reserved != requested) {
|
||||
munmap(reserved, kGuestSpaceSize + kAllocationGranularity);
|
||||
std::ostringstream oss;
|
||||
oss << "Unable to reserve the 4 GiB flat guest address space at 0x" << std::hex
|
||||
<< reinterpret_cast<uintptr_t>(requested) << std::dec
|
||||
<< ". Something else in this process already occupies part of the 16 TiB region - "
|
||||
"an injected library, an overlay or a debugging tool is the usual cause.";
|
||||
throw std::runtime_error(oss.str());
|
||||
}
|
||||
#endif
|
||||
|
||||
g_base = static_cast<uint8_t*>(reserved);
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
// Carves `size` bytes out of the enclosing placeholder so a view or a private
|
||||
// commit can replace it. Splitting an exact-size placeholder is a no-op that
|
||||
// reports ERROR_INVALID_PARAMETER; the caller validates the replacement.
|
||||
void SplitPlaceholder(uint8_t* address, uint64_t size) {
|
||||
VirtualFree(address, static_cast<SIZE_T>(size), MEM_RELEASE | kMemPreservePlaceholder);
|
||||
}
|
||||
#endif
|
||||
|
||||
void MapGuestView(const Section& section, uint64_t sectionOffset, uint32_t guestBase,
|
||||
uint64_t mappedSize) {
|
||||
uint8_t* target = g_base + guestBase;
|
||||
#if defined(_WIN32)
|
||||
SplitPlaceholder(target, mappedSize);
|
||||
void* view = g_mapViewOfFile3(section.handle, GetCurrentProcess(), target, sectionOffset,
|
||||
static_cast<SIZE_T>(mappedSize), kMemReplacePlaceholder,
|
||||
@@ -208,16 +306,37 @@ void MapGuestView(const Section& section, uint64_t sectionOffset, uint32_t guest
|
||||
<< ")";
|
||||
throw std::runtime_error(oss.str());
|
||||
}
|
||||
#else
|
||||
// MAP_FIXED is safe (and needs no particular kernel version) here specifically because we're
|
||||
// deliberately overwriting a sub-range of the PROT_NONE reservation this module already owns
|
||||
// exclusively (see EnsureReservation) - unlike the initial reservation itself, there's no
|
||||
// "something else might already be there" concern to guard against.
|
||||
void* view = mmap(target, static_cast<size_t>(mappedSize), kProtReadWrite,
|
||||
MAP_SHARED | MAP_FIXED, section.fd, static_cast<off_t>(sectionOffset));
|
||||
if (view == MAP_FAILED) {
|
||||
std::ostringstream oss;
|
||||
oss << "Unable to map guest region 0x" << std::hex << guestBase << " (+0x" << mappedSize
|
||||
<< ") into the flat reservation" << std::dec << " (" << std::strerror(errno) << ")";
|
||||
throw std::runtime_error(oss.str());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Replaces a placeholder with private committed memory. Used for the MMIO
|
||||
// window (read-only zeros) and for on-demand commits of stray guest pages.
|
||||
bool CommitPlaceholder(uint8_t* address, uint64_t size, DWORD protection) {
|
||||
bool CommitPlaceholder(uint8_t* address, uint64_t size, ProtectionFlags protection) {
|
||||
#if defined(_WIN32)
|
||||
SplitPlaceholder(address, size);
|
||||
void* result = g_virtualAlloc2(GetCurrentProcess(), address, static_cast<SIZE_T>(size),
|
||||
MEM_RESERVE | MEM_COMMIT | kMemReplacePlaceholder, protection,
|
||||
nullptr, 0);
|
||||
return result != nullptr;
|
||||
#else
|
||||
// No separate reserve-vs-commit step is needed: the anonymous PROT_NONE reservation this
|
||||
// range came from is already demand-zero backed, so mprotect() alone both "commits" and
|
||||
// protects it.
|
||||
return ProtectRange(address, size, protection);
|
||||
#endif
|
||||
}
|
||||
|
||||
// One definition of the two windows lives in memory_access.h; these are the
|
||||
@@ -237,8 +356,7 @@ void ApplyExecutableProtectionLocked() {
|
||||
for (uint64_t page = first; page < last; page += kHostPageSize) {
|
||||
const uint32_t pageIndex = static_cast<uint32_t>(page >> 12);
|
||||
if (protectedPages[pageIndex] != 0) continue;
|
||||
DWORD previous = 0;
|
||||
if (VirtualProtect(g_base + page, kHostPageSize, PAGE_READONLY, &previous) != FALSE) {
|
||||
if (ProtectRange(g_base + page, kHostPageSize, kProtRead)) {
|
||||
protectedPages[pageIndex] = 1;
|
||||
}
|
||||
}
|
||||
@@ -284,8 +402,16 @@ void ZeroMappedStorage() {
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
LONG CALLBACK FlatGuestVectoredHandler(EXCEPTION_POINTERS* info) {
|
||||
if (HandleAccessViolation(info)) {
|
||||
const auto* record = info->ExceptionRecord;
|
||||
if (record == nullptr || record->ExceptionCode != EXCEPTION_ACCESS_VIOLATION ||
|
||||
record->NumberParameters < 2) {
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
void* faultAddress = reinterpret_cast<void*>(record->ExceptionInformation[1]);
|
||||
const bool isWrite = record->ExceptionInformation[0] != 0;
|
||||
if (HandleAccessViolation(faultAddress, isWrite)) {
|
||||
return EXCEPTION_CONTINUE_EXECUTION;
|
||||
}
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
@@ -298,6 +424,7 @@ void InstallVectoredHandler() {
|
||||
throw std::runtime_error(LastErrorText("AddVectoredExceptionHandler"));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void ReportFatalGuestFault(const char* category, uint32_t guestAddress, bool isWrite,
|
||||
const char* detail) {
|
||||
@@ -377,9 +504,7 @@ void Initialize(const std::vector<RegionRequest>& regions) {
|
||||
for (const auto& range : DeferredRanges()) {
|
||||
const uint64_t first = static_cast<uint64_t>(range.start) & ~(kHostPageSize - 1u);
|
||||
const uint64_t last = RoundUp(range.end, kHostPageSize);
|
||||
DWORD previous = 0;
|
||||
VirtualProtect(g_base + first, static_cast<SIZE_T>(last - first), PAGE_READWRITE,
|
||||
&previous);
|
||||
ProtectRange(g_base + first, last - first, kProtReadWrite);
|
||||
}
|
||||
DeferredRanges().clear();
|
||||
ZeroMappedStorage();
|
||||
@@ -408,6 +533,7 @@ void Initialize(const std::vector<RegionRequest>& regions) {
|
||||
const uint64_t rounded = RoundUp(size, kAllocationGranularity);
|
||||
Section section;
|
||||
section.size = rounded;
|
||||
#if defined(_WIN32)
|
||||
section.handle = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE,
|
||||
static_cast<DWORD>(rounded >> 32),
|
||||
static_cast<DWORD>(rounded & 0xFFFFFFFFu), nullptr);
|
||||
@@ -419,6 +545,25 @@ void Initialize(const std::vector<RegionRequest>& regions) {
|
||||
if (section.hostView == nullptr) {
|
||||
throw std::runtime_error(LastErrorText("MapViewOfFile for the host guest-RAM alias"));
|
||||
}
|
||||
#else
|
||||
// The section is an anonymous shared-memory object: the SAME physical pages get mapped
|
||||
// twice below (once here as the always-accessible host view, once per-region as the
|
||||
// guest view whose protection the fault handler controls), the same "one backing store,
|
||||
// two VA aliases" trick CreateFileMapping/MapViewOfFile(3) gives Windows.
|
||||
section.fd = memfd_create("wiicompiled-guest-ram", MFD_CLOEXEC);
|
||||
if (section.fd < 0) {
|
||||
throw std::runtime_error(LastErrorText("memfd_create for guest RAM"));
|
||||
}
|
||||
if (ftruncate(section.fd, static_cast<off_t>(rounded)) != 0) {
|
||||
throw std::runtime_error(LastErrorText("ftruncate for guest RAM"));
|
||||
}
|
||||
section.hostView = static_cast<uint8_t*>(
|
||||
mmap(nullptr, static_cast<size_t>(rounded), kProtReadWrite, MAP_SHARED, section.fd, 0));
|
||||
if (section.hostView == MAP_FAILED) {
|
||||
section.hostView = nullptr;
|
||||
throw std::runtime_error(LastErrorText("mmap for the host guest-RAM alias"));
|
||||
}
|
||||
#endif
|
||||
Sections()[key] = section;
|
||||
}
|
||||
|
||||
@@ -435,12 +580,14 @@ void Initialize(const std::vector<RegionRequest>& regions) {
|
||||
|
||||
// MMIO stays inaccessible in both directions so the vectored handler can report missing HLE; the old
|
||||
// PAGE_READONLY read window that returned zero turned missing devices into silent hangs instead.
|
||||
if (!CommitPlaceholder(g_base + 0xCC000000u, 0x02000000u, PAGE_NOACCESS)) {
|
||||
if (!CommitPlaceholder(g_base + 0xCC000000u, 0x02000000u, kProtNone)) {
|
||||
throw std::runtime_error(LastErrorText("committing the no-access MMIO window"));
|
||||
}
|
||||
|
||||
ApplyExecutableProtectionLocked();
|
||||
#if defined(_WIN32)
|
||||
InstallVectoredHandler();
|
||||
#endif
|
||||
|
||||
// Freshly created section objects are demand-zero, so no explicit clear is
|
||||
// needed on the first mapping (that would fault in all 152 MiB at startup).
|
||||
@@ -470,9 +617,7 @@ void ProtectDeferredRange(uint32_t address, size_t length) {
|
||||
std::lock_guard<std::mutex> lock(StateMutex());
|
||||
const uint64_t first = static_cast<uint64_t>(address) & ~(kHostPageSize - 1u);
|
||||
const uint64_t last = RoundUp(end, kHostPageSize);
|
||||
DWORD previous = 0;
|
||||
if (VirtualProtect(g_base + first, static_cast<SIZE_T>(last - first), PAGE_NOACCESS,
|
||||
&previous) == FALSE) {
|
||||
if (!ProtectRange(g_base + first, last - first, kProtNone)) {
|
||||
// An unmapped destination cannot be trapped; the checked path still
|
||||
// clears the readable bias, so nothing silently reads stale bytes.
|
||||
return;
|
||||
@@ -492,8 +637,7 @@ void UnprotectDeferredRange(uint32_t address, size_t length) {
|
||||
ranges.erase(it);
|
||||
const uint64_t first = static_cast<uint64_t>(address) & ~(kHostPageSize - 1u);
|
||||
const uint64_t last = RoundUp(end, kHostPageSize);
|
||||
DWORD previous = 0;
|
||||
VirtualProtect(g_base + first, static_cast<SIZE_T>(last - first), PAGE_READWRITE, &previous);
|
||||
ProtectRange(g_base + first, last - first, kProtReadWrite);
|
||||
}
|
||||
|
||||
void RegisterExecutableRange(uint32_t start, uint32_t end) {
|
||||
@@ -541,21 +685,14 @@ void LogFaultSummary() noexcept {
|
||||
std::cerr.flush();
|
||||
}
|
||||
|
||||
bool HandleAccessViolation(void* exceptionPointers) noexcept {
|
||||
if (!g_initialized || exceptionPointers == nullptr) return false;
|
||||
auto* info = static_cast<EXCEPTION_POINTERS*>(exceptionPointers);
|
||||
const auto* record = info->ExceptionRecord;
|
||||
if (record == nullptr || record->ExceptionCode != EXCEPTION_ACCESS_VIOLATION ||
|
||||
record->NumberParameters < 2) {
|
||||
return false;
|
||||
}
|
||||
bool HandleAccessViolation(void* faultAddress, bool isWrite) noexcept {
|
||||
if (!g_initialized || faultAddress == nullptr) return false;
|
||||
|
||||
const uintptr_t fault = static_cast<uintptr_t>(record->ExceptionInformation[1]);
|
||||
const uintptr_t fault = reinterpret_cast<uintptr_t>(faultAddress);
|
||||
const uintptr_t base = reinterpret_cast<uintptr_t>(g_base);
|
||||
if (fault < base || fault - base >= kGuestSpaceSize) return false;
|
||||
|
||||
const uint32_t guestAddress = static_cast<uint32_t>(fault - base);
|
||||
const bool isWrite = record->ExceptionInformation[0] != 0;
|
||||
|
||||
// 1) Deferred (EFB) read: materialize the pending copy and drop the trap for the whole 4 KiB page span,
|
||||
// not just the registered range, since protection is page-granular. Leaving a range registered but
|
||||
@@ -581,9 +718,7 @@ bool HandleAccessViolation(void* exceptionPointers) noexcept {
|
||||
ranges.erase(it);
|
||||
spanFirst = static_cast<uint64_t>(rangeStart) & ~(kHostPageSize - 1u);
|
||||
spanLast = RoundUp(rangeEnd, kHostPageSize);
|
||||
DWORD previous = 0;
|
||||
VirtualProtect(g_base + spanFirst, static_cast<SIZE_T>(spanLast - spanFirst),
|
||||
PAGE_READWRITE, &previous);
|
||||
ProtectRange(g_base + spanFirst, spanLast - spanFirst, kProtReadWrite);
|
||||
}
|
||||
}
|
||||
if (covered) {
|
||||
@@ -618,9 +753,8 @@ bool HandleAccessViolation(void* exceptionPointers) noexcept {
|
||||
// Those arrive in bulk, so the page is opened permanently
|
||||
// rather than trapping every relocation.
|
||||
std::lock_guard<std::mutex> lock(StateMutex());
|
||||
DWORD previous = 0;
|
||||
if (VirtualProtect(g_base + (static_cast<uint64_t>(pageIndex) << 12), kHostPageSize,
|
||||
PAGE_READWRITE, &previous) != FALSE) {
|
||||
if (ProtectRange(g_base + (static_cast<uint64_t>(pageIndex) << 12), kHostPageSize,
|
||||
kProtReadWrite)) {
|
||||
ExecutableProtectedPages()[pageIndex] = 0;
|
||||
}
|
||||
}
|
||||
@@ -665,6 +799,7 @@ bool HandleAccessViolation(void* exceptionPointers) noexcept {
|
||||
bool committed = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(StateMutex());
|
||||
#if defined(_WIN32)
|
||||
MEMORY_BASIC_INFORMATION mbi{};
|
||||
if (VirtualQuery(g_base + blockBase, &mbi, sizeof(mbi)) == 0) return false;
|
||||
if (mbi.State == MEM_COMMIT) {
|
||||
@@ -678,9 +813,24 @@ bool HandleAccessViolation(void* exceptionPointers) noexcept {
|
||||
PAGE_EXECUTE)) != 0;
|
||||
return isWrite ? writable : readable;
|
||||
}
|
||||
if (!CommitPlaceholder(g_base + blockBase, kAllocationGranularity, PAGE_READWRITE)) {
|
||||
#else
|
||||
// mprotect has no VirtualQuery counterpart to ask "is this block already committed and
|
||||
// how", so this module tracks the same fact itself (UnmappedCommittedBlocks, checked and
|
||||
// set under this same lock): once a block has been committed READ|WRITE by an earlier
|
||||
// call here (this thread's or a racing one's), every subsequent fault on it is a no-op
|
||||
// resume - there is no POSIX equivalent of "committed but insufficiently permissioned"
|
||||
// for a block only this function ever touches.
|
||||
const uint32_t blockIndex = static_cast<uint32_t>(blockBase / kAllocationGranularity);
|
||||
if (UnmappedCommittedBlocks()[blockIndex] != 0) {
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
if (!CommitPlaceholder(g_base + blockBase, kAllocationGranularity, kProtReadWrite)) {
|
||||
return false;
|
||||
}
|
||||
#if !defined(_WIN32)
|
||||
UnmappedCommittedBlocks()[blockIndex] = 1;
|
||||
#endif
|
||||
committed = true;
|
||||
}
|
||||
if (committed) {
|
||||
|
||||
+106
-1
@@ -37,6 +37,8 @@
|
||||
#include <mmsystem.h>
|
||||
#include <dbghelp.h>
|
||||
#else
|
||||
#include <signal.h>
|
||||
#include <ucontext.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
@@ -988,7 +990,12 @@ LONG CALLBACK SehLogger(EXCEPTION_POINTERS* info) {
|
||||
// flat module registers its own handler first, but registration order is
|
||||
// not guaranteed once another VEH is installed later, so consult it here
|
||||
// too - resolving a fault twice is a no-op.
|
||||
if (GuestFlat::HandleAccessViolation(info)) {
|
||||
if (info->ExceptionRecord != nullptr &&
|
||||
info->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION &&
|
||||
info->ExceptionRecord->NumberParameters >= 2 &&
|
||||
GuestFlat::HandleAccessViolation(
|
||||
reinterpret_cast<void*>(info->ExceptionRecord->ExceptionInformation[1]),
|
||||
info->ExceptionRecord->ExceptionInformation[0] != 0)) {
|
||||
return EXCEPTION_CONTINUE_EXECUTION;
|
||||
}
|
||||
if (g_suppressSehReporting && g_sehJumpTarget) {
|
||||
@@ -1059,6 +1066,102 @@ void InstallSehLogger() {
|
||||
g_vectoredSehHandle = AddVectoredExceptionHandler(1, SehLogger);
|
||||
}
|
||||
}
|
||||
#else
|
||||
// POSIX counterpart to SehLogger above. Unlike Windows' AddVectoredExceptionHandler, which lets
|
||||
// GuestFlat and this module each install their own handler and defensively re-check each other,
|
||||
// sigaction only allows one handler per signal - the second registration replaces the first
|
||||
// instead of chaining. So this is the single SIGSEGV/SIGBUS handler for the whole process, and it
|
||||
// owns checking GuestFlat's fault-interception logic first, exactly mirroring the order SehLogger
|
||||
// already uses on Windows.
|
||||
void ReportUnhandledSignalFault(int sig, void* faultAddress) {
|
||||
RT_LOG(RT_TAG_RUNTIME) << "Signal " << sig << " (fault address 0x" << std::hex
|
||||
<< reinterpret_cast<uintptr_t>(faultAddress) << std::dec << ")";
|
||||
if (!g_lastEntryLabel.empty()) {
|
||||
std::cerr << " while executing " << g_lastEntryLabel;
|
||||
}
|
||||
std::cerr << std::endl;
|
||||
if (const CpuContext* cpu = TryGetCpuContext()) {
|
||||
RT_LOG(RT_TAG_RUNTIME) << "===== DUMPING CPU STATE =====" << std::endl;
|
||||
SystemBridge::DumpCpuState(cpu);
|
||||
}
|
||||
std::cerr.flush();
|
||||
}
|
||||
|
||||
void PosixMemoryFaultHandler(int sig, siginfo_t* info, void* ucontextVoid) {
|
||||
void* faultAddress = info != nullptr ? info->si_addr : nullptr;
|
||||
bool isWrite = false;
|
||||
#if defined(__x86_64__)
|
||||
// Standard glibc technique for a POSIX fastmem-style handler: bit 1 (0x2) of the hardware
|
||||
// error code x86 pushes on a page fault records whether it was a write.
|
||||
if (ucontextVoid != nullptr) {
|
||||
auto* uc = static_cast<ucontext_t*>(ucontextVoid);
|
||||
isWrite = (uc->uc_mcontext.gregs[REG_ERR] & 0x2) != 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Guest-space faults are the flat memory interception mechanism (MMIO, deferred EFB reads,
|
||||
// the executable-write guard, unmapped pages). Resolving one here means resuming the
|
||||
// faulting instruction, which just returning from the handler does.
|
||||
if (faultAddress != nullptr && GuestFlat::HandleAccessViolation(faultAddress, isWrite)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_suppressSehReporting && g_sehJumpTarget) {
|
||||
g_sehLastExceptionCode = static_cast<uint32_t>(sig);
|
||||
g_sehLastExceptionAddress = reinterpret_cast<uintptr_t>(faultAddress);
|
||||
g_sehLastAccessType = isWrite ? 1u : 0u;
|
||||
g_sehLastAccessedAddress = reinterpret_cast<uintptr_t>(faultAddress);
|
||||
siglongjmp(*g_sehJumpTarget, 1);
|
||||
}
|
||||
if (g_suppressSehReporting) {
|
||||
// Reporting suppressed but nobody armed a recovery jump: restore the default disposition
|
||||
// and re-raise so the process still terminates, instead of returning into the same fault.
|
||||
signal(sig, SIG_DFL);
|
||||
raise(sig);
|
||||
return;
|
||||
}
|
||||
|
||||
// Guard against re-entrancy: if we crash while reporting, don't recurse.
|
||||
static std::atomic_flag s_inCrashHandler = ATOMIC_FLAG_INIT;
|
||||
if (s_inCrashHandler.test_and_set()) {
|
||||
std::_Exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
ReportUnhandledSignalFault(sig, faultAddress);
|
||||
std::ostringstream popupDetails;
|
||||
popupDetails << "A native signal (" << sig << ") occurred";
|
||||
if (!g_lastEntryLabel.empty()) {
|
||||
popupDetails << " while executing " << g_lastEntryLabel;
|
||||
}
|
||||
if (faultAddress != nullptr) {
|
||||
popupDetails << ".\n\nThe game attempted a " << (isWrite ? "write" : "read")
|
||||
<< " at host address 0x" << std::hex
|
||||
<< reinterpret_cast<uintptr_t>(faultAddress) << std::dec;
|
||||
}
|
||||
popupDetails << ".\n\nThe process transcript and crash log contain the full CPU and stack "
|
||||
"diagnostics.";
|
||||
ShowRuntimeFatalPopup("a native crash occurred", popupDetails.str());
|
||||
DumpHostStackTrace();
|
||||
WriteFatalLogImpl(sig == SIGBUS ? "sigbus" : "sigsegv");
|
||||
|
||||
std::cerr.flush();
|
||||
std::cout.flush();
|
||||
std::fflush(stdout);
|
||||
std::fflush(stderr);
|
||||
std::_Exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
void InstallPosixMemoryFaultHandler() {
|
||||
struct sigaction action {};
|
||||
action.sa_sigaction = PosixMemoryFaultHandler;
|
||||
action.sa_flags = SA_SIGINFO;
|
||||
sigemptyset(&action.sa_mask);
|
||||
sigaction(SIGSEGV, &action, nullptr);
|
||||
// A touch beyond a memfd-backed mapping's ftruncate()'d size raises SIGBUS rather than
|
||||
// SIGSEGV on Linux; region sizing should make this unreachable, but routing it to the same
|
||||
// handler costs nothing and avoids a silent gap if it ever isn't.
|
||||
sigaction(SIGBUS, &action, nullptr);
|
||||
}
|
||||
#endif
|
||||
|
||||
void AbortSignalHandler(int signum) {
|
||||
@@ -1174,6 +1277,8 @@ int RuntimeMain(int argc, char** argv) {
|
||||
ConfigureWindowsFatalDialogBehavior();
|
||||
InstallSehLogger();
|
||||
WindowsTimerResolutionGuard timerResolutionGuard;
|
||||
#else
|
||||
InstallPosixMemoryFaultHandler();
|
||||
#endif
|
||||
InitializeProcessTranscript(argc, argv);
|
||||
std::signal(SIGABRT, AbortSignalHandler);
|
||||
|
||||
@@ -9,10 +9,12 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
|
||||
#include "abi_bridge.h"
|
||||
@@ -26,7 +28,7 @@
|
||||
|
||||
// Global flag to suppress SEH reporting during static constructor execution
|
||||
bool g_suppressSehReporting = false;
|
||||
thread_local jmp_buf* g_sehJumpTarget = nullptr;
|
||||
thread_local MkwJmpBuf* g_sehJumpTarget = nullptr;
|
||||
thread_local uint32_t g_sehLastExceptionCode = 0;
|
||||
thread_local uintptr_t g_sehLastExceptionAddress = 0;
|
||||
thread_local uintptr_t g_sehLastAccessedAddress = 0;
|
||||
@@ -347,9 +349,9 @@ void SystemBridge::Initialize() {
|
||||
if (funcAddr == 0 || funcAddr == 0xFFFFFFFF) continue;
|
||||
|
||||
if (TranslatedFunctionRegistry::FindByAddressPtr(funcAddr)) {
|
||||
jmp_buf jumpBuf;
|
||||
MkwJmpBuf jumpBuf;
|
||||
g_sehJumpTarget = &jumpBuf;
|
||||
if (setjmp(jumpBuf) == 0) {
|
||||
if (MKW_SETJMP(jumpBuf) == 0) {
|
||||
cpu.gpr[1] = 0x81700000u;
|
||||
InvokeIndirectCpu(funcAddr, &cpu);
|
||||
dolCount++;
|
||||
@@ -387,9 +389,9 @@ void SystemBridge::Initialize() {
|
||||
if (funcAddr == 0 || funcAddr == 0xFFFFFFFF) continue;
|
||||
|
||||
if (TranslatedFunctionRegistry::FindByAddressPtr(funcAddr)) {
|
||||
jmp_buf jumpBuf;
|
||||
MkwJmpBuf jumpBuf;
|
||||
g_sehJumpTarget = &jumpBuf;
|
||||
if (setjmp(jumpBuf) == 0) {
|
||||
if (MKW_SETJMP(jumpBuf) == 0) {
|
||||
cpu.gpr[1] = 0x81700000u;
|
||||
InvokeIndirectCpu(funcAddr, &cpu);
|
||||
count++;
|
||||
|
||||
Reference in New Issue
Block a user