How to catch a memory write and call function with address of write
Asked Answered
E

2

8

I would like to catch memory writes to specific memory ranges and call a function with the address of the memory location being written to. Preferably, after the write to memory has already happened.

I know this can be done by the operating system by twiddling with the page table entries. However, how can this be similar accomplished from within an application that wants to do this?

Edra answered 4/11, 2011 at 4:41 Comment(2)
There's a pretty good answer there, but I suspect that if you tell us why you want to do this, there may be an even simpler solution.Calcutta
@Adrian - I'm working on a new compiler and OS and thought about hosting both within an process for testing and debugging purposes. Catching the write would be important for emulating some simple devices.Edra
A
18

Well, you could do something like this:

// compile with Open Watcom 1.9: wcl386 wrtrap.c

#include <windows.h>
#include <stdio.h>

#ifndef PAGE_SIZE
#define PAGE_SIZE 4096
#endif


UINT_PTR RangeStart = 0;
SIZE_T RangeSize = 0;

UINT_PTR AlignedRangeStart = 0;
SIZE_T AlignedRangeSize = 0;


void MonitorRange(void* Start, size_t Size)
{
  DWORD dummy;

  if (Start &&
      Size &&
      (AlignedRangeStart == 0) &&
      (AlignedRangeSize == 0))
  {
    RangeStart = (UINT_PTR)Start;
    RangeSize = Size;

    // Page-align the range address and size

    AlignedRangeStart = RangeStart & ~(UINT_PTR)(PAGE_SIZE - 1);

    AlignedRangeSize = ((RangeStart + RangeSize - 1 + PAGE_SIZE) &
                        ~(UINT_PTR)(PAGE_SIZE - 1)) -
                       AlignedRangeStart;

    // Make the page range read-only
    VirtualProtect((LPVOID)AlignedRangeStart, 
                   AlignedRangeSize,
                   PAGE_READONLY,
                   &dummy);
  }
  else if (((Start == NULL) || (Size == 0)) &&
           AlignedRangeStart &&
           AlignedRangeSize)
  {
    // Restore the original setting
    // Make the page range read-write
    VirtualProtect((LPVOID)AlignedRangeStart,
                   AlignedRangeSize,
                   PAGE_READWRITE,
                   &dummy);

    RangeStart = 0;
    RangeSize = 0;

    AlignedRangeStart = 0;
    AlignedRangeSize = 0;
  }
}

// This is where the magic happens...
int ExceptionFilter(LPEXCEPTION_POINTERS pEp,
                    void (*pMonitorFxn)(LPEXCEPTION_POINTERS, void*))
{
  CONTEXT* ctx = pEp->ContextRecord;
  ULONG_PTR* info = pEp->ExceptionRecord->ExceptionInformation;
  UINT_PTR addr = info[1];
  DWORD dummy;

  switch (pEp->ExceptionRecord->ExceptionCode)
  {
  case STATUS_ACCESS_VIOLATION:
    // If it's a write to read-only memory,
    // to the pages that we made read-only...
    if ((info[0] == 1) &&
        (addr >= AlignedRangeStart) &&
        (addr < AlignedRangeStart + AlignedRangeSize))
    {
      // Restore the original setting
      // Make the page range read-write
      VirtualProtect((LPVOID)AlignedRangeStart,
                     AlignedRangeSize,
                     PAGE_READWRITE,
                     &dummy);

      // If the write is exactly within the requested range,
      // call our monitoring callback function
      if ((addr >= RangeStart) && (addr < RangeStart + RangeSize))
      {
        pMonitorFxn(pEp, (void*)addr);
      }

      // Set FLAGS.TF to trigger a single-step trap after the
      // next instruction, which is the instruction that has caused
      // this page fault (AKA access violation)
      ctx->EFlags |= (1 << 8);

      // Execute the faulted instruction again
      return EXCEPTION_CONTINUE_EXECUTION;
    }

    // Don't handle other AVs
    goto ContinueSearch;

  case STATUS_SINGLE_STEP:
    // The instruction that caused the page fault
    // has now succeeded writing to memory.
    // Make the page range read-only again
    VirtualProtect((LPVOID)AlignedRangeStart,
                   AlignedRangeSize,
                   PAGE_READONLY,
                   &dummy);

    // Continue executing as usual until the next page fault
    return EXCEPTION_CONTINUE_EXECUTION;

  default:
  ContinueSearch:
    // Don't handle other exceptions
    return EXCEPTION_CONTINUE_SEARCH;
  }
}


// We'll monitor writes to blah[1].
// volatile is to ensure the memory writes aren't
// optimized away by the compiler.
volatile int blah[3] = { 3, 2, 1 };

void WriteToMonitoredMemory(void)
{
  blah[0] = 5;
  blah[0] = 6;
  blah[0] = 7;
  blah[0] = 8;

  blah[1] = 1;
  blah[1] = 2;
  blah[1] = 3;
  blah[1] = 4;

  blah[2] = 10;
  blah[2] = 20;
  blah[2] = 30;
  blah[2] = 40;
}

// This pointer is an attempt to ensure that the function's code isn't
// inlined. We want to see it's this function's code that modifies the
// monitored memory.
void (* volatile pWriteToMonitoredMemory)(void) = &WriteToMonitoredMemory;

void WriteMonitor(LPEXCEPTION_POINTERS pEp, void* Mem)
{
  printf("We're about to write to 0x%X from EIP=0x%X...\n",
         Mem,
         pEp->ContextRecord->Eip);
}

int main(void)
{
  printf("&WriteToMonitoredMemory() = 0x%X\n", pWriteToMonitoredMemory);
  printf("&blah[1] = 0x%X\n", &blah[1]);

  printf("\nstart\n\n");

  __try
  {
    printf("blah[0] = %d\n", blah[0]);
    printf("blah[1] = %d\n", blah[1]);
    printf("blah[2] = %d\n", blah[2]);

    // Start monitoring memory writes
    MonitorRange((void*)&blah[1], sizeof(blah[1]));

    // Write to monitored memory
    pWriteToMonitoredMemory();

    // Stop monitoring memory writes
    MonitorRange(NULL, 0);

    printf("blah[0] = %d\n", blah[0]);
    printf("blah[1] = %d\n", blah[1]);
    printf("blah[2] = %d\n", blah[2]);
  }
  __except(ExceptionFilter(GetExceptionInformation(),
                           &WriteMonitor)) // write monitor callback function
  {
    // never executed
  }

  printf("\nstop\n");
  return 0;
}

Output (run on Windows XP):

&WriteToMonitoredMemory() = 0x401179
&blah[1] = 0x4080DC

start

blah[0] = 3
blah[1] = 2
blah[2] = 1
We're about to write to 0x4080DC from EIP=0x4011AB...
We're about to write to 0x4080DC from EIP=0x4011B5...
We're about to write to 0x4080DC from EIP=0x4011BF...
We're about to write to 0x4080DC from EIP=0x4011C9...
blah[0] = 8
blah[1] = 4
blah[2] = 40

stop

That's the idea.

You will likely need to change things around to make the code work well in multiple threads, make it work with other SEH code (if any), with C++ exceptions (if applicable).

And, of course, if you really want it, you can make it call the writes monitoring callback function after the write's been completed. For that you'll need to save the memory address from the STATUS_ACCESS_VIOLATION case somewhere (TLS?) so that the STATUS_SINGLE_STEP case can pick it up later and pass to the function.

Amato answered 4/11, 2011 at 12:5 Comment(4)
@bdonlan: Why abuse? It's a documented and legitimate use of it. :) You just don't do it often. Yeah, TF helps a lot. Otherwise one would need to write a (more or less) complete instruction emulator in order to intercept completion of memory accessing instructions.Amato
Well, for one, it would be "interesting" if another function deeper in the call chain caught single-step exceptions or something... :)Sahib
@bdonlan: Don't catch what's not yours and what you can't handle. :) That's why in the answer I warned about 'other SEH code'.Amato
@tgiphil: I don't know much about .Net/C#.Amato
S
1

Alternatively you may use Page Guards which similarly cause an exception on access but are automatically cleared by the system (one-shot). Those should also work for read-only memory.

In your case you still need the single-step trap trick though to re-enable the page guard.

Used for example by vkTrace and potentially also by OpenGL/Vulkan Persistently Mapped Buffer driver implementations themselves. vkTrace source code also shows how to do this kind of thing on Linux and Android.

Scandinavian answered 18/8, 2017 at 15:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.