вторник, 28 октября 2008 г.

"Анатомия" стека, или Зачем нужны отладочные символы...

Постулат №1 программиста, занимающегося нетривиальной отладкой: "Чтобы получить правильный стэк потока нужно иметь все отладочные символы для загруженных в процесс модулей".
Целью этого поста является не столько ответ на вопрос "как" (ресурсов на эту тему хватает, в том числе и русскоязычных), сколько ответ на вопрос "почему"...

Для начала совсем короткий экскурс на уровень кода, генерируемого обычно компилятором для следующей функции (для примера использован MSVC), платформа x86 (Windows):

int my_function(int arg1, int arg2)
{
volatile int my_local(rand() );
return (arg1 + arg2) % my_local;
}

....
my_function(1, 2);


00FB143E  push        2    
00FB1440 push 1
00FB1442 call my_function (0FB11A4h)
00FB1447 add esp,8

001E13B0 push ebp
001E13B1 mov ebp,esp
001E13B3 sub esp,0CCh
.....
001E13F1 add esp,0CCh
001E13F7 cmp ebp,esp
001E13F9 call @ILT+325(__RTC_CheckEsp) (1E114Ah)
001E13FE mov esp,ebp
001E1400 pop ebp
001E1401 ret

Большинство функций, которые разработчик может увидеть в процессе отладки начинается комбинацией команд "push ebp", "mov ebp, esp", "sub esp, XXX".
Поскольку стек в x86 растет вверх (к младшим адресам), то это позволяет иметь следующую структуру на стеке:

local var 1
local var 2
old ebp
ret addr
param 1
param 2
...
param N

При этом доступ к параметрам вызова возможен через [ebp + XXX], доступ к локальным переменным возможен через [ebp - XXX], и, что самое интересное, по адресу [ebp] хранится адрес предыдущей такой же структуры (собственно о стековом фрейме на дилетантском уровне это почти все - именно этот факт и использует DbgHelp, когда производит раскрутку стека).
Параметры вызывающая процедура помещает на стек в обратном порядке, она же по возвращении из процедуры восстанавливает значение esp.

Это соглашение по вызову называется cdecl, есть еще масса других типа stdcall, fastcall и т.д, но на суть дела это влияет не сильно, влияет другое.
Зачастую, накладные расходы на формирование стекового фрейма оказываются с точки зрения компилятора лишними (например, для функции, принимающей параметры в регистрах и в них же хранящей локальные переменные).
В этом случае, если включена оптимизация "Frame Pointer Omission (FPO)", формирование стекового фрейма не производится.
Зато в PDB-файле появляется запись типа FPO_DATA, которая позволяет DbgHelp (при доступности этого файла) правильно обрабатывать такие функции.
Если же этого не происходит, то DbgHelp, встречая такие функции, "садится в лужу", после чего и появляется сакраментальное "the following frames may be wrong".

Основной вывод - для правильной "раскрутки" стека в момент отладки необходимо иметь на руках все символы, как от своей программы и операционной системы, так и от третьесторонних библиотек.
Те поставщики библиотек, которые понимают суть вопроса, символы в том или ином виде поставляют.
А вот их остутствие заставляет задуматься - а нужно ли с таким поставщиком иметь дела?

Комментариев нет: