add linux option for guest memory usage

This commit is contained in:
theofficialgman
2026-08-25 21:11:32 -04:00
parent a0b54b874b
commit 88ab029c74
5 changed files with 315 additions and 41 deletions
+7 -3
View File
@@ -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
+14 -1
View File
@@ -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;
+181 -31
View File
@@ -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
View File
@@ -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);
+7 -5
View File
@@ -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++;