Пользователь
- Статус
- Оффлайн
- Регистрация
- 12 Дек 2022
- Сообщения
- 128
- Реакции
- 31
Как отладчик реализует трассировку стека?
Введение
Трассировка стека - это процесс восстановления цепочки вызовов функций, приведших к текущей точке выполнения. Это один из ключевых инструментов отладки, особенно при работе с краш-дампами или при анализе поведения программы без доступа к исходному коду
Теоретическая база
Каждый вызов функции в x86 и x64 архитектурах сопровождается сохранением адреса возврата в стек. Это позволяет "пройти" по стеку вызовов, начиная с текущего контекста выполнения, и получить последовательность вызовов вплоть до точки входа в приложение
Как устроен стек вызовов?
На низком уровне каждый стековый кадр обычно содержит:
адрес возврата (return address)
указатель на предыдущий стековый кадр (frame pointer, например, EBP в x86)
локальные переменные и аргументы
Стек растёт вниз, и, двигаясь от одного кадра к другому по цепочке указателей (если она не разрушена оптимизациями компилятора), можно получить полную трассировку вызовов
Роль отладочных символов (PDB)
Для отображения имён функций, номеров строк и переменных по адресам инструкций, отладчику необходимы отладочные символы - информация, которую компилятор сохраняет в отдельном PDB-файле (Program Database)
Эти символы можно загрузить в процессе выполнения при помощи:
DbgHelp API (SymInitialize, SymFromAddr, SymGetLineFromAddr64)
DIASDK от Microsoft
Способы трассировки стека
Существует несколько подходов к получению стека вызовов в Windows:
Практическая реализация трассировки с помощью DbgHelp
Пример вывода
С отладочными символами (PDB присутствует):
Функции отображаются по именам, в том числе и пользовательская f
Без отладочных символов:
Здесь видны только адреса и системные функции. Отсутствие имён означает, что отладчик не может сопоставить адреса с функциями без PDB-файлов. Для решения можно:
указать путь к локальным PDB
использовать сервер символов от Microsoft (SRV*...)
использовать альтернативные средства (DIA SDK)
Как работает StackWalk64?
Функция StackWalk64 абстрагирует работу с различными форматами стека (x86, x64, ARM) и восстанавливает адреса возврата, используя:
таблицы unwind-информации (x64),
цепочку EBP-кадров (x86),
или метаданные от компилятора (если включена оптимизация)
Она использует:
SymFunctionTableAccess64 для поиска unwind-таблиц,
SymGetModuleBase64 для определения базового адреса модуля
Заключение
В статье был рассмотрен один из популярных способов трассировки стека с использованием библиотеки DbgHelp. Как видно, без загруженных символов (PDB-файлов) ценность такой трассировки существенно снижается, поскольку большинство адресов не разрешаются в читаемые имена функций. Тем не менее, даже в таком виде она может быть полезной, особенно при диагностике с краш-дампов или в отладочных билдах.
Отдельная благодарность vmexit за помощь с кодом и консультации. Спасибо всем, кто дочитал до конца - удачи в реверсе и отладке
Введение
Трассировка стека - это процесс восстановления цепочки вызовов функций, приведших к текущей точке выполнения. Это один из ключевых инструментов отладки, особенно при работе с краш-дампами или при анализе поведения программы без доступа к исходному коду
Теоретическая база
Каждый вызов функции в x86 и x64 архитектурах сопровождается сохранением адреса возврата в стек. Это позволяет "пройти" по стеку вызовов, начиная с текущего контекста выполнения, и получить последовательность вызовов вплоть до точки входа в приложение
Как устроен стек вызовов?
На низком уровне каждый стековый кадр обычно содержит:
адрес возврата (return address)
указатель на предыдущий стековый кадр (frame pointer, например, EBP в x86)
локальные переменные и аргументы
Стек растёт вниз, и, двигаясь от одного кадра к другому по цепочке указателей (если она не разрушена оптимизациями компилятора), можно получить полную трассировку вызовов
Роль отладочных символов (PDB)
Для отображения имён функций, номеров строк и переменных по адресам инструкций, отладчику необходимы отладочные символы - информация, которую компилятор сохраняет в отдельном PDB-файле (Program Database)
Эти символы можно загрузить в процессе выполнения при помощи:
DbgHelp API (SymInitialize, SymFromAddr, SymGetLineFromAddr64)
DIASDK от Microsoft
Способы трассировки стека
Существует несколько подходов к получению стека вызовов в Windows:
Практическая реализация трассировки с помощью DbgHelp
C++:
#include <windows.h>
#include <dbghelp.h>
#include <iostream>
#include <iomanip>
#include <vector>
#include <memory>
#pragma comment(lib, "dbghelp.lib")
bool PrintCallStackBackTrace( )
{
SymSetOptions( SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS | SYMOPT_LOAD_LINES );
CONTEXT context{ };
RtlCaptureContext( &context );
// x86 code:
STACKFRAME64 stack{ };
stack.AddrPC.Offset = context.Eip;
stack.AddrPC.Mode = AddrModeFlat;
stack.AddrStack.Offset = context.Esp;
stack.AddrStack.Mode = AddrModeFlat;
stack.AddrFrame.Offset = context.Ebp;
stack.AddrFrame.Mode = AddrModeFlat;
std::vector< uint32_t > backtrace{ };
std::string path = R"(SRV*D:\symbols*https://msdl.microsoft.com/download/symbols)";
if ( !SymInitialize( reinterpret_cast< HANDLE >( -1 ), path.c_str( ), true ) )
{
printf( "[*] failed to SymInitialize with error: %i\n", GetLastError( ) );
return false;
}
while ( StackWalk64( IMAGE_FILE_MACHINE_I386, reinterpret_cast< HANDLE >( -1 ), reinterpret_cast< HANDLE >( -2 ), &stack, &context, nullptr, SymFunctionTableAccess64, SymGetModuleBase64, nullptr ) )
{
backtrace.push_back( stack.AddrPC.Offset );
}
for ( const auto& frame : backtrace )
{
const int max_length = 256;
uint64_t buffer[( sizeof( SYMBOL_INFO ) + max_length * sizeof( wchar_t ) + sizeof( uint64_t ) - 1 ) / sizeof( uint64_t )];
memset( buffer, 0, sizeof( buffer ) );
uint64_t sym_displacement{ };
PSYMBOL_INFO symbol = reinterpret_cast< PSYMBOL_INFO >( &buffer[0] );
symbol->SizeOfStruct = sizeof( SYMBOL_INFO );
symbol->MaxNameLen = max_length - 1;
bool has_symbol = SymFromAddr( reinterpret_cast< HANDLE >( -1 ), frame, &sym_displacement, symbol );
if ( has_symbol )
{
printf( "[*] 0x%x | %s\n", frame, symbol->Name );
}
else
{
printf( "[*] 0x%x\n", frame );
}
}
if ( !SymCleanup( reinterpret_cast< HANDLE >( -1 ) ) )
{
printf( "[*] failed to SymCleanup with error: %i\n", GetLastError( ) );
return false;
}
return true;
}
void f( )
{
PrintCallStackBackTrace( );
}
int main( )
{
f( );
SetConsoleTitleW ( L"test" );
system( "pause" );
return 0;
}
Пример вывода
С отладочными символами (PDB присутствует):
Функции отображаются по именам, в том числе и пользовательская f
Без отладочных символов:
Здесь видны только адреса и системные функции. Отсутствие имён означает, что отладчик не может сопоставить адреса с функциями без PDB-файлов. Для решения можно:
указать путь к локальным PDB
использовать сервер символов от Microsoft (SRV*...)
использовать альтернативные средства (DIA SDK)
Как работает StackWalk64?
Функция StackWalk64 абстрагирует работу с различными форматами стека (x86, x64, ARM) и восстанавливает адреса возврата, используя:
таблицы unwind-информации (x64),
цепочку EBP-кадров (x86),
или метаданные от компилятора (если включена оптимизация)
Она использует:
SymFunctionTableAccess64 для поиска unwind-таблиц,
SymGetModuleBase64 для определения базового адреса модуля
сделал небольшую табличку о том, как устроен стек вызовов
Заключение
В статье был рассмотрен один из популярных способов трассировки стека с использованием библиотеки DbgHelp. Как видно, без загруженных символов (PDB-файлов) ценность такой трассировки существенно снижается, поскольку большинство адресов не разрешаются в читаемые имена функций. Тем не менее, даже в таком виде она может быть полезной, особенно при диагностике с краш-дампов или в отладочных билдах.
Отдельная благодарность vmexit за помощь с кодом и консультации. Спасибо всем, кто дочитал до конца - удачи в реверсе и отладке