2011-11-04 16 views
6

Vorrei raccogliere le scritture di memoria su specifici intervalli di memoria e chiamare una funzione con l'indirizzo della posizione di memoria su cui si scrive. Preferibilmente, dopo che la scrittura nella memoria è già avvenuta.Come catturare una memoria scrivere e chiamare la funzione con l'indirizzo di scrittura

So che questo può essere fatto dal sistema operativo girando con le voci della tabella di pagina. Tuttavia, come può essere simile realizzato all'interno di un'applicazione che vuole farlo?

+0

C'è una buona risposta, ma ho il sospetto che se ci dici perché vuoi farlo, potrebbe esserci una soluzione ancora più semplice. –

+0

@Adrian - Sto lavorando a un nuovo compilatore e sistema operativo e ho pensato di ospitare entrambi all'interno di un processo per scopi di test e di debug. Acquisire la scrittura sarebbe importante per l'emulazione di alcuni dispositivi semplici. – tgiphil

risposta

13

Beh, si potrebbe fare qualcosa di simile:

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

uscita (eseguito su 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 

Questa è l'idea.

Probabilmente sarà necessario modificare le cose per far funzionare bene il codice in più thread, farlo funzionare con altro codice SEH (se presente), con eccezioni C++ (se applicabile).

E, naturalmente, se lo si desidera davvero, è possibile farlo chiamare la funzione di controllo di callback di scrittura dopo che la scrittura è stata completata. Per questo è necessario salvare l'indirizzo di memoria dalla custodia STATUS_ACCESS_VIOLATION da qualche parte (TLS?) In modo che la custodia STATUS_SINGLE_STEP possa prenderla in seguito e passare alla funzione.

+0

Bel abuso di SEH! Non sapevo che potresti impostare TF dallo spazio utente ... – bdonlan

+0

@bdonlan: Perché abusare? È un uso documentato e legittimo di esso. :) Semplicemente non farlo spesso. Sì, la TF aiuta molto. Altrimenti bisognerebbe scrivere un emulatore di istruzioni (più o meno) completo per intercettare il completamento delle istruzioni di accesso alla memoria. –

+0

Beh, per uno, sarebbe "interessante" se un'altra funzione più profonda nella catena di chiamate rilevasse eccezioni a passo singolo o qualcosa del genere ... :) – bdonlan

0

In alternativa è possibile utilizzare Page Guards che provocano analogamente un'eccezione all'accesso, ma vengono automaticamente cancellati dal sistema (one-shot). Questi dovrebbero funzionare anche per la memoria di sola lettura.

Nel tuo caso hai ancora bisogno del trucchetto a passaggio singolo per riabilitare il salva pagina.

Utilizzato ad esempio da vkTrace e potenzialmente anche dalle implementazioni dei driver di buffer persistentemente mappati di OpenGL/Vulkan stessi. codice sorgente vkTrace mostra anche come fare questo genere di cose su Linux e Android.