Окна Memory и Disassembly
Окна Memory и Disassembly имеют симбиозные отношения. Пытаясь определить, что делает последовательность операций языка ассемблера в окне Disassembly, надо держать окно Memory открытым, чтобы можно было видеть и адреса, и значения. Инструкции языка ассемблера работают в памяти, а память воздействует на выполнение этих инструкций. Окна Disassembly и Memory вместе позволяют наблюдать динамику этих взаимоотношений. Само по себе, окно Memory — просто море чисел, особенно когда происходит аварийный отказ. Однако, комбинируя эти два окна, можно начать вычисления, связанные некоторыми неприятными проблемами аварийных отказов. Совместное использование этих окон наиболее важно при отладке оптимизированного кода, когда прохождение стека отладчика затруднено. Чтобы разрешить аварийную ситуацию, необходимо пройти стек вручную. На первом этапе прохождения стека нужно знать, по каким адресам памяти загружены ваши двоичные файлы. В отладчике Visual C++ 6 добавлено диалоговое окно Module List, отображающее все двоичные файлы, загруженные вашей программой. Оно показывает также имя модуля, путь к модулю в дереве каталогов, порядок загрузки и, самое важное, диапазон адресов загрузки модуля. Поскольку это окно является модальным, лучше записать имена модулей и их загрузочные адреса, потому что эта информация понадобится в будущем неоднократно. Сравнивая элементы стека со списком диапазонов адресов, можно получить некоторое представление о том, какие элементы адресованы в ваших модулях.
Просмотрев диапазоны адресов загрузки, откройте окна Memory и Disassembly. В окне Memory введите в поле Address регистр стека ESP и покажите значения в формате двойного слова, щелкая правой кнопкой мыши в пределах окна и выбирая команду Long Hex Format в контекстном меню. Используя либо свой список адресов загрузки, либо диалоговое окно Module List, начните просмотр чисел в окне Memory слева направо и сверху вниз.
Для проверки числа на принадлежность одному из ваших загруженных модулей, выберите его и перетащите в окно Disassembly.
В нем будут отображены инструкции языка ассемблера по этому адресу, а если в приложение включена полная отладочная информация, то вы сможете увидеть вызы вающую функцию.
Если регистр ESP не содержит ничего, что напоминало бы адрес модуля, выведите дамп регистра ЕВР в окно Memory и выполните те же действия. Освоившись с языком ассемблера, вы сможете просматривать дизассемблерный код, окружающий адрес аварийного останова. Изучение "криминальной" аварийной ситуации позволит вам понять, где бы мог быть расположен адрес возврата — в ESP или в ЕВР.
Поговорим немного сначала о достоинствах, а затем и о недостатках окна Memory. Во-первых, окно Memory — единственное место, где можно просматривать символьные строки, превышающие 255 знаков. Кроме того, окно Memory позволяет просматривать любую переменную или участок памяти.
Теперь о недостатках. Первый состоит в том, что одновременно с отладчиком Visual C++ можно просматривать только один участок памяти, и это ограничение обойти нельзя. Во-вторых, отображаемые данные, которые вы просматриваете, могут резко перемещаться по экрану. Это случается главным образом при изменении формата памяти. Оказалось, что лучше всего выполнять правый щелчок мыши только на адресе, который необходимо видеть (в окне Memory) — и нигде больше. Очевидно, окно Memory запоминает то места, где оно показывает текущую строку адреса. Например, если строка текущего адреса — десятая от вершины окна, а пользователь вводит новый адрес в поле Address, то новый адрес будет отображен в десятой строке. Если при выполнении правого щелчка мыши в окне изменяется формат памяти, окно Memory перемещает строку текущего адреса к позиции правого щелчка, и результат может быть непредсказуемым.
История отладочной войны
Что может быть не так в функции GlobalUnlock?
Ведь она просто разыменовывает указатель.
Сражение
Отладка чрезвычайно неприятной аварийной ситуации, грозившей сорвать выпуск продукта, длилась почти месяц. Дублировать аварию не удавалось, и разработчики понятия не имели, в чем ее причина.
Единственным ключом к разгадке было то, что аварийный сбой случался только после открытия диалогового окна Print и изменения некоторых ее установок. Аварийный останов происходил немного позже (после закрытия этого окна) в элементах управления независимых поставщиков. Аварийный стек вызовов указывал, что авария произошла в середине функции GlobalUnlock.
Результат
Во-первых, не было уверенности, что кто-то еще использует функции дескрип-торной памяти (handle-based memory) GlobalAlloc, GlobalLock, GlobalFree и
GlobalUnlock) в \Л/1п32-програм|»ировании. Однако после просмотра в коде дизассемблера сторонних управляющих элементов стало понятно, что их автор, очевидно, перенес их из 16-разрядной кодовой базы. Первая гипотеза состояла в том, что эти элементы неправильно взаимодействовали с API-функциями де-скрипторной памяти.
Для проверки этой гипотезы были установлены несколько точек прерывания на функциях GlobalAlloc, GlobalLock и GlobalUnlock, чтобы получить возможность найти те места в элементах управления, где выполнялось выделение и манипуляции с памятью. В результате установки контрольных точек в сторонних подпрограммах удалось наблюдать, как они использовали дескрипторную память. Все казалось нормальным, пока не было начато их пошаговое выполнение, чтобы дублировать аварийную ситуацию.
В некоторой точке после закрытия диалогового окна Print, мы заметили, что стартовавшая функция GlobalAlloc возвращала значения дескриптора, которые завершались нечетными цифрами, например, 5. Поскольку дескрипторная память в Win32 нуждается в разыменовании указателя, чтобы преобразовать дескриптор в значения памяти, я сразу же понял, что наткнулся на критическую ситуацию. Каждое распределение памяти в Win32 должно заканчиваться шест-надцатеричными цифрами 0, 4, 8 или С, потому что все указатели должны быть выровнены на двойное слово. Значения же дескрипторов, выходящие из GlobalAlloc, были явно (и довольно значительно) искажены.
Вооруженный этой информацией, менеджер проекта был готов потребовать исходный код от поставщика элемента управления, потому что был уверен, что причиной аварии и задержки выпуска был этот элемент.
Однако мы убедили его, что найденное ничего не доказывает и что необходимо продолжить проверку использования памяти. Оказалось, что элемент управления "не виноват", и новой гипотезой стало предположение, что реальную проблему содержало само приложение. Авария же в стороннем элементе управления была просто совпадением.
Проверка исходного кода показала, что приложение было полным 32-разрядным Windows-приложением и ничего не делало с дескрипторной памятью. Тогда мы проверили функцию печати — ее код выглядел безупречно.
Было решено сузить область дублирования аварии. После нескольких прогонов оказалось, что для аварийного останова нужно было лишь открыть диалоговое окно Print и изменить ориентацию печатной страницы, а после закрытия окна Print надо было просто повторно открыть его. Аварийный останов происходил вскоре после второго закрытия окна. Ориентация страницы, вероятно, просто
изменяла байт где-то в памяти, что и служило причиной аварии.
Хотя при начальном чтении код выглядел безупречно, я просматривал каждую строку в обратном порядке и перепроверял ее по документации MSDN. Через 10 минут ошибка была найдена. Команда сохраняла структуру данных PRINTDLG, используемую для инициализации диалогового окна Print, с помощью API-функции PrintDlg. Третье поле в этой структуре — hDevMode — является значением дескрипторной памяти, которую выделяет диалоговое окно Print. Ошибка состояла в том, что разработчики использовали это значение памяти как регулярный указатель и должным образом не разыменовывали дескриптор или вызывали функцию GlobalLock для дескриптора. Изменяя значения в структуре DEVMODE, на самом деле они выполняли запись в глобальную таблицу дескрипторов процесса. Эта таблица является участком памяти, в котором хранятся все распределения динамической памяти дескрипторов. При случайной записи в глобальную таблицу дескрипторов обращение к GlobalAlloc использует неправильные смещения, а значения, вычисленные по такой таблице, приводили к тому, что функция GlobalAlloc возвращала некорректные указатели.
Уроки
Урок первый — нужно тщательно читать документацию. Если в документации говорится, что структура данных есть "перемещаемый глобальный объект памяти", то память предназначена для дескрипторов, и необходимо должным образом разыменовывать этот дескриптор памяти или использовать на нем функцию GlobalLock. Хотя Windows 3.1 сильно устарела, некоторые 16-разрядные компоненты все еще входят в Win32 API, и следует обращать на них внимание.
Урок второй — глобальная таблица дескрипторов хранится в перезаписываемой памяти. Лично я считаю, что такая важная структура операционной системы должна храниться в памяти "только-для-чтения". Почему Microsoft не защитил эту память? Могу предположить, что дело в следующем. Технически память дескрипторов используется только для обратной совместимости, и 32-разрядные Windows-приложения должны бы были использовать специфические для Win32 типы памяти. Защита глобальной таблицы дескрипторов потребовала бы двух переключений контекста (из режима пользователя в режим ядра) на каждом вызове функции дескрипторной памяти. Поскольку эти контекстные переключения очень дороги (в смысле времени их обработки), можно понять, почему Microsoft не защитил глобальную таблицу дескрипторов от записи.
Урок последний — мы потратили слишком много времени, сосредоточившись на стороннем элементе управления. Всего мне потребовалось около семи часов, чтобы найти ошибку. Однако тот факт, что ошибка могла дублироваться только при открытии диалогового окна Print, которая пришла из кода приложения, должен был предупредить меня, что проблема была "ближе к дому" (а не где-то на стороне).