[Memory] Move new watches to heap-aware Memory from MMIOHandler

This commit is contained in:
Triang3l
2019-07-30 08:00:20 +03:00
parent 83da671bb4
commit 4aceeb73c4
12 changed files with 535 additions and 387 deletions

View File

@@ -24,17 +24,19 @@ namespace cpu {
MMIOHandler* MMIOHandler::global_handler_ = nullptr;
std::unique_ptr<MMIOHandler> MMIOHandler::Install(uint8_t* virtual_membase,
uint8_t* physical_membase,
uint8_t* membase_end) {
std::unique_ptr<MMIOHandler> MMIOHandler::Install(
uint8_t* virtual_membase, uint8_t* physical_membase, uint8_t* membase_end,
AccessViolationCallback access_violation_callback,
void* access_violation_callback_context) {
// There can be only one handler at a time.
assert_null(global_handler_);
if (global_handler_) {
return nullptr;
}
auto handler = std::unique_ptr<MMIOHandler>(
new MMIOHandler(virtual_membase, physical_membase, membase_end));
auto handler = std::unique_ptr<MMIOHandler>(new MMIOHandler(
virtual_membase, physical_membase, membase_end, access_violation_callback,
access_violation_callback_context));
// Install the exception handler directed at the MMIOHandler.
ExceptionHandler::Install(ExceptionCallbackThunk, handler.get());
@@ -44,18 +46,14 @@ std::unique_ptr<MMIOHandler> MMIOHandler::Install(uint8_t* virtual_membase,
}
MMIOHandler::MMIOHandler(uint8_t* virtual_membase, uint8_t* physical_membase,
uint8_t* membase_end)
uint8_t* membase_end,
AccessViolationCallback access_violation_callback,
void* access_violation_callback_context)
: virtual_membase_(virtual_membase),
physical_membase_(physical_membase),
memory_end_(membase_end) {
system_page_size_log2_ = xe::log2_ceil(uint32_t(xe::memory::page_size()));
uint32_t physical_page_count = (512 * 1024 * 1024) >> system_page_size_log2_;
physical_write_watched_pages_.resize(physical_page_count >> 4);
assert_true(physical_write_watched_pages_.size() != 0);
std::memset(physical_write_watched_pages_.data(), 0,
physical_write_watched_pages_.size() * sizeof(uint64_t));
}
memory_end_(membase_end),
access_violation_callback_(access_violation_callback),
access_violation_callback_context_(access_violation_callback_context) {}
MMIOHandler::~MMIOHandler() {
ExceptionHandler::Uninstall(ExceptionCallbackThunk, this);
@@ -231,86 +229,8 @@ void MMIOHandler::CancelAccessWatch(uintptr_t watch_handle) {
delete entry;
}
void* MMIOHandler::RegisterPhysicalWriteWatch(
PhysicalWriteWatchCallback callback, void* callback_context) {
PhysicalWriteWatchEntry* entry = new PhysicalWriteWatchEntry;
entry->callback = callback;
entry->callback_context = callback_context;
auto lock = global_critical_region_.Acquire();
physical_write_watches_.push_back(entry);
return entry;
}
void MMIOHandler::UnregisterPhysicalWriteWatch(void* watch_handle) {
auto entry = reinterpret_cast<PhysicalWriteWatchEntry*>(watch_handle);
{
auto lock = global_critical_region_.Acquire();
auto it = std::find(physical_write_watches_.begin(),
physical_write_watches_.end(), entry);
assert_false(it == physical_write_watches_.end());
if (it != physical_write_watches_.end()) {
physical_write_watches_.erase(it);
}
}
delete entry;
}
void MMIOHandler::ProtectAndWatchPhysicalMemory(
uint32_t physical_address_and_heap, uint32_t length) {
// Bits to set in 16-bit blocks to mark that the pages are protected.
uint64_t block_heap_mask;
if (physical_address_and_heap >= 0xE0000000) {
block_heap_mask = 0x4444444444444444ull;
} else if (physical_address_and_heap >= 0xC0000000) {
block_heap_mask = 0x2222222222222222ull;
} else if (physical_address_and_heap >= 0xA0000000) {
block_heap_mask = 0x1111111111111111ull;
} else {
assert_always();
return;
}
uint32_t heap_relative_address = physical_address_and_heap & 0x1FFFFFFF;
length = std::min(length, 0x20000000u - heap_relative_address);
if (length == 0) {
return;
}
uint32_t page_first = heap_relative_address >> system_page_size_log2_;
uint32_t page_last =
(heap_relative_address + length - 1) >> system_page_size_log2_;
uint32_t block_first = page_first >> 4;
uint32_t block_last = page_last >> 4;
auto lock = global_critical_region_.Acquire();
// Set the bits indicating that the pages are watched and access violations
// there are intentional.
for (uint32_t i = block_first; i <= block_last; ++i) {
uint64_t block_set_bits = block_heap_mask;
if (i == block_first) {
block_set_bits &= ~((1ull << ((page_first & 15) * 4)) - 1);
}
if (i == block_last && (page_last & 15) != 15) {
block_set_bits &= (1ull << (((page_last & 15) + 1) * 4)) - 1;
}
physical_write_watched_pages_[i] |= block_set_bits;
}
// Protect only in one range (due to difficulties synchronizing protection
// levels between those ranges).
memory::Protect(virtual_membase_ + (physical_address_and_heap & ~0x1FFFFFFF) +
(page_first << system_page_size_log2_),
(page_last - page_first + 1) << system_page_size_log2_,
memory::PageAccess::kReadOnly, nullptr);
}
void MMIOHandler::InvalidateRange(uint32_t physical_address_and_heap,
uint32_t length, bool unprotect) {
uint32_t length) {
uint32_t heap_relative_address = physical_address_and_heap & 0x1FFFFFFF;
length = std::min(length, 0x20000000u - heap_relative_address);
if (length == 0) {
@@ -319,61 +239,6 @@ void MMIOHandler::InvalidateRange(uint32_t physical_address_and_heap,
auto lock = global_critical_region_.Acquire();
// Trigger the new (per-page) watches and unwatch the pages.
if (physical_address_and_heap >= 0xA0000000) {
uint32_t heap_address = physical_address_and_heap & ~0x1FFFFFFF;
uint64_t heap_bit;
if (heap_address >= 0xE0000000) {
heap_bit = 1 << 2;
} else if (heap_address >= 0xC0000000) {
heap_bit = 1 << 1;
} else {
heap_bit = 1 << 0;
}
uint32_t page_first = heap_relative_address >> system_page_size_log2_;
uint32_t page_last =
(heap_relative_address + length - 1) >> system_page_size_log2_;
uint32_t range_start = UINT32_MAX;
for (uint32_t i = page_first; i <= page_last; ++i) {
uint64_t page_heap_bit = heap_bit << ((i & 15) * 4);
if (physical_write_watched_pages_[i >> 4] & page_heap_bit) {
if (range_start == UINT32_MAX) {
range_start = i;
}
physical_write_watched_pages_[i >> 4] &= ~page_heap_bit;
} else {
if (range_start != UINT32_MAX) {
for (auto it = physical_write_watches_.begin();
it != physical_write_watches_.end(); ++it) {
auto entry = *it;
entry->callback(entry->callback_context, range_start, i - 1);
}
if (unprotect) {
memory::Protect(virtual_membase_ + heap_address +
(range_start << system_page_size_log2_),
(i - range_start) << system_page_size_log2_,
xe::memory::PageAccess::kReadWrite, nullptr);
}
range_start = UINT32_MAX;
}
}
}
if (range_start != UINT32_MAX) {
for (auto it = physical_write_watches_.begin();
it != physical_write_watches_.end(); ++it) {
auto entry = *it;
entry->callback(entry->callback_context, range_start, page_last);
if (unprotect) {
memory::Protect(virtual_membase_ + heap_address +
(range_start << system_page_size_log2_),
(page_last - range_start + 1)
<< system_page_size_log2_,
xe::memory::PageAccess::kReadWrite, nullptr);
}
}
}
}
// Trigger the legacy (per-range) watches.
for (auto it = access_watches_.begin(); it != access_watches_.end();) {
auto entry = *it;
@@ -421,38 +286,8 @@ bool MMIOHandler::CheckAccessWatch(uint32_t physical_address,
uint32_t heap_address) {
bool hit = false;
// Trigger new (per-page) access watches.
if (heap_address >= 0xA0000000) {
uint32_t page_index = physical_address >> system_page_size_log2_;
// Check the watch only for the virtual memory mapping it was triggered in,
// because as guest protection levels may be different for different
// mappings of the physical memory, it's difficult to synchronize protection
// between the mappings.
uint64_t heap_bit;
if (heap_address >= 0xE0000000) {
heap_bit = 1 << 2;
} else if (heap_address >= 0xC0000000) {
heap_bit = 1 << 1;
} else {
heap_bit = 1 << 0;
}
heap_bit <<= (page_index & 15) * 4;
if (physical_write_watched_pages_[page_index >> 4] & heap_bit) {
hit = true;
memory::Protect(virtual_membase_ + heap_address +
(page_index << system_page_size_log2_),
size_t(1) << system_page_size_log2_,
xe::memory::PageAccess::kReadWrite, nullptr);
physical_write_watched_pages_[page_index >> 4] &= ~heap_bit;
for (auto it = physical_write_watches_.begin();
it != physical_write_watches_.end(); ++it) {
auto entry = *it;
entry->callback(entry->callback_context, page_index, page_index);
}
}
}
// Trigger legacy (per-range) access watches.
// TODO(Triang3l): Remove when legacy watches are deleted.
auto lock = global_critical_region_.Acquire();
for (auto it = access_watches_.begin(); it != access_watches_.end();) {
auto entry = *it;
@@ -694,9 +529,24 @@ bool MMIOHandler::ExceptionCallback(Exception* ex) {
// Access is not found within any range, so fail and let the caller handle
// it (likely by aborting).
// TODO(Triang3l): Don't call for the host physical memory view when legacy
// watches are removed.
return CheckAccessWatch(guest_address, guest_heap_address);
// TODO(Triang3l): Remove legacy CheckAccessWatch, only call the callback.
bool hit = CheckAccessWatch(guest_address, guest_heap_address);
if (access_violation_callback_) {
switch (ex->access_violation_operation()) {
case Exception::AccessViolationOperation::kRead:
hit |= access_violation_callback_(access_violation_callback_context_,
size_t(ex->fault_address()), false);
break;
case Exception::AccessViolationOperation::kWrite:
hit |= access_violation_callback_(access_violation_callback_context_,
size_t(ex->fault_address()), true);
break;
default:
// Data Execution Prevention or something else uninteresting.
break;
}
}
return hit;
}
auto rip = ex->pc();

View File

@@ -30,9 +30,6 @@ typedef void (*MMIOWriteCallback)(void* ppc_context, void* callback_context,
uint32_t addr, uint32_t value);
typedef void (*AccessWatchCallback)(void* context_ptr, void* data_ptr,
uint32_t address);
typedef void (*PhysicalWriteWatchCallback)(void* context_ptr,
uint32_t page_first,
uint32_t page_last);
struct MMIORange {
uint32_t address;
@@ -48,15 +45,22 @@ class MMIOHandler {
public:
virtual ~MMIOHandler();
typedef bool (*AccessViolationCallback)(void* context, size_t host_address,
bool is_write);
enum WatchType {
kWatchInvalid = 0,
kWatchWrite = 1,
kWatchReadWrite = 2,
};
static std::unique_ptr<MMIOHandler> Install(uint8_t* virtual_membase,
uint8_t* physical_membase,
uint8_t* membase_end);
// access_violation_callback is called in global_critical_region, so if
// multiple threads trigger an access violation in the same page, the callback
// will be called only once.
static std::unique_ptr<MMIOHandler> Install(
uint8_t* virtual_membase, uint8_t* physical_membase, uint8_t* membase_end,
AccessViolationCallback access_violation_callback,
void* access_violation_callback_context);
static MMIOHandler* global_handler() { return global_handler_; }
bool RegisterRange(uint32_t virtual_address, uint32_t mask, uint32_t size,
@@ -79,59 +83,10 @@ class MMIOHandler {
void* callback_context, void* callback_data);
void CancelAccessWatch(uintptr_t watch_handle);
// Physical memory write watching, allowing subsystems to invalidate cached
// data that depends on memory contents.
//
// Placing a watch simply marks the pages (of the system page size) as
// watched, individual watched ranges (or which specific subscribers are
// watching specific pages) are not stored. Because of this, callbacks may be
// triggered multiple times for a single range, and for any watched page every
// registered callbacks is triggered. This is a very simple one-shot method
// for use primarily for cache invalidation - there may be spurious firing,
// for example, if the game only changes the protection level without writing
// anything.
//
// A range of pages can be watched at any time, but pages are only unwatched
// when watches are triggered (since multiple subscribers can depend on the
// same memory, and one subscriber shouldn't interfere with another).
//
// Callbacks can be triggered for one page (if the guest just stores words) or
// for multiple pages (for file reading, protection level changes).
//
// Only guest physical memory mappings are watched - the host-only mapping is
// not protected so it can be used to bypass the write protection (for file
// reads, for example - in this case, watches are triggered manually).
//
// Ranges passed to ProtectAndWatchPhysicalMemory must not contain read-only
// or inaccessible pages - this must be checked externally! Otherwise the MMIO
// handler will make them read-only, but when a read is attempted, it will
// make them read-write!
//
// IMPORTANT NOTE: When a watch is triggered, the watched page is unprotected
// ***ONLY IN THE HEAP WHERE THE ADDRESS IS LOCATED***! Since different
// virtual memory mappings of physical memory can have different protection
// levels for the same pages, and watches must not be placed on read-only or
// totally inaccessible pages, there are significant difficulties with
// synchronizing all the three ranges.
//
// TODO(Triang3l): Allow the callbacks to unwatch regions larger than one page
// (for instance, 64 KB) so there are less access violations. All callbacks
// must agree to unwatch larger ranges because in some cases (like regions
// near the locations that render targets have been resolved to) it is
// necessary to invalidate only a single page and none more.
void* RegisterPhysicalWriteWatch(PhysicalWriteWatchCallback callback,
void* callback_context);
void UnregisterPhysicalWriteWatch(void* watch_handle);
// Force-protects the range in ***ONE SPECIFIC HEAP***, either 0xA0000000,
// 0xC0000000 or 0xE0000000, depending on the higher bits of the address.
void ProtectAndWatchPhysicalMemory(uint32_t physical_address_and_heap,
uint32_t length);
// Fires and clears any write watches that overlap this range in one heap.
// Unprotecting can be inhibited if this is called right before applying
// different protection to the same range.
void InvalidateRange(uint32_t physical_address_and_heap, uint32_t length,
bool unprotect = true);
void InvalidateRange(uint32_t physical_address_and_heap, uint32_t length);
// Returns true if /all/ of this range is watched.
// TODO(Triang3l): Remove when legacy watches are removed.
@@ -147,13 +102,10 @@ class MMIOHandler {
void* callback_data;
};
struct PhysicalWriteWatchEntry {
PhysicalWriteWatchCallback callback;
void* callback_context;
};
MMIOHandler(uint8_t* virtual_membase, uint8_t* physical_membase,
uint8_t* membase_end);
uint8_t* membase_end,
AccessViolationCallback access_violation_callback,
void* access_violation_callback_context);
static bool ExceptionCallbackThunk(Exception* ex, void* data);
bool ExceptionCallback(Exception* ex);
@@ -162,24 +114,18 @@ class MMIOHandler {
void ClearAccessWatch(AccessWatchEntry* entry);
bool CheckAccessWatch(uint32_t guest_address, uint32_t guest_heap_address);
uint32_t system_page_size_log2_;
uint8_t* virtual_membase_;
uint8_t* physical_membase_;
uint8_t* memory_end_;
std::vector<MMIORange> mapped_ranges_;
AccessViolationCallback access_violation_callback_;
void* access_violation_callback_context_;
xe::global_critical_region global_critical_region_;
// TODO(benvanik): data structure magic.
std::list<AccessWatchEntry*> access_watches_;
std::vector<PhysicalWriteWatchEntry*> physical_write_watches_;
// For each page, there are 4 bits (16 pages in each word):
// 0 - whether the page is protected in A0000000.
// 1 - whether the page is protected in C0000000.
// 2 - whether the page is protected in E0000000.
// 3 - unused, always zero.
std::vector<uint64_t> physical_write_watched_pages_;
static MMIOHandler* global_handler_;
};