• Updated 2023-07-12: Hello, Guest! Welcome back, and be sure to check out this follow-up post about our outage a week or so ago.

Making Memory Pages ignore writes on PowerMacs

DBJ314

Member
On PowerPC Macs, this code will designate a page as "Quiet Read-Only". Writes to that page will be silently ignored, instead of generating an exception.

It can cause all sorts of interesting crashes and misbehavior. Most code assumes that when you make a memory write, memory actually gets written. It is thus an excellent tool for making code behave oddly.

Using this when Virtual Memory is on is probably a bad idea. Virtual Memory will have no idea that you did this, and might try to move or page out the page you have modified.

It has to be compiled as 68k, because the NanoKernel Virtual Memory API (invoked with 0xFE06 or 0xFE0A) only works from the 68k Emulator.

Code:
#include <ConditionalMacros.h>
#include <MacTypes.h>

#if !TARGET_OS_MAC || !TARGET_CPU_68K || TARGET_RT_MAC_CFM
#error needs to be compiled for non-cfm 68k
#endif

#define EMMUOpTrap 0xFE06 //do VM op, cause a line F instruction if it fails
#define EMMUOpCCR  0xFE0A //do VM op, set CCR.Z if it fails

#pragma parameter __D0 KernelVMDispatch(__D0, __A0, __A1, __D1)
EXTERN_API(UInt32)
KernelVMDispatch(UInt32 routine_selector, UInt32 param1, UInt32 param2, UInt32 param3) ONEWORDINLINE(EMMUOpTrap);

#pragma parameter __D0 VMGetPTEntryGivenPage(__A0)
EXTERN_API(UInt32)
VMGetPTEntryGivenPage(UInt32 page) TWOWORDINLINE(0x7000+19, EMMUOpTrap);

#pragma parameter VMSetPTEntryGivenPage(__A0, __A1)
EXTERN_API(void)
VMSetPTEntryGivenPage(UInt32 pte, UInt32 page) TWOWORDINLINE(0x7000+20, EMMUOpTrap);

#pragma parameter VMMakePageWriteThrough(__A0)
EXTERN_API(void)
VMMakePageWriteThrough(UInt32 page) TWOWORDINLINE(0x7000+24, EMMUOpTrap);

#pragma parameter VMMakePageCacheable(__A0)
EXTERN_API(void)
VMMakePageCacheable(UInt32 page) TWOWORDINLINE(0x7000+17, EMMUOpTrap);

#pragma parameter VMMakePageNonCacheable(__A0)
EXTERN_API(void)
VMMakePageNonCacheable(UInt32 page) TWOWORDINLINE(0x7000+18, EMMUOpTrap);

void MakePageQuietReadOnly(void * address)
{
	UInt32 page = ((UInt32)address) >> 12;
	UInt32 pte  = VMGetPTEntryGivenPage(page);
	pte |= 4;//set write-protect bit
	VMSetPTEntryGivenPage(pte, page);
	VMMakePageWriteThrough(page);
	//quiet = write-protect + write-through
}

void MakePageReadWrite(void * address)
{
	UInt32 page = ((UInt32)address) >> 12;
	UInt32 pte  = VMGetPTEntryGivenPage(page);
	pte &= ~4;//clear write-protect bit
	VMSetPTEntryGivenPage(pte, page);
	VMMakePageCacheable(page);
}
 

Crutch

Well-known member
OK I have to ask — how did you figure this out and why is this even possible?  (Thanks for sharing either way, how interesting and weird)

 
Last edited by a moderator:

DBJ314

Member
OK I have to ask — how did you figure this out and why is this even possible?  (Thanks for sharing either way, how interesting and weird)


I did it by reading the reverse-engineered source code of the NanoKernel. https://github.com/elliotnunn/NanoKernel

The NanoKernel has a very complicated system to handle paging-related exceptions.

A DSI Exception happens. (An Alignment Exception also triggers this code path). The faulting instruction is read from memory. Certain bits of it are indexed into this table, which tells the NanoKernel's highly-optimized (and dense) instruction emulation engine exactly what it is supposed to be doing. Every PowerPC instruction that loads or stores data (and several others, like icbi & dcbz) is in that table and can be emulated by the NanoKernel. The actual emulation code is https://github.com/elliotnunn/NanoKernel/blob/b2b5613a103a1bac576ea6c8bc83b391f03ea22b/MROptabCode.shere, but it passes each load/store into a seperate table and code group, which handles loads and stores from 1-8 bytes in length at 8 address alignments (the low 3 bits of the address are indexed into the table). That code then does each individual load and store, but puts a seperate vector table in register sprg3, so any hardware exceptions will point here, instead of the normal handlers. If a DSI exception happens there, one of the possible situations is a write to a read-only page (PP bits are 11) that is marked as write-through (W bit is 1). If that happens, it will skip the instruction that emulated that particular write. (Another code path is when the instruction faults for a reason the NanoKernel can't fix on its own. In that case, it will send an exception to userspace with the emulation state saved to resume once the userspace code has handled it. The usespace code can even handle part of the emulation on its own and send the modified state back to the NanoKernel). The NanoKernel will return back to userspace after the emulation is done. The only indication that the instruction was emulated at all is in the NanoKernel's event counters.

The NanoKernel's trap-based interface and Virtual Memory API are used to tell the NanoKernel that the page should be read-only and write-through in the first place. You can find some tiny scraps of documentation and example code in the leaked Toolbox ROM sources.

 
Top