Проблема трассировки стека C ++ - PullRequest
6 голосов
/ 24 февраля 2012

Я работаю над классом, который хотел бы использовать для регистрации текущего стека вызовов на компьютерах с Windows Vista / 7.(Очень похоже на «Ходить по стеку вызовов» http://www.codeproject.com/Articles/11132/Walking-the-callstack).

Сначала я использовал RtlCaptureContext для получения текущей записи контекста, затем я использовал StackWalk64 для получения отдельных кадров стека. Теперь я понял, что счетчик программ в STACKFRAME64.AddrPC фактически изменяется для конкретной строки кода всякий раз, когда я закрываю свою программу и запускаю ее снова. По какой-то причине я думал, что PC-адрес для конкретной строки кода останется прежним, пока я не изменю исходный код и не перекомпилируюэто снова.

Мне нужен PC-адрес, чтобы использовать SymFromAddr и SymGetLineFromAddr64 для получения информации о вызываемой функции, файле кода и номере строки. К сожалению, это работает только до тех пор, пока работает Program-Debug-Database (PDB-Файл), но мне не разрешено предоставлять это клиенту.

Мой план состоял в том, чтобы записать адреса ПК в стеке вызовов (всякий раз, когда это необходимо), а затем отправить его от клиента кменя. Так что я мог использовать мои PDB-файлы, чтобы узнать, какие функции были названы бно это, конечно, работает, только если адреса ПК являются уникальными идентификаторами.Поскольку они меняются каждый раз, когда я запускаю программу, я не могу использовать этот подход.

Знаете ли вы лучший способ прочитать стек вызовов или преодолеть проблему с изменяющимся счетчиком программы?

Я думаю, что одним из возможных решений может быть получение PC-адреса в известном месте и использование его в качестве справочного материала для определения только смещения между различными адресами ПК.Кажется, это работает, но я не уверен, что это правильный метод и всегда будет работать.

Большое спасибо за вашу помощь!Я опубликую окончательное (инкапсулированное) решение на codeproject.com и, ЕСЛИ ВАМ НРАВИТСЯ, скажу, что вы мне помогли.

Ответы [ 4 ]

5 голосов
/ 24 февраля 2012

Используя информационную форму CONTEXT, вы можете найти раздел функций и смещение в изображении PE.Например, вы можете использовать эту информацию для получения имени функции из файла .map, сгенерированного компоновщиком.

  1. Get CONTEXT struct.Вы заинтересованы в участнике программы.Поскольку CONTEXT зависит от платформы, вы должны сами это выяснить.Вы делаете это уже при инициализации, например STACKFRAME64.AddrPC.Offset = CONTEXT.Rip для x64 Windows.Теперь мы начинаем обход стека и используем STACKFRAME64.AddrPC.Offset, заполненный StaclkWalk64 в качестве нашей отправной точки.

  2. Вам необходимо преобразовать его в относительный виртуальный адрес (RVA), используя базовый адрес выделения:RVA = STACKFRAME64.AddrPC.Offset - AllocationBase.Вы можете получить AllocationBase, используя VirtualQuery.

  3. Когда у вас есть это, вам нужно выяснить, в какой раздел попадает этот RVA, и вычесть из него начальный адрес раздела, чтобы получить SectionOffset: SectionOffset = RVA - SectionBase = STACKFRAME64.AddrPC.Offset - AllocationBase - SectionBase.Для этого вам нужно получить доступ к структуре заголовка образа PE (IMAGE_DOS_HEADER, IMAGE_NT_HEADER, IMAGE_SECTION_HEADER), чтобы получить количество разделов в PE и их начальный / конечный адреса.Это довольно просто.

Вот и все.Теперь у вас есть номер раздела и смещение в изображении PE.Смещение функции - это наибольшее смещение, меньшее, чем SectionOffset в файле .map.

Я могу опубликовать код позже, если хотите.

РЕДАКТИРОВАТЬ: Код для печати function address (мы предполагаем x64 универсальный ЦП):

#include <iostream>
#include <windows.h>
#include <dbghelp.h>

void GenerateReport( void )
{
  ::CONTEXT lContext;
  ::ZeroMemory( &lContext, sizeof( ::CONTEXT ) );
  ::RtlCaptureContext( &lContext );

  ::STACKFRAME64 lFrameStack;
  ::ZeroMemory( &lFrameStack, sizeof( ::STACKFRAME64 ) );
  lFrameStack.AddrPC.Offset = lContext.Rip;
  lFrameStack.AddrFrame.Offset = lContext.Rbp;
  lFrameStack.AddrStack.Offset = lContext.Rsp;
  lFrameStack.AddrPC.Mode = lFrameStack.AddrFrame.Mode = lFrameStack.AddrStack.Mode = AddrModeFlat;

  ::DWORD lTypeMachine = IMAGE_FILE_MACHINE_AMD64;

  for( auto i = ::DWORD(); i < 32; i++ )
  {
    if( !::StackWalk64( lTypeMachine, ::GetCurrentProcess(), ::GetCurrentThread(), &lFrameStack, lTypeMachine == IMAGE_FILE_MACHINE_I386 ? 0 : &lContext,
            nullptr, &::SymFunctionTableAccess64, &::SymGetModuleBase64, nullptr ) )
    {
      break;
    }
    if( lFrameStack.AddrPC.Offset != 0 )
    {
      ::MEMORY_BASIC_INFORMATION lInfoMemory;
      ::VirtualQuery( ( ::PVOID )lFrameStack.AddrPC.Offset, &lInfoMemory, sizeof( lInfoMemory ) );
      ::DWORD64 lBaseAllocation = reinterpret_cast< ::DWORD64 >( lInfoMemory.AllocationBase );

      ::TCHAR lNameModule[ 1024 ];
      ::GetModuleFileName( reinterpret_cast< ::HMODULE >( lBaseAllocation ), lNameModule, 1024 );

      PIMAGE_DOS_HEADER lHeaderDOS = reinterpret_cast< PIMAGE_DOS_HEADER >( lBaseAllocation );
      PIMAGE_NT_HEADERS lHeaderNT = reinterpret_cast< PIMAGE_NT_HEADERS >( lBaseAllocation + lHeaderDOS->e_lfanew );
      PIMAGE_SECTION_HEADER lHeaderSection = IMAGE_FIRST_SECTION( lHeaderNT );
      ::DWORD64 lRVA = lFrameStack.AddrPC.Offset - lBaseAllocation;
      ::DWORD64 lNumberSection = ::DWORD64();
      ::DWORD64 lOffsetSection = ::DWORD64();

      for( auto lCnt = ::DWORD64(); lCnt < lHeaderNT->FileHeader.NumberOfSections; lCnt++, lHeaderSection++ )
      {
        ::DWORD64 lSectionBase = lHeaderSection->VirtualAddress;
        ::DWORD64 lSectionEnd = lSectionBase + max( lHeaderSection->SizeOfRawData, lHeaderSection->Misc.VirtualSize );
        if( ( lRVA >= lSectionBase ) && ( lRVA <= lSectionEnd ) )
        {
          lNumberSection = lCnt + 1;
          lOffsetSection = lRVA - lSectionBase;
          break;
        }
      }    
      std::cout << lNameModule << " : 000" << lNumberSection << " : " << reinterpret_cast< void * >( lOffsetSection ) << std::endl;
    }
    else
    {
      break;
    }
  }
}

void Run( void );
void Run( void )
{
 GenerateReport();
 std::cout << "------------------" << std::endl;
}

int main( void )
{
  ::SymSetOptions( SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS );
  ::SymInitialize( ::GetCurrentProcess(), 0, 1 );

  try
  {
    Run();
  }
  catch( ... )
  {
  }
  ::SymCleanup( ::GetCurrentProcess() );

  return ( 0 );
}

Обратите внимание, нашстек вызовов (наизнанку) GenerateReport()->Run()->main().Вывод программы (на моей машине путь абсолютно):

D:\Work\C++\Source\Application\Prototype.Console\Prototype.Console.exe : 0001 : 0000000000002F8D
D:\Work\C++\Source\Application\Prototype.Console\Prototype.Console.exe : 0001 : 00000000000031EB
D:\Work\C++\Source\Application\Prototype.Console\Prototype.Console.exe : 0001 : 0000000000003253
D:\Work\C++\Source\Application\Prototype.Console\Prototype.Console.exe : 0001 : 0000000000007947
C:\Windows\system32\kernel32.dll : 0001 : 000000000001552D
C:\Windows\SYSTEM32\ntdll.dll : 0001 : 000000000002B521
------------------

Теперь стек вызовов в терминах адресов (наизнанку) 00002F8D->000031EB->00003253->00007947->0001552D->0002B521.Сравнение первых трех смещений с содержимым файла .map:

...

 0001:00002f40       ?GenerateReport@@YAXXZ     0000000140003f40 f   FMain.obj
 0001:000031e0       ?Run@@YAXXZ                00000001400041e0 f   FMain.obj
 0001:00003220       main                       0000000140004220 f   FMain.obj

...

, где 00002f40 - самое близкое меньшее смещение к 00002F8D и так далее.Последние три адреса относятся к функциям CRT / OS, которые вызывают main (_tmainCRTstartup и т. Д.) - мы должны их игнорировать ...

Итак, мы видим, что мы можем восстановить трассировку стека с помощью.map файл.Чтобы сгенерировать трассировку стека для сгенерированного исключения, все, что вам нужно сделать, это поместить код GenerateReport() в конструктор исключений (на самом деле этот GenerateReport() был взят из моего кода конструктора пользовательских классов исключений (некоторая его часть)).

4 голосов
/ 24 февраля 2012

Самого стека недостаточно, вам нужна карта загруженных модулей, чтобы затем вы могли связать любой адрес (случайный, истинный) с модулем и найти символ PDB. Но вы действительно заново изобретаете колесо, потому что есть как минимум два хорошо поддерживаемых готовых решения для решения этой проблемы:

  • API-интерфейс мини-дамп DbgHlp для Windows: MiniDumpWriteDump. Ваше приложение не должно вызывать это напрямую, но вместо этого вы должны поставлять с крошечным .exe-файлом, что все, что он делает, - это дамп процесса (идентификатор процесса, указанный в качестве аргумента), и ваше приложение, когда сталкивается с ошибкой, должно запустить это. Отлично, а затем официант для его завершения. Причина в том, что процесс dumper замораживает сброшенный процесс во время дампа, поэтому сбрасываемый процесс не может быть тем же процессом, который принимает дамп. Эта схема является общей для всех приложений, которые реализуют WER . Не говоря уже о том, что полученный дамп является настоящим .mdmp, который вы можете загрузить в WinDbg (или в VisualStudio, если вам это нужно).

  • кроссплатформенное решение с открытым исходным кодом: Breakpad . Используется Chrome, Firefox, Picassa и другими известными приложениями.

Так что, в первую очередь, не изобретайте велосипед. Как примечание, есть также службы, которые вносят дополнительный вклад в отчеты об ошибках, такие как агрегация, уведомления, отслеживание и автоматические клиентские ответы, такие как вышеупомянутая WER, предлагаемая Microsoft (ваш код должен иметь цифровую подпись, чтобы соответствовать требованиям), airbreak.io , exceptioneer.com , bugcollect.com (этот действительно создан вами) и другие, но на самом деле. только WER работает с собственными приложениями Windows.

2 голосов
/ 24 февраля 2012

Вам необходимо отправить отображение работающей памяти программы, в котором будет указана базовая адресная библиотека / программа, загруженная из клиента.

Затем вы можете вычислить символ по базовому адресу.

1 голос
/ 24 февраля 2012

Я бы посоветовал взглянуть на настройки вашего проекта Visual Studio: Linker-> Advanced-> Randomized Base Address для всех ваших программ и зависимых библиотек (что вы можете восстановить) и попробуйте снова. Это единственное, что приходит на ум.

Надеюсь, это поможет.

...