EreTIk's Box » Cтатьи, исходники » Аппаратная точка останова на запись и инструкция CMPXCHG


Часто возникает ситуация, когда необходимо мониторить и логировать обращения к некоторому значению памяти. Используя WinDbg, я часто использую аппаратную точку останова вида:


ba w 4 ADDR "?dwo(ADDR);?@$thread;kb;gc;"

В результате получаем какая нить какое значение записывала по целевому адресу. Плюс полезным подспорьем выступает стек вызовов.


Совсем недавно я разгребал подобные логи, целевой код для которых выглядел примерно следующим образом:


  for (; ; )
  {
    if (!InterlockedCompareExchange(pBarrier, TRUE, FALSE))
      break;
    WaitForBarrier();
  }

  // ...

  InterlockedExchange(pBarrier, TRUE);
  SignalBarrier()
                

Не секрет, что InterlockedCompareExchange(...) реализуется процессорной инструкцией CMPXCHG с префиксом LOCK. Соответсвенно, для правильной интерпретации логов, возник вопрос: будет ли срабатывать аппаратная точка останова, если после выполнения CMPXCHG значение памяти не меняется (было TRUE).


Если прочесть документацию от Intel, то там есть интересная ремарка:


To simplify the interface to the processor’s bus, the destination operand receives a write cycle without regard to the result of the comparison. The destination operand is written back if the comparison fails; otherwise, the source operand is written into the destination. (The processor never produces a locked read without also producing a locked write.)

Получается, что запись происходит всегда. Что бы это проверить, набросаем небольшой тест:


struct CCmpXchgData
{
  CHandle m_hThreadStatedEvent;
  CHandle m_hStartBarrierEvent;
  volatile long m_nDest;

  CCmpXchgData() : m_nDest(0) {}
};

// ----------------------------------------------------------------------------

static DWORD CmpXchgExceptionFilter(
  __in DWORD dwExceptionCode
)
{
  if (EXCEPTION_SINGLE_STEP == dwExceptionCode)
  {
    _tprintf(_T(" *** EXCEPTION_SINGLE_STEP occurred\n"));
    return EXCEPTION_CONTINUE_EXECUTION;
  }
  return EXCEPTION_CONTINUE_SEARCH;
}

// ----------------------------------------------------------------------------

static DWORD WINAPI CmpXchgThreadProc(
  __inout PVOID pParam
)
{
  CCmpXchgData *pCmpXchgData = reinterpret_cast<CCmpXchgData *>(pParam);

  VERIFY(::SetEvent(pCmpXchgData->m_hThreadStatedEvent));
  VERIFY(
    WAIT_OBJECT_0 == 
    ::WaitForSingleObject(pCmpXchgData->m_hStartBarrierEvent, INFINITE)
  );

  __try
  {
    _tprintf(
      _T("lock cmpxchg %u <- 1\n"),
       InterlockedCompareExchange(&pCmpXchgData->m_nDest, 1, 0));
    _tprintf(
      _T("lock cmpxchg %u <- 1\n"),
       InterlockedCompareExchange(&pCmpXchgData->m_nDest, 1, 0));
  }
  __except( CmpXchgExceptionFilter(GetExceptionCode()) )
  {
  }

  return ERROR_SUCCESS;
}

// ----------------------------------------------------------------------------

INT _tmain(
  IN  INT nArgCount,
  IN  PTSTR arrArguments[]
)
{
  CCmpXchgData CmpXchgData;

  CmpXchgData.m_hThreadStatedEvent.Attach(
    ::CreateEvent(NULL, TRUE, FALSE, NULL));
  if (!CmpXchgData.m_hThreadStatedEvent)
    return PrintFormatError(_T("Create start event"));

  CmpXchgData.m_hStartBarrierEvent.Attach(
    ::CreateEvent(NULL, TRUE, FALSE, NULL));
  if (!CmpXchgData.m_hStartBarrierEvent)
    return PrintFormatError(_T("Create barrier event"));

  CHandle hCmpXchgThread;
  hCmpXchgThread.Attach(
    ::CreateThread(NULL, 0, &CmpXchgThreadProc, &CmpXchgData, 0, NULL)
  );
  if (!hCmpXchgThread)
    return PrintFormatError(_T("Create test thread"));
  VERIFY(
    WAIT_OBJECT_0 == 
    ::WaitForSingleObject(CmpXchgData.m_hThreadStatedEvent, INFINITE)
  );

  if ((DWORD)-1 == ::SuspendThread(hCmpXchgThread))
    return PrintFormatError(_T("Suspend test thread"));

  CONTEXT ThreadContext;
  ThreadContext.ContextFlags = CONTEXT_ALL;
  if (!::GetThreadContext(hCmpXchgThread, &ThreadContext))
    return PrintFormatError(_T("Get test thread context"));

  ThreadContext.Dr0 = reinterpret_cast<ULONG_PTR>(&CmpXchgData.m_nDest);
  ThreadContext.Dr7 = 0x00010501;
  if (!::SetThreadContext(hCmpXchgThread, &ThreadContext))
    return PrintFormatError(_T("Set test thread context"));

  if ((DWORD)-1 == ::ResumeThread(hCmpXchgThread))
    return PrintFormatError(_T("Suspend test thread"));

  VERIFY(::SetEvent(CmpXchgData.m_hStartBarrierEvent));
  VERIFY(WAIT_OBJECT_0 == ::WaitForSingleObject(hCmpXchgThread, INFINITE));

  return ERROR_SUCCESS;
}
                

Тест использует механизм аппаратных точек останова, манипулируя DR-регистрами. Отладочные исключения в обрабатываются механизмом SEH'а. Запустим собранный код теста, получим результаты:


*** EXCEPTION_SINGLE_STEP occurred lock cmpxchg 0 <- 1 *** EXCEPTION_SINGLE_STEP occurred lock cmpxchg 1 <- 1

Результаты недвусмысленно подтверждают, что даже если по результату выполнения CMPXCHG память не изменила своего значения, происходит отладочное исключение на запись по целевому адресу. Соответственно, в случае с логами, нужно при анализе отличать первую запись значения от повторной попытки записи.


ΞρεΤΙκ