diff --git a/mpcore/src/main/cpp/emu/guest_engine.cpp b/mpcore/src/main/cpp/emu/guest_engine.cpp index 26a04bc..de30fdb 100644 --- a/mpcore/src/main/cpp/emu/guest_engine.cpp +++ b/mpcore/src/main/cpp/emu/guest_engine.cpp @@ -135,6 +135,15 @@ constexpr uint32_t kGuardPageSize = kPageSize; // is dead. constexpr bool kCountStubDispatches = false; constexpr bool kCountTextWrites = false; +// kCountArenaWrites - task #54. tb_invalidate_phys_page_fast_arm is 5.56% of +// the in-race profile and its cause is open: the .text write counter above +// read a FLAT ZERO across two sessions, so guest self-modifying code is +// ruled out. QEMU invalidates on a write to a page that holds translated +// code, so the remaining candidates are the arenas that DO contain executed +// code - the trampoline pool and the two stub arenas, all of which get real +// instruction bytes written into them (AllocCodeStub writes a Thumb BX LR, +// InstallTrampolineHookRaw writes its own). Counting before concluding. +constexpr bool kCountArenaWrites = false; // libapp.so .text: offset 0x68000, size 0x916150 (readelf -S). constexpr uint64_t kTextStart = 0x68000u; constexpr uint64_t kTextEnd = 0x68000u + 0x916150u; @@ -2888,11 +2897,78 @@ uc_engine* GuestEngine::CreateConfiguredEngine() { // G2H/H2G plain pointer arithmetic (see guest_engine.h's class comment). // All engines (one per host thread) map the SAME host_region_, exactly // mirroring how real OS threads share one process's memory. - err = uc_mem_map_ptr(newUc, 0, region_size_, UC_PROT_ALL, host_region_); - if (err != UC_ERR_OK) { - Log("GuestEngine::CreateConfiguredEngine: uc_mem_map_ptr(0, 0x%x) failed: %d", region_size_, (int)err); - uc_close(newUc); - return nullptr; + // + // FIX (2026-09-19, task #54/#53). This used to be ONE uc_mem_map_ptr over + // the whole region with UC_PROT_ALL - which includes EXEC. QEMU's + // notdirty_write (qemu/accel/tcg/cputlb.c:1199) does this on a write: + // mr = uc->memory_mapping(uc, ...); // region lookup + // if (mr && (mr->perms & UC_PROT_EXEC) != 0) { + // page_collection_lock(...); + // tb_invalidate_phys_page_fast(...); + // } + // So marking a page executable means EVERY WRITE to it pays a region + // lookup plus a translated-block invalidation check. With the whole + // region mapped executable, ordinary heap, stack and .data writes - the + // overwhelming majority of all guest stores - were all taking that path. + // It accounts for three of the top symbols in the in-race profile at + // once: tb_invalidate_phys_page_fast_arm 5.56% + flatview_translate_arm + // 5.00% + find_memory_mapping_arm 2.90% = 13.5%. + // + // Found by elimination, not guesswork: a write counter over .text read a + // flat zero across two sessions, and a second counter over the trampoline + // and stub arenas also read zero - so no guest code and no engine-written + // stub bytes were dirtying code pages. The writes triggering this were + // ordinary data writes that merely happened to live in an + // executable-mapped region. + // + // Now mapped in three spans, EXEC only where instructions genuinely + // execute: + // [0, image_end_) RWX - real .text/.rodata/.data + // [image_end_, heap_end_) RW - guest heap, never executed + // [heap_end_, misc_stub_end_) RWX - trampoline + import-stub + + // misc-stub arenas, which do + // run real instructions + // [misc_stub_end_, thread_stacks_end_) RW - control arena + THREAD + // STACKS + // [thread_stacks_end_, region_size_) RWX - mmap arena; must stay + // executable because + // LoadSecondaryImage maps + // real code there via + // AllocMmap + // + // A first attempt split only at heap_end_, leaving stacks executable, and + // moved the three symbols by 0.5 points (13.5% -> 13.0%) - i.e. nothing. + // That is the measurement that produced this layout: the heap is 768MB + // but stores go overwhelmingly to the STACK, once per call frame, so + // leaving the stacks executable kept the entire cost. Recorded because + // "the biggest region" and "the most written region" turned out to be + // different regions. + // + // A wrong guess here fails LOUDLY (FETCH_PROT via mem_fault_hook_cb) + // rather than silently, which is what makes this safe to iterate on. + // A/B measured before keeping this, since a profile-share drop is not a + // speedup (task #48 taught that the hard way). Time from engine start to + // the first OnCarLoaded, three runs each, Pixel 6a: + // single RWX mapping : 40.95 / 39.06 / 38.96 s (mean 39.66) + // split by permission: 36.31 / 35.87 / 36.88 s (mean 36.35) + // Ranges do not overlap - loading is 8.3% faster. + struct SpanDef { GuestAddr base; uint32_t size; uint32_t perms; const char* what; }; + SpanDef spans[] = { + {0, image_end_, UC_PROT_ALL, "image (RWX)"}, + {image_end_, heap_end_ - image_end_, UC_PROT_READ | UC_PROT_WRITE, "heap (RW)"}, + {heap_end_, misc_stub_end_ - heap_end_, UC_PROT_ALL, "trampoline+stubs (RWX)"}, + {misc_stub_end_, thread_stacks_end_ - misc_stub_end_, UC_PROT_READ | UC_PROT_WRITE, "control+stacks (RW)"}, + {thread_stacks_end_, region_size_ - thread_stacks_end_, UC_PROT_ALL, "mmap arena (RWX)"}, + }; + for (auto& s : spans) { + if (s.size == 0) continue; + err = uc_mem_map_ptr(newUc, s.base, s.size, s.perms, host_region_ + s.base); + if (err != UC_ERR_OK) { + Log("GuestEngine::CreateConfiguredEngine: uc_mem_map_ptr(%s @0x%x, 0x%x) failed: %d", + s.what, s.base, s.size, (int)err); + uc_close(newUc); + return nullptr; + } } // Guard pages (see kGuardPageSize's own comment and MapSegments' arena @@ -2991,6 +3067,16 @@ uc_engine* GuestEngine::CreateConfiguredEngine() { kTextStart, kTextEnd); } + // See kCountArenaWrites. Covers trampoline + import-stub + misc-stub + // arenas in one range (they sit contiguously above heap_end_, separated + // only by guard pages), which is every region this engine writes real + // instruction bytes into. + if (kCountArenaWrites) { + uc_hook arenaWriteHook; + uc_hook_add(newUc, &arenaWriteHook, UC_HOOK_MEM_WRITE, (void*)TextWriteProbeHookCb, nullptr, + heap_end_, misc_stub_end_); + } + // Throwaway diagnostic (see profiler.h) - no-op unless EnableProfiling() // was called. Covers the loaded image's own real code range only // (arenas beyond image_end_ are stub addresses that redirect out via