【问题标题】:How to catch a memory write and call function with address of write如何使用写入地址捕获内存写入和调用函数
【发布时间】:2011-12-21 17:47:07
【问题描述】:

我想捕获对特定内存范围的内存写入,并使用正在写入的内存位置的地址调用一个函数。最好是在写入内存已经发生之后。

我知道这可以由操作系统通过调整页表条目来完成。但是,如何在想要执行此操作的应用程序中实现类似的操作?

【问题讨论】:

  • 那里有一个很好的答案,但我怀疑如果你告诉我们你为什么要这样做,可能会有一个更简单的解决方案。
  • @Adrian - 我正在开发一个新的编译器和操作系统,并考虑将两者托管在一个进程中以进行测试和调试。捕捉写入对于模拟一些简单的设备很重要。

标签: windows memory device-emulation


【解决方案1】:

好吧,你可以这样做:

// 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;
}

输出(在 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

就是这样。

您可能需要进行更改以使代码在多个线程中正常工作,使其与其他 SEH 代码(如果有)以及 C++ 异常(如果适用)一起工作。

当然,如果你真的想要它,你可以让它在写入完成后调用写入监控回调函数。为此,您需要将 STATUS_ACCESS_VIOLATION 案例中的内存地址保存在某处(TLS?),以便 STATUS_SINGLE_STEP 案例可以稍后将其拾取并传递给函数。

【讨论】:

  • @bdonlan:为什么滥用?这是对它的记录和合法使用。 :) 你只是不经常这样做。是的,TF有很大帮助。否则需要编写一个(或多或少)完整的指令模拟器,以拦截内存访问指令的完成。
  • 好吧,首先,如果调用链中更深层次的另一个函数捕获单步异常或什么的,那将是“有趣的”...... :)
  • @bdonlan:不要抓住不属于你的东西和你不能处理的东西。 :) 这就是我在回答中警告“其他 SEH 代码”的原因。
  • @tgiphil:我对 .Net/C# 了解不多。
【解决方案2】:

您也可以使用Page Guards,这同样会导致访问异常,但会被系统自动清除(一次性)。这些也应该适用于只读内存。

在您的情况下,您仍然需要单步陷阱技巧来重新启用页面保护。

例如由vkTrace 使用,也可能由 OpenGL/Vulkan Persistently Mapped Buffer 驱动程序实现本身使用。 vkTrace 源代码还展示了如何在 Linux 和 Android 上执行此类操作。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-09-04
    • 1970-01-01
    • 1970-01-01
    • 2014-11-27
    • 2022-11-28
    • 2015-10-14
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多