Как бы хорошо не работал отдел тестирования, все же часто продукт уходит пользователю с изрядным количеством ошибок. В зависимости от продукта борьба с этими ошибками ведется по разному: ведение журнала операций, взятие у пользователя проблемного проекта, выезд к пользователю и т.д.
Для борьбы с критическими ошибками, приводящими к вылету, в моей компании используется механизм автоматического снятия дампа памяти и отправки нам на сервер. Каждый дамп подвергается изучению и на основе информации полученной из дампа принимаются дальнейшие решения.
Однако для того, что бы проанализировать дамп наименьшими силами необходимы файлы отладочной информации, в частности PDB-файлы, осуществляющие обратную связь от исполняемого файла к исходникам. В общем виде PDB файлы в моей компании сохраняются в отведенной и доступное всем место.
Однако не так давно случилось так, что PDB-файлы от выпущенной версии были случайно удалены. О том как я восстанавливал отладочную информацию я и хочу рассказать.
Первое что необходимо было сделать это собрать версию из тех же исходников, что не стало проблемой (будем называть эту сборку PDB_сборкой). В результате я получил PDB файлы примерно того же содержания. Примерно, потому что бинарно они не идентичны. Гарантию того, что они будут корректны для сборки у пользователя так же нету, однако опыт отладки подсказывает что большая часть проблем при помощи полученных PDB Можно решить.
Далее мне требовалось найти способ обмануть отладчик Visual Studio и заставить загружать эти PDB (для честности надо сказать что WinDbg может загружать практически куда угодно что угодно. Для этого надо выполнить команду .symopt+0x40). Проблема здесь заключается в том, что для каждой сборки генерируется GUID, который зашивается в глубины всех исполняемых и отладочных файлов. Так же студия при загрузке проверяет отметку времени последней записи в файл, которая должна быть идентична бинарному файлу.
Поиск в интернете меня навел на утилиту ChkMach, которая позволяет подменить GUID.
На деле оказалось что утилита не смогла справиться со всеми моими PDB файлами (если PDB файл размером больше примерно 70 метров утилита отказывается его перепрошивать).
Было решено написать нечто свое, что позволило бы сделать нужные махинации.
Алгоритм получился примерно следующий:
1) Загрузить исполняемый файл из сборки ушедшей пользователю и получить из него GUID. Этот GUID надо будет записать в PDB из PDB_сборки.
2) Загрузить PDB файл из PDB_сборки.
3) Получить GUID зашитый в PDB.
4) Найти все вхождения полученного GUID в PDB и заменить их на GUID полученный в пункте 1.
Я нашел два способа получить GUID из исполняемого файла.
Первый способ — это использование интерфейсов IDia. В Visual Studio (%VSINSTALLDIR%\DIA SDK\Samples\) есть пример использования. У этого способа однако есть проблема. Если для загружаемого исполняемого файла не найден PDB-файл, то исполняемый файл загружен не будет.
Второй способ заключается в том, что бы разбирать структуру исполняемого файла самостоятельно (то есть открыть как обычный бинарный файл и читать, читать и читать). Вот здесь есть статья посвященная поиску сегмента отладочной информации в исполняемом файле, так же есть исходник примера, которым я и пользовался. Исходного кода там получилось достаточно много и здесь его приводить не буду, так что лучше качайте исходник (ссылка в конце статьи).
Что касается пункта 2 и 3, оказалось их крайне легко решить, воспользовавшись интерфейсами IDia.
Поиск и замена GUID в файле, я думаю является, достаточно простой задачей, и описывать ее я не буду.
Однако стоит остановиться на одной мелочи.
Объявление структуры GUID выглядит так.
В силу того, что в конечном виде этот GUID предполагалось писать как массив байтов, необходимо было учесть порядок байт, и как следствие развернуть поля Data1, Data2 и Data3
Для борьбы с критическими ошибками, приводящими к вылету, в моей компании используется механизм автоматического снятия дампа памяти и отправки нам на сервер. Каждый дамп подвергается изучению и на основе информации полученной из дампа принимаются дальнейшие решения.
Однако для того, что бы проанализировать дамп наименьшими силами необходимы файлы отладочной информации, в частности PDB-файлы, осуществляющие обратную связь от исполняемого файла к исходникам. В общем виде PDB файлы в моей компании сохраняются в отведенной и доступное всем место.
Однако не так давно случилось так, что PDB-файлы от выпущенной версии были случайно удалены. О том как я восстанавливал отладочную информацию я и хочу рассказать.
Первое что необходимо было сделать это собрать версию из тех же исходников, что не стало проблемой (будем называть эту сборку PDB_сборкой). В результате я получил PDB файлы примерно того же содержания. Примерно, потому что бинарно они не идентичны. Гарантию того, что они будут корректны для сборки у пользователя так же нету, однако опыт отладки подсказывает что большая часть проблем при помощи полученных PDB Можно решить.
Далее мне требовалось найти способ обмануть отладчик Visual Studio и заставить загружать эти PDB (для честности надо сказать что WinDbg может загружать практически куда угодно что угодно. Для этого надо выполнить команду .symopt+0x40). Проблема здесь заключается в том, что для каждой сборки генерируется GUID, который зашивается в глубины всех исполняемых и отладочных файлов. Так же студия при загрузке проверяет отметку времени последней записи в файл, которая должна быть идентична бинарному файлу.
Поиск в интернете меня навел на утилиту ChkMach, которая позволяет подменить GUID.
На деле оказалось что утилита не смогла справиться со всеми моими PDB файлами (если PDB файл размером больше примерно 70 метров утилита отказывается его перепрошивать).
Было решено написать нечто свое, что позволило бы сделать нужные махинации.
Алгоритм получился примерно следующий:
1) Загрузить исполняемый файл из сборки ушедшей пользователю и получить из него GUID. Этот GUID надо будет записать в PDB из PDB_сборки.
2) Загрузить PDB файл из PDB_сборки.
3) Получить GUID зашитый в PDB.
4) Найти все вхождения полученного GUID в PDB и заменить их на GUID полученный в пункте 1.
Я нашел два способа получить GUID из исполняемого файла.
Первый способ — это использование интерфейсов IDia. В Visual Studio (%VSINSTALLDIR%\DIA SDK\Samples\) есть пример использования. У этого способа однако есть проблема. Если для загружаемого исполняемого файла не найден PDB-файл, то исполняемый файл загружен не будет.
Второй способ заключается в том, что бы разбирать структуру исполняемого файла самостоятельно (то есть открыть как обычный бинарный файл и читать, читать и читать). Вот здесь есть статья посвященная поиску сегмента отладочной информации в исполняемом файле, так же есть исходник примера, которым я и пользовался. Исходного кода там получилось достаточно много и здесь его приводить не буду, так что лучше качайте исходник (ссылка в конце статьи).
Что касается пункта 2 и 3, оказалось их крайне легко решить, воспользовавшись интерфейсами IDia.
bool LoadDataFromPdb(const wchar_t *pdbFileName,
IDiaDataSource **ppSource,
IDiaSession **ppSession,
IDiaSymbol **ppGlobal,
GUID * guid)
CoInitialize(NULL);
hr = CoCreateInstance(__uuidof(DiaSource),
NULL,
CLSCTX_INPROC_SERVER,
__uuidof(IDiaDataSource),
(void **) ppSource);
if (FAILED(hr))
{
wprintf(L"CoCreateInstance failed - HRESULT = %08X\n", hr);
return false;
}
hr = (*ppSource)->loadDataFromPdb(pdbFileName);
if (FAILED(hr))
{
wprintf(L"loadDataFromPdb failed - HRESULT = %08X\n", hr);
return false;
}
hr = (*ppSource)->openSession(ppSession);
if (FAILED(hr))
{
wprintf(L"openSession failed - HRESULT = %08X\n", hr);
return false;
}
hr = (*ppSession)->get_globalScope(ppGlobal);
return !FAILED((*ppGlobal)->get_guid(guid));
Поиск и замена GUID в файле, я думаю является, достаточно простой задачей, и описывать ее я не буду.
Однако стоит остановиться на одной мелочи.
Объявление структуры GUID выглядит так.
typedef struct _GUID {
unsigned long Data1;
unsigned short Data2;
unsigned short Data3;
unsigned char Data4[ 8 ];
} GUID;
В силу того, что в конечном виде этот GUID предполагалось писать как массив байтов, необходимо было учесть порядок байт, и как следствие развернуть поля Data1, Data2 и Data3
char getByteFromLong(long l, int pos)
{
_ASSERTE(pos < 4 && pos >= 0);
return *(((byte *)(&l)) + pos);
}
char getByteFromShort(short l, int pos)
{
_ASSERTE(pos < 2 && pos >= 0);
return *(((byte *)(&l)) + pos);
}
std::vector transformGuid(const GUID & guid)
{
std::vector v;
v.push_back(getByteFromLong(guid.Data1, 0));
v.push_back(getByteFromLong(guid.Data1, 1));
v.push_back(getByteFromLong(guid.Data1, 2));
v.push_back(getByteFromLong(guid.Data1, 3));
v.push_back(getByteFromShort(guid.Data2, 0));
v.push_back(getByteFromShort(guid.Data2, 1));
v.push_back(getByteFromShort(guid.Data3, 0));
v.push_back(getByteFromShort(guid.Data3, 1));
for (int i = 0; i < 8; ++i)
v.push_back(guid.Data4[i]);
return v;
}
В итоге я получил PDB файлы, которые подхватываются студией при открытии дампа, позволяют видеть корректный стэк и даже показывает мне исходники. Первичный осмотр показал, что восстановленная отладочная информация корректна с большего корректная. С большего, потому что гарантировать на 100% это нельзя (Microsoft по этому поводу вообще говорит, что утерянные PDB не вернуть никак).
Здесь можно взять бинарник и исходник
Вдохновение для статьи и поделки бралось отсюда debuginfo.com