Рецепты отладки. Return в пустоту.

Сегодня речь пойдет про отладку некоторых проблемных событий, на которые я получаю частый ответ типа “а хз где сдохло, висим на нулевом адресе, stack frame нету, сделать ничего не могу”.

Сегодня речь пойдет про отладку некоторых проблемных событий, на которые я получаю частый ответ типа “а хз где сдохло, висим на нулевом адресе, stack frame нету, сделать ничего не могу”.

Ну, во-первых, как такую ситуацию можно получить наиболее простым способом?

void testFunc()

{

    int array[10];    array[12] = 0;

    return;

}
_Winnie C++ Colorizer

Если интересуют технические подробности того, что произошло, то:

  • вызов call testFunc() положил на стек адрес возврата;
  • после этого во входе в функцию прошел push ebp для стекфрейма (напоминаю, что стек растет вниз);
  • после этого сам ebp указывает на начало локальной области аргументов (40 байт для int array[10]);
  • по смещению 44 байта находится старое значение ebp (его можно использовать для раскрутки стека или чтобы достучаться до локальных переменных родительской функции);
  • по смещению 48 находится наш родной адрес возврата, который мы затерли.

Соответственно ret вернет нас не на запомненный адрес, а на ноль.
Компилируем, запускаем в debug: цель достигнута – падение в адресе 00000000, callstack не определяется, куда ползти, вроде бы действительно хз.


Окно CallStack


Окно дизассемблера

Но не все так плохо. Давайте посмотрим, что можно выяснить в этой ситуации.

Значение стека непосредственно возвращаемой функции действительно затерто нулем. Однако в стеке (если он только не засран вусмерть) должно было остаться значение предыдущей вызванной функции. Поэтому делаем следующее:

1. Открываем на экране окна:

  • Debug-Windows-Memory-Memory1.
  • Debug-Windows-Modules.
  • Debug-Windows-Disassembly.

2. Окно Memory1 при помощи контекстного меню переводим в режим отображения 4-byte Integer.
3. В окне Memory1 в поле адреса вбиваем “ESP” и жмем на Enter.
4. Начинаем просматривать окно Memory1 на предмет наличия каких-то похожих на адрес возврата цифр.
5. Для того, чтобы определить адекватность цифр, надо сделать следующее:

  • в открытом окне modules сравнить искомые числа с диапазоном адресов, которые занимает тот или иной модуль.
  • если число находится в нужном диапазоне, перейти в окно Disassembly и вбить этот адрес. Он должен показать дизасссемблеровский код, в котором указатель указывает на команду, следующую за call.


Область памяти стека


Список модулей и их адресов


Адрес предыдущего вызова перед сбоем

В зависимости от того, насколько далеко мы продвинулись по стеку в поисках заветной строчки с адресом возврата, этот call будет являться адресом предыдущей вызванной функции или каким-то еще из более глубоких адресов. После этого уже можно будет поставить на эту строчку брекпоинт (не бойтесь делать это в окне дизассемблера – это не больно :)), перезапустить программу и пройти по вызываемым функциям до точки падения. Если отладочной информации по каким-то причинам нет, но спасен map файл линкера, можно выяснить названия функций по их адресам через этот файл. Ну и наконец, если начинает падать приложение после защиты, и нет никакой возможности запустить под отладчиком, то можно просто напихать в код OutputDebugString(“Enter ” __FUNCTION__), OutputDebugString(“Leave ” __FUNCTION__). Только не надо определять дополнительные переменные или классы в подозрительных функциях – вы рискуете испортить карту распределения стека, и злосчастное падение перестанет проявляться.

Падение может быть и не только на ноль. Например, в следующем коде

void testFunc()

{

    int array[10];

    int c;    array[12] = c;

    return;

}
_Winnie C++ Colorizer

мы в дебаге будем падать на 0xcccccccc. В общем, вариантов много, поиск по стеку – срабатывает в большинстве случаев.

Приятной вам отладки.

  • ypp

    +1 к debug skills. Спасибо ;)

  • Tsukrov

    Кажется, чоткие пацаны пишут не OutputDebugString(“ENTER “__FUCTION__), а

    FuncGuard f(__FUNCTION__);

    Или что-то типа FuncGuard f(); чтобы не заводить данных на стэке.

  • http://alfeg.blogspot.com AlfeG

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

  • http://www.sdl.ru TSS

    Ну, примерчик высосан из пальца, но ничего, так, наглядненько.
    К слову, ret по 00000000 приведет к падению не в нуле, а на самом ret, т.к. физически сначала считывает команда из памяти, а только потом она выполняется.

  • ForestMan

    Спасибо, будем знать. :)
    До этого, при решении подобных проблем, в основном применял брекпоинты или метод “OutputDebugString(”Enter ” __FUNCTION__), OutputDebugString(”Leave ” __FUNCTION__)”.

  • Dront

    2TSS: вилимо, ret первым делом кладет в IP новый адрес (ноль), потом уже все остальное. Свалится может и ret, а не инструкция по адресу 0, но вот отладчик точно скажет, что мы сейчас в нуле.

    Второй пример с int c – нормальными компиляторами не соберется, т.к. использование неинициализированной локальной переменной определится аналитически.

    Ну и, как обычно, пример сильно PC specific. На платформах, где 0 – вполне валидный адрес для кода, искать такие баги значительно труднее. (Это, например, один из Palm’ов старых такое мог вытворять… а под них ведь тоже игры писали).

  • dDIMA

    2TSS: На PC падение происходит именно в адресе 0×00000000 – точно так, как показано на скриншотах. И пример совсем не высосан из пальца – пост был написан вчера после обсуждения с одним из программистов способов отладки именно такой ситуации (возврат на нулевой адрес после memset(data, 0, …)).
    Если уж быть сверхточным про время падения – то exception генерируется даже не в момент исполнения команды ret, а за несколько тактов до этого, когда конвейер начинает выбирать нужные команды из области памяти. Но поскольку такая ситуация часто является валидной (например, по ветвлению в конце codeseg), exception откладывается до момента непосредственного исполнения инструкции.

  • volodya

    Эти вещи, также, помимо этого блога, более чем подробно рассмотрены в http://www.dumpanalysis.org/blog/index.php/2008/06/26/heuristic-stack-trace-in-windbg-693113/ Если там пропустить пургу, которую активно гонит автор, то оставшиеся 80% блога – ну ооооочень даже ничего.

  • MihaPro

    А у меня чёта не получилось по примеру отладить :( В стеке найденный адрес ведёт в код, вокруг которого BP не срабатывают. Крутил разные настройки компиляции, не помогло.

  • dDIMA

    2 MihaPro. Значит, надо посмотреть следующие адреса в стеке в сторону увеличения. Там ведь могут быть и не относящиеся к сегменту данных значения, например, ссылки на локальные и глобальные переменные и т.п.

  • std.denis

    Кстати, раз уж речь зашла о “просто напихать в код”,
    может кто подскажет, можно ли как-то заставить компилятор вставлять вызовы моих функции пролога и эпилога в каждой функции/методе/конструкторе/деструкторе проекта? Ведь умеет компилятор вставлять код проверки валидности стека, могли бы реализовать и такую плюшку тоже.
    Такую штуку можно сделать добавив в фазу билда препроцессор, который каждый компилируемый сорец сперва обработает и добавит код вызова функций, но такой код смотреть сквозь отладчик будет не очень приятно.

  • dDIMA
  • std.denis

    громадное спасибо, Дима! прям то что доктор прописал