2011-11-04 78 views
6

我想捕获内存写入特定的内存范围,并调用一个函数与正在写入的内存位置的地址。优选地,在写入存储器之后已经发生。如何捕获写入地址的内存写入和调用函数

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

+0

这里有一个相当不错的答案,但我怀疑如果你告诉我们为什么你要这样做,那么可能有一个更简单的解决方案。 –

+0

@Adrian - 我正在研究一种新的编译器和操作系统,并考虑在进程内部进行测试和调试。捕捉写入对于模拟一些简单的设备非常重要。 – tgiphil

回答

13

嗯,你可以做这样的事情:

// 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案件可以稍后提取并传递给该函数。

+0

很好的滥用SEH!我不知道你可以从用户空间设置TF ... – bdonlan

+0

@bdonlan:为什么滥用?这是一个记录和合法使用它。 :)你只是不经常这样做。是的,TF帮助很大。否则就需要编写一个(或多或少)完整的指令模拟器来拦截内存访问指令的完成。 –

+0

呃,其中一个,如果调用链中更深层的另一个功能捕捉到单步异常或其他东西,那将是“有趣的”...... :) – bdonlan

0

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

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

用于例如vkTrace以及潜在的OpenGL/Vulkan持久映射缓冲区驱动程序实现本身。 vkTrace源代码还显示了如何在Linux和Android上执行此类事情。