Сообщений 8    Оценка 655 [+1/-0]         Оценить  
Система Orphus

Возможности встроенного отладчика Visual C++

Автор: Александр Шаргин
Источник: RSDN Magazine #0
Опубликовано: 27.01.2002
Исправлено: 13.03.2005
Версия текста: 3.0
Как работает отладчик
Точки останова
Отладочные символы
Настройка отладки
Запуск и прекращение отладки
Подключение к уже запущенному процессу
Just-in-time debugging
Завершение отладки
Точки останова
Позиционные точки останова
Окно Edit->Breakpoints
Пошаговая отладка
Окна отладчика
Окно Variables
Окно Watch
Окно Registers
Окно Memory
Окно Call Stack
Окно Disassembly
Диалоги отладчика
Quick Watch
Exceptions
Threads
Modules
Edit and Continue
Файл autoexp.dat
Секция [AutoExpand]
Секция [ExecutionControl]

Встроенный отладчик Visual C++ – это мощный и удобный инструмент, оказывающий неоценимую помощь в процессе поиска и устранения ошибок в программе. Прежде чем изучать конкретные техники отладки, необходимо ознакомиться с его возможностями.

Как работает отладчик

В отладке принимают участие два процесса – отладчик (debugger) и отлаживаемый (debuggee). Отладчик имеет полный контроль над отлаживаемым процессом. Он может приостанавливать и возобновлять его потоки, читать или изменять память и регистры процессора и т. д. Кроме того, отладчик получает уведомления обо всех важных событиях, которые происходят в отлаживаемом процессе. К таким событиям относятся запуск или завершение новых процессов и потоков, загрузка и выгрузка DLL, исключения, а также вывод отладочной информации посредством функции OutputDebugString. В случае возникновения одного из этих событий отлаживаемый процесс приостанавливается и ждёт, пока отладчик не выполнит необходимые действия и возобновит его работу. Завершение отладчика приводит к завершению отлаживаемого процесса.

Точки останова

Точки останова (breakpoints) играют важную роль в отладке приложений. На самом деле, точка останова – это некоторое условие (например, достижение определённой команды в программе), при выполнении которого возбуждается исключение EXCEPTION_BREAKPOINT (0x80000003). Как мы уже знаем, исключение приводит к тому, что отлаживаемый процесс приостанавливается, а отладчик получает управление. В частности, он может считать текущие значения регистров процессора и ячеек памяти и выдать их на экран для анализа. После этого он может возобновить выполнение отлаживаемого процесса по команде программиста.

Для реализации точек останова в Visual C++ используется специальная инструкция процессора (int 3 на процессорах Intel). Выполнение этой инструкции приводит к исключению EXCEPTION_BREAKPOINT. Установить точку останова можно в любом месте программы. Для этого отладчик записывает по соответствующему адресу инструкцию int 3 (1 байт с кодом 0xCC). Очевидно, что это можно сделать и вручную, вставив в программу инструкцию __asm int 3. Можно также использовать функцию DebugBreak из Win32 API. Ниже мы увидим, для чего применяется этот приём.

В современных процессорах Intel существуют отладочные регистры (DR0-DR7), которые позволяют установить до 4 аппаратных точек останова. Visual C++ использует эти регистры, но когда их количества оказывается недостаточно, их функциональность эмулируется посредством int 3.

С помощью точек останова в отладчике Visual C++ реализовано множество полезных возможностей, например, пошаговая отладка (режим, в котором пользователь может выполнять по одной инструкции отлаживаемой программы за раз).

Отладочные символы

Конечно, возможность читать данные из памяти отлаживаемого процесса полезна. Но для программиста на C++ гораздо удобнее просматривать значения переменных, а не безликих ячеек памяти. Пошаговую отладку также гораздо удобнее выполнять по исходному тексту программы, а не по ассемблерному листингу. Чтобы это стало возможным, применяются отладочные символы (debugging symbols). Эти символы генерируются в процессе построения программы. Существуют различные форматы записи символов. По умолчанию Visual C++ записывает их в отдельный файл с расширением PDB, включая в приложение или DLL абсолютный путь к этому файлу.

PDB-файл содержит информацию, которая позволяет определить адрес любой переменной, функции или строки кода. Возможно и обратное преобразование. Кроме того, в PDB-файл включается информация о типах. Благодаря этому отладчик может не только считать из памяти значение переменной, но и выдать это значение на экран в определённом виде (в зависимости от типа переменной). Пошаговая отладка по исходному тексту также становится возможна (при условии, что у вас есть этот исходный текст, так как он не включается в PDB-файл).

Обратите внимание, что отладочные символы хранятся отдельно для каждого модуля (EXE или DLL). Вполне возможна ситуация, когда отладочные символы доступны для DLL, которую загружает приложение, но не для самого приложения. В этом случае код DLL можно будет отлаживать на уровне исходных текстов, а код приложения – на уровне ассемблера.

Настройка отладки

Для настройки отладки используется закладка Debug в окне Project->Settings. На этой закладке можно выбрать две категории: General и Additional DLLs. Первая категория используется для задания общих параметров отладки (рис. 1).


Рисунок 1. Настройка отладки, категория General

Здесь вы можете задать следующие параметры.

В категории Additional DLLs задаются модули, для которых необходимо принудительно загрузить отладочные символы (рис. 2).


Рисунок 2. Настройка отладки, категория Additional DLLs

Зачем это может понадобиться, спросите вы. Дело в том, что сразу после запуска отлаживаемого процесса отладчик пытается установить в нём заданные вами точки останова. Для этого ему требуются отладочные символы модулей, в которых эти точки устанавливаются Символы exe-файла приложения и подключаемых к нему при запуске DLL отладчик загружает сразу. Но символы DLL, которые будут загружены в программу позже (это касается явно загружаемых DLL, внутрипроцессных COM-серверов и т.п.), по умолчанию не загружаются. Это приведёт к сообщению “One or more breakpoints cannot be set and have been disabled”, когда отладчик попытается установить точки останова в этих DLL, а сами точки будут отключены. Для решения этой проблемы и используется категория Additional DLLs. К этому вопросу мы ещё вернёмся, когда будем обсуждать отладку DLL.

Запуск и прекращение отладки

Чтобы вы могли контролировать выполнение программы и наблюдать её состояние, нужно запустить её под управлением отладчика Visual C++. Для этой цели используются команды из меню Build->Start Debug. Первые три из них (Go, Step Into и Run to Cursor) запускают тот процесс, который указан в настройках отладки.

Подключение к уже запущенному процессу

Команда Attach to Process из меню Debug открывает одноименный диалог, позволяющий подключить отладчик к любому уже запущенному процессу (рис. 3). Например, если написанная вами программа вдруг "зависла", можно тут же подключиться к ней и выяснить причину ошибки. Вы можете даже подключиться к оболочке Windows (explorer.exe), чтобы изучить её работу с помощью отладчика. Правда, в этом случае вам придётся работать с ассемблером, так как Микрософт не поставляет исходные тексты своей оболочки.


Рисунок 3. Окно Attach To Process

Выбрать нужный процесс можно по заголовку его главного окна (колонка Title) или имени его exe-файла (колонка Process), а если этого недостаточно – по уникальному идентификатору процесса (колонка Process Id). Можно отсортировать процессы по любому из этих признаков, щёлкнув по заголовку соответствующего столбца. Обратите внимание на галочку Show System Processes. Если её установить, в список процессов будут включены системные процессы, такие, как сервисы Windows NT.

Just-in-time debugging

В Visual C++ начиная с версии 4.2 появилась возможность отладки "на лету" (Just-in-time debugging). Благодаря этой возможности отладчик можно подключить к программе именно в тот момент, когда она "упала" (например, из-за ошибки доступа). Вот как выглядит окно Application Error в Windows 2000 (рис. 4).


Рисунок 4. Окно Application Error

Если нажать кнопку Cancel, операционная система запустит отладчик, подключит его к сбойному процессу и предоставит вам возможность найти ошибку в момент её возникновения. Кроме этого, в Windows NT/2000 существует ещё один способ подключить отладчик к существующему процессу. Откройте Task Manager, найдите нужный процесс на закладке Processes, подсветите его в списке и выберите из контекстного меню команду Debug.

Just-in-time debugging работает следующим образом. Путь к отладчику запоминается операционной системой в параметре Debugger. В Windows NT/2000 этот параметр хранится в реестре под ключом HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug, а в Windows 9x – в файле win.ini в секции [AeDebug]. Одновременно на машине может быть только один отладчик, зарегистрированный для Just-in-time debugging. Чтобы прописать туда отладчик Visual C++, откройте окно Tools->Options, перейдите на закладку Debug и установите галочку Just-in-Time Debugging.

Завершение отладки

Сеанс отладки автоматически заканчивается, когда завершается отлаживаемая программа. Кроме этого, отладку можно прекратить в любой момент, вызвав команду Stop Debugging из меню Debug или нажав Shift+F5.

ПРИМЕЧАНИЕ

Здесь и далее для команд отладчика приводятся клавиатурные сокращения, выбранные в Visual C++ по умолчанию. При желании их можно изменить на любые другие.

Точки останова

Как я уже говорил, точки останова играют важную роль в процессе отладки приложения. Visual C++ поддерживает множество разновидностей точек останова. Условием для прерывания программы может быть достижение некоторой команды, изменение глобальной переменной, приход заданного сообщения Windows и т. п. Приостановленную программу можно выполнять в пошаговом режиме, анализировать текущие значения переменных и даже вносить в программу изменения прямо "на лету", не прерывая сеанса отладки.

ПРИМЕЧАНИЕ

Самый простой способ приостановить программу – вызвать команду Break из меню Debug (ей соответствует команда Break Execution на панели инструментов). Иногда это бывает удобно. Если, к примеру, ваша программа вошла в бесконечный цикл, можно прервать её и посмотреть, почему это произошло. Однако в большинстве случаев такой способ недостаточно точен, и вместо него используются точки останова.

Позиционные точки останова

В простейшем случае точка останова жёстко привязывается к строке в программе (такие точки называются позиционными). Позиционную точку останова можно установить прямо в редакторе кода. Для этого нужно поместить курсор на нужной строчке и вызвать команду Debug->Insert/Remove Breakpoint (или нажать F9). Для удаления точки останова используется та же команда.

Установка точки останова на конкретную функцию (метод)

Чтобы поставить точку останова на некоторую функцию (метод), нужно ввести её имя в поле ввода Find на стандартной панели инструментов, а затем нажать F9 (способ настолько неочевидный, что поначалу был даже принят за ошибку. – прим. ред.). Кроме того, можно выбрать нужную функцию или метод в окне ClassView, а затем также нажать F9.

Установка точки останова на ассемблерную инструкцию

Такие точки останова устанавливаются в окне Disassembly (о нём мы поговорим немного ниже). Переместитесь к нужному адресу и нажмите F9.

СОВЕТ

Проще всего переместиться к нужному адресу, используя команду Edit->Go To.

Окно Edit->Breakpoints

Окно Breakpoints из меню Edit предоставляет дополнительные возможности для работы с точками останова. В нижней части этого окна находится список уже поставленных точек останова (любую из них можно активизировать, отключить или удалить), а вверху расположены три закладки, предназначенные для установки точек останова различных типов.

Закладка Location


Рисунок 5. Закладка Location

На закладке Location (рис. 5) настраиваются позиционные точки останова. Все рассмотренные выше разновидности точек можно установить и с этой закладки. Местонахождение точки останова указывается в поле Break at в формате:

{имя_функции, имя_исходного_файла, имя_двоичного_модуля} адрес

Здесь "адрес" – это номер строки в программе (ему должен предшествовать символ "@"), адрес ассемблерной инструкции или имя функции (метода). Выражение в фигурных скобках называется контекстом. Можно опускать как отдельные параметры контекста, так и весь контекст целиком. Главное, чтобы при этом не возникало неоднозначности. Если, к примеру, точка останова ставится на строку в программе, необходимо указать файл, в котором эта строка расположена. Иначе возникает неоднозначность.

Для задания местонахождения точки останова можно воспользоваться окном Advanced breakpoint. Чтобы вызвать это окно, щелкните на стрелке справа от поля ввода и выберите пункт Advanced.

В окне Condition можно дополнительно указать условие срабатывания точки останова. Условием может быть любое выражение. Если выражение имеет тип bool, точка останова срабатывает, когда оно истинно; в противном случае она срабатывает при изменении значения выражения.

Бывают случаи, когда точку останова нужно пропустить несколько раз, прежде чем прерывать на ней программу. Специально для этого в окне Condition предусмотрено ещё одно поле, Skip count (в самом низу). С помощью этого поля можно, к примеру, пропустить 10 итераций цикла и прервать программу только на одиннадцатой.

Закладка Data


Рисунок 6. Закладка Data

На закладке Data (рис. 6) устанавливаются точки останова по данным. Их отличие состоит в том, что они могут сработать в любом месте программы, как только изменится (или станет истинным) введённое вами выражение.

Если выражение имеет смысл только в определённом контексте (например, в нём используются локальные переменные какой-либо функции), этот контекст необходимо указать в том же формате, что и для позиционной точки останова. Но здесь уже нужно указать имя функции, а не файла.

Visual C++ позволяет вам контролировать изменение не только отдельных переменных, но и массивов. Рассмотрим следующий фрагмент программы.

int main(int argc, char* argv[])
{
...
    int array[10];

    for(int i=0; i<10; i++)
        array[i] = 0;
...
}

Если теперь задать на закладке Data точку останова в виде '{main,,} array', то программа будет прерываться каждый раз, когда хоть один элемент массива array меняет своё значение (в нашем примере – на каждой итерации цикла).

Можно также контролировать не целый массив, а только некоторую его часть. Для этого на закладке Data имеется поле ввода Enter the number of elements to watch in an array or structure. В этом поле задаётся количество элементов массива, которое нужно контролировать. Отсчёт ведётся от элемента массива, к которому вы обращаетесь в выражении (по индексу или указателю).

Вернёмся к нашему примеру. Чтобы отслеживать изменения в первых трёх элементах массива array, нужно задать для точки останова выражение '{main,,} array[0]' или '{main,,} *array'. Чтобы реагировать на изменения в элементах с третьего по пятый, нужно задать '{main,,} array[2]' или '{main,,} *(array+2)'. И в том, и в другом случае нужно задать количество контролируемых элементов - три.

Если вам потребуется отслеживать изменения в нескольких несвязанных между собой блоках массива, придётся завести по одной точке останова на каждый блок.

Точки останова по данным являются мощнейшим отладочным средством. К сожалению, они не всегда работают надёжно. Так, Visual C++ не может отследить изменение переменной-члена класса, если объект этого класса адресуется указателем. Рассмотрим небольшой пример.

/* 01 */ class A
/* 02 */ {
/* 03 */ public:
/* 04 */     int m_i;
/* 05 */ };
/* 06 */
/* 07 */ int main(int argc, char* argv[])
/* 08 */ {
/* 09 */     A *pa = new A();
/* 10 */     pa->m_i = 1;
/* 11 */     delete pa;
/* 12 */     return 0;
/* 13 */ }

Допустим, нам требуется перехватить изменение переменной pa->m_i в 10 строке программы. Можно предположить, что для этого достаточно установить точку останова по данным на выражение {main,,}pa->m_i. Но, как показывает практика, эта точка никогда не сработает. Чтобы решить подобную проблему, можно прибегнуть к следующему трюку. Сначала нужно поместить переменную в окно Watch (например, перетащив ее мышью) в момент, когда программа прервана внутри функции, где данная переменная доступна. Далее нужно поставить перед именем переменной (в окне Watch) амперсанд, чтобы получить её адрес. Теперь нужно добавить точку останова по данным, указав полученный адрес вместо имени переменной на закладке Data. Чтобы определить необходимый размер контролируемой области памяти, нужно заставить Visual C++ интерпретировать этот адрес как указатель на определенный тип. Следует учесть, что установленная таким образом точка прерывания будет действовать корректно только в течение текущего сеанса отладки. В следующем сеансе отладки такую точку останова придется переустанавливать. Несмотря на сложность и неудобство данного способа, не стоит сбрасывать его со счетов, так как он позволяет находить очень сложные ошибки, например, проходы по памяти.

Закладка Messages


Рисунок 7. Закладка Messages

На закладке Messages (рис. 7) устанавливаются точки останова на сообщения. В верхнем поле указывается имя функции окна, а в нижнем – сообщение, которое должно прийти в эту функцию для срабатывания точки останова. Обратите внимание, что функция окна должна иметь стандартный прототип:

LRESULT WINAPI WndProc(HWND, UINT, WPARAM, LPARAM);

Обычно точки останова на сообщения используются в программах, написанных с использованием "чистого" Win32 API. В программах, использующих MFC, удобнее ставить точки останова в соответствующие обработчики сообщений. Для этого можно использовать методику установки точек останова на функции, описанную выше.

Пошаговая отладка

Когда программа прервана, её можно выполнять в пошаговом режиме. Для этого в Visual C++ предусмотрено несколько команд из меню Debug (таблица 1).

КомандаОписание
Go (F5)Продолжить выполнение программы до следующей точки останова.
Step Into (F11)Выполнить одну инструкцию. Если это вызов функции, точка выполнения перемещается на первую инструкции этой функции (то есть происходит "заход" в функцию).
Step Over (F10)Выполнить одну инструкцию. Если это вызов функции, то она выполняется целиком (то есть "захода" в функцию не происходит).
Step Out (Shift+F11)Выполнять программу до возврата из текущей функции.
Run to Cursor (Ctrl+F10)Выполнять программу до инструкции, на которой находится курсор ввода. Эта команда эквивалентна установке временной точки останова с последующим вызовом команды Go.
Step Into Specific FunctionЭта команда аналогична Step Into, но позволяет явно указать, в какую функцию зайти (для этого надо установить на неё курсор ввода). Полезна, если на одной строке выполняется несколько вызовов функции, например: f1(f2(),f3(0));.
Таблица 1. Команды пошагового режима отладки

Иногда в процессе отладки возникает необходимость перенести точку выполнения. Например, вы заметили ошибку и хотите "перескочить" через неё или, наоборот, хотите вернуться немного назад и выполнить фрагмент программы ещё раз. Чтобы это сделать, установите курсор в нужном месте и выберите команду Set Next Statement из контекстного меню (или нажмите Ctrl+Shift+F10).

Окна отладчика

В процессе пошаговой отладки программист может использовать целый ряд окон отладчика для наблюдения за состоянием программы. Каждому окну соответствует кнопка на панели инструментов Debug, с помощью которой можно показывать и прятать соответствующее окно. Можно также показывать и скрывать отладочные окна, используя команды из меню View->Debug Windows. Назначение отладочных окон описано в следующих разделах.

Окно Variables

В окне Variables (Alt+4) автоматически отображаются значения локальных переменных (закладка Locals) и переменных-членов класса, адресуемого указателем this (закладка This). В нём же отображаются значения всех переменных, которые используются в предыдущей и текущей инструкциях программы (закладка Auto). На закладке Auto также показываются возвращаемые значения функций.

ПРИМЕЧАНИЕ

Чтобы отладчик показывал возвращаемые значения функций, необходимо открыть окно Tools->Options и установить галочку Return value на закладке Debug.

Чтобы изменить значение переменной, показанной в окне Variables, нужно просто сделать двойной щелчок на старом значении и ввести новое. Чтобы узнать тип переменной, нужно щёлкнуть по ней правой кнопкой и выбрать Properties из всплывающего меню.

Выпадающий список Context позволяет просматривать локальные переменные любой из вызванных в данный момент функций. Код выбранной в нём функции автоматически отображается в окне редактора.

Если в вашей программе используются строки в формате UNICODE, но их содержимое не отображается в окне Variables, откройте окно Tools->Options и установите флажок Display unicode strings на закладке Debug.

Окно Watch

Окно Watch (Alt+3) позволяет просматривать значения любых переменных и выражений. Их можно размещать на любой из четырёх закладок (Watch 1, Watch 2, Watch 3 или Watch 4). Добавить переменную или выражение в окно Watch можно одним из следующих способов:

Чтобы изменить значение переменной, так же, как и в окне Variables, достаточно сделать двойной щелчок на старом значении и ввести новое. Значение выражений изменять нельзя. Чтобы узнать тип переменной или выражения, нужно щёлкнуть по ним правой кнопкой и выбрать Properties из всплывающего меню.

Регистры и псевдорегистры

В окне Watch можно наблюдать любые регистры процессора и изменять их значения. Можно также использовать регистры в выражениях. Кроме стандартного набора регистров процессора Intel отладчик может отображать несколько псевдорегистров. Полный список регистров и псевдорегистров, которые можно использовать в окне Watch, приведён в таблице 2.

РегистрыОписание
ERRКод последней ошибки, который возвращает функция GetLastError.
TIBИнформационный блок текущего потока.
CLKРегистр часов (недокументированный). Может использоваться для измерения времени выполнения инструкций в программе.
EAX, EBX, ECX, EDX, ESI, EDI, EIP, ESP, EBP, EFLРегистры процессора Intel.
CS, DS, ES, SS, FS, GSСегментные регистры процессора Intel.
ST0, ST1, ST2, ST3, ST4, ST5, ST6, ST7Регистры чисел с плавающей точкой процессора Intel.
MM0, MM1, MM2, MM3, MM4, MM5, MM6, MM7Регистры из набора MMX.
Таблица 2. Регистры и псевдорегистры

Символы форматирования

Можно указать отладчику, в каком формате выводить значение переменной/выражения, используя символы форматирования. Эти символы добавляются к переменной или выражению через запятую. Большинство из них совпадает с символами форматирования функции printf: d – целое число со знаком, u – беззнаковое целое, f – число с плавающей точкой, c – символ, s – строка и т. д. Полный список символов форматирования приведён в таблице 3.

СимволОписание
d, iцелое со знаком
Uбеззнаковое целое
Oбеззнаковое восьмеричное целое
x, Xбеззнаковое шестнадцатеричное целое
l, h"длинное" или "короткое" целое (префиксы, используемые совместно с символами d, i, u, o, x, X)
Fвещественное число со знаком
Eвещественное число со знаком в научной нотации
Gвещественное число со знаком (нотация выбирается автоматически)
Ccимвол
Sстрока в кодировке ANSI
Suстрока в кодировке Unicode
Stстрока в кодировке ANSI или Unicode
HrHRESULT или код ошибки Win32
Wcфлаг класса окна
Wmкод сообщения
<число>количество элементов массива
Таблица 3. Символы форматирования

Назначение большинства символов вопросов не вызывает, но последние четыре следует рассмотреть подробнее.

Символы wm превращают код сообщения в его название, например:

0x01,wm = WM_CREATE

Символы wc позволяют "расшифровать" стиль окна, например:

0x6840000,wc = WS_OVERLAPPEDWINDOW WS_CLIPSIBLINGS WS_CLIPCHILDREN

Символы hr переводят коды ошибок Win32 и значения HRESULT, возвращаемые функциями COM, в удобочитаемый вид, например:

0x02,hr = 0x00000002 Системе не удается найти указанный файл.

Символы hr могут оказаться полезными во многих случаях. Например, можно применить их к псевдорегистру ERR, чтобы всегда иметь перед глазами описание последней ошибки при вызове функций Windows API. Можно также добавить в окно Watch строчку "EAX,hr". Поскольку COM-функции возвращают свой HRESULT через регистр EAX, вы сможете постоянно видеть описания ошибок COM, возникающих в отлаживаемой программе.


Рисунок 8. Использование символов форматирования wm, wc и hr

Наконец, число, задаваемое через запятую после указателя (рис. 9), позволяет просмотреть заданное количество элементов массива, адресуемого этим указателем (по умолчанию показывается всего один элемент). Допустим, мы выделили динамический массив из 10 целых чисел:

int *pInt = new int[10];

Чтобы просмотреть его содержимое в окне Watch, нужно ввести:

pInt,10


Рисунок 9. Просмотр массива в окне Watch

Окно Registers

Окно Registers (Alt+5) позволяет просматривать и изменять значения регистров процессора. В нём также отображаются все флаги процессора и содержимое стека. Любое значение в окне можно изменить. Для этого нужно переместить курсор ввода в нужное место и просто ввести новое значение поверх старого.

Команда Floating Point Registers из контекстного меню позволяет включить или выключить отображение регистров с плавающей точкой.

Окно Memory

Окно Memory (Alt+6) позволяет просматривать и изменять содержимое ячеек памяти. Содержимое памяти может отображаться в самых различных форматах. Из контекстного меню можно выбрать Byte Format (отдельные байты), Short Hex Format (слова в шестнадцатеричном виде) и Long Hex Format (двойные слова в шестнадцатеричном виде). Полный список форматов для окна Memory доступен в окне Tools->Options на закладке Debug (выпадающий список Format).

Значение любой ячейки памяти можно изменить. Для этого следует переместить курсор ввода в нужное место и просто ввести новое значение поверх старого. Кроме того, вы можете перетащить в окно Memory любую переменную, и в нём отобразится участок памяти, в котором хранится значение этой переменной.

Окно Call Stack

Окно Call Stack (Alt+7) показывает последовательность вызванных функций (стек вызовов). Используя контекстное меню, можно отобразить также типы (команда Parameter Types) и значения (команда Parameter Values) параметров этих функций. К тексту любой функции можно переместиться, сделав двойной щелчок на её имени. Кроме того, точки останова можно ставить прямо в этом окне.

Окно Disassembly

В окне Disassembly (Alt+8) отображается текст отлаживаемой программы на языке ассемблера. Иногда без помощи этого окна ошибку в программе найти не удаётся. Точки останова можно ставить прямо в этом окне. При этом обеспечивается позиционирование с точностью до команды процессора (в окне редактора кода такой точности достичь не удаётся).

Если вы захотите посмотреть, как работают функции Windows API, вам также не обойтись без окна Assembly. В нём вы сможете заходить в любую функцию Win32 и выполнять её по шагам.

ПРИМЕЧАНИЕ

При использовании Windows 9x попытка зайти в функцию WIn32 API приведет к появлению сообщения "Can not trace into system DLLs". В этой операционной системе заходить в функции API можно, только используя отладчик уровня ядра (например, WinDBG или SoftIce).

Если вы не видите названий переменных и функций в окне дизассемблера, следует установить отладочные символы для библиотек вашей операционной системы. Чтобы установить отладочные символы для Windows NT4, можно использовать программу Windows NT Symbols Setup, которая входит в Visual Studio. Символы для Windows 2000 можно установить с диска Customer Support Diagnostics или скачать с сайта Microsoft по адресу http://www.microsoft.com/ddk/debugging/symbols.asp.

СОВЕТ

Если отладочные символы для системных DLL вам недоступны, установите галочку Load COFF & Exports на закладке Debug окна Tools->Options. Это позволит увидеть в отладчике, по крайней мере, имена функций, экспортируемых из системных DLL. По умолчанию эта галочка отключена из соображений повышения производительности.

Диалоги отладчика

Диалоги отладчика предоставляют ряд дополнительных возможностей. Все они вызываются из меню Debug.

Quick Watch

Этот диалог имеет возможности, аналогичные возможностям окна Watch, с той разницей, что в нём можно просматривать только одну переменную за раз. Используется, когда вам не хочется добавлять переменную в окно Watch. Его можно вызвать, используя комбинацию Shift+F9.

Exceptions

Этот диалог позволяет настроить реакцию отладчика на возникновение системных и пользовательских исключений. Для каждого исключения можно выбрать действие Stop always (останавливаться всегда) или Stop if not handled (останавливаться, если исключение не обработано).

Threads

Этот диалог показывает список потоков, созданных вашей программой. Позволяет приостановить (suspend) или продолжить (resume) любой поток, а также установить на него фокус (то есть сделать его текущим). Этот диалог – практически единственное средство для отладки многопоточных приложений, которое предоставляет Visual C++.

Modules

Этот диалог показывает список загруженных модулей. Для каждого модуля выводится диапазон адресов и имя файла.

Edit and Continue

Функция Edit and Continue впервые появилась в Visual C++ 6.0. С её помощью вы можете вносить изменения в код программы и перестраивать её, не прерывая сеанса отладки.

Для этого достаточно вызвать команду Apply code changes из меню Debug (или нажать Alt+F10), после того как вы подправили исходные тексты. Более того, Visual C++ может вызывать для вас эту команду автоматически. Это будет происходить, если в окне Tools->Options на закладке Debug установить флаг Debug commands invoke Edit and Continue.

Файл autoexp.dat

Файл autoexp.dat находится в каталоге %Visual Studio Folder%\Common\MSDev98\Bin (%Visual Studio Folder% - каталог, в которой установлен пакет Visual Studio). С помощью этого файла можно выполнять некоторые тонкие настройки отладчика. Изменения вступают в силу после перезапуска среды Visual C++.

ПРЕДУПРЕЖДЕНИЕ

Большая часть информации, относящейся к файлу autoexp.dat, не документирована. Используйте её на свой страх и риск.

По формату autoexp.dat – это обычный ini-файл с несколькими секциями. Рассмотрим их более подробно.

Секция [AutoExpand]

Вы, вероятно, заметили, что отладчик "умеет" распознавать стандартные структуры данных (CString, RECT и т. п.) и показывать их содержимое в окнах Variables и Watch, а также во всплывающих подсказках. Оказывается, можно не только изменить представление этих структур отладчиком, но и определить представление для собственных структур. Именно для этого и используется секция [AutoExpand]. Каждая строка в ней описывает одну структуру (или класс). Описание имеет следующий формат:

имя_типа = текст<имя_поля[,ключ_форматирования]>...

Имя_типа – это имя класса или структуры. Оно может быть шаблоном. Параметры шаблона можно указать явно или использовать символ "*" для обозначения всех возможных параметров. Текст может быть произвольным. Чтобы вставить в него значение поля структуры, имя этого поля записывается в угловых скобках (возможно, с символом форматирования). Список возможных символов совпадает со списком символов для окна Watch (таблица 3) за одним исключением: задание числа элементов массива действовать не будет.

Вот как может выглядеть описание для структуры tagMSG, содержащей сообщение Windows.

tagMSG=msg=<message,wm> wp=<wParam,x> lp=<lParam,x>

Кроме задания переменных-членов класса, можно вызывать и его методы. Благодаря этому можно написать для своих классов специальный метод форматирования, доступный только в отладочной версии приложения, и использовать его для представления данных в классе. Практика показывает, что этот метод нельзя реализовывать внутри класса. Это сбивает с толку вычислитель выражений Visual C++ (expression evaluator), и вместо вашей строки выводятся вопросительные знаки. Вот пример форматирующего метода.

struct MyRect : public RECT
{
#ifdef _DEBUG
    const char *Format() const;
#endif
};

#ifdef _DEBUG
const char *MyRect::Format() const
{
    static char buf[255];
    sprintf(buf, "width=%d; height=%d", right-left, bottom-top);
    return buf;
}
#endif

Соответственно, в файле autoexp.dat следует написать:

MyRect =<Format(), s>

Другой пример применения этой возможности – отображение строк STL.

std::basic_string<*>=<c_str(), s>
ПРЕДУПРЕЖДЕНИЕ

Включение в программу методов, вызываемых отладчиком, может привести к непредсказуемым побочным эффектам. Категорически не рекомендуется писать что-либо в память или вызывать функции Win32 API из таких методов.

Директивы $BUILTIN и $ADDIN

Если ограничения, накладываемые на форматирующие методы, показались вам слишком жёсткими, спешу вас обрадовать: в вашем распоряжении есть полноценная альтернатива. Использовать её сложнее, но гораздо безопаснее. Дело в том, что вычислитель выражений позволяет использовать для отображения структур встроенные или внешние процедуры. Встроенные процедуры подключаются при помощи директивы $BUILTIN, а внешние - с помощью директивы $ADDIN.

Директива $BUILTIN вряд ли вам когда-нибудь пригодится. Встроенные функции вычислителя существуют для таких типов, как GUID и VARIANT, но они и так подключены к соответствующим структурам в файле autoexp.dat:

tagVARIANT=$BUILTIN(VARIANT)
VARIANT=$BUILTIN(VARIANT)
_GUID=$BUILTIN(GUID)

Зато директива $ADDIN может оказаться весьма полезной. Она принимает два параметра: имя DLL и имя функции, реализующей форматирование. DLL должна находиться в том же каталоге, что и файл autoexp.dat. Форматирующая функция имеет следующий прототип.

HRESULT WINAPI AddIn(
    DWORD dwAddress,
    DEBUGHELPER *pHelper,
    int nBase,
    BOOL bUniStrings,
    char *pResult,
    size_t max,
    DWORD reserved
);

Параметры функции имеют следующий смысл.

Чтобы получить данные объекта, для которого вызывается add-in, нужно воспользоваться функциями из структуры DEBUGHELPER:

typedef struct tagDEBUGHELPER
{
    DWORD dwVersion;
    BOOL (WINAPI *ReadDebuggeeMemory)(
        struct tagDEBUGHELPER *pThis,
        DWORD dwAddr,
        DWORD nWant,
        VOID* pWhere,
        DWORD *nGot
    );
    // from here only when dwVersion >= 0x20000
    DWORDLONG (WINAPI *GetRealAddress)(struct tagDEBUGHELPER *pThis);
    BOOL (WINAPI *ReadDebuggeeMemoryEx)(
        struct tagDEBUGHELPER *pThis,
        DWORDLONG qwAddr,
        DWORD nWant,
        VOID* pWhere,
        DWORD *nGot
    );
    int (WINAPI *GetProcessorType)(struct tagDEBUGHELPER *pThis);
} DEBUGHELPER;

Из этих функций в Visual C++ 6.0 можно использовать только первую – ReadDebuggeeMemory. Остальные доступны в Visual C++ 7.0. Описание всех функций приведено в таблице 4. Назначение параметров должно быть понятно из их названий.

ФункцияОписание
ReadDebuggeeMemoryЧитает данные из памяти отлаживаемого процесса.
ReadDebuggeeMemoryExЧитает данные из памяти отлаживаемого процесса. В отличие от ReadDebuggeeMemory, работает с 8-байтными адресами.
GetRealAddressВозвращает реальный 8-байтный адрес объекта, для которого вызван add-in, для последующей передачи в функцию ReadDebuggeeMemoryEx.
GetProcessorTypeВозвращает тип процессора.
Таблица 4. Функции, которые может использовать add-in

Этой информации вполне достаточно, чтобы написать add-in. Для примера реализуем в нём ту же функциональность, которую выполняла функция MyRect::Format. Код DLL выглядит так.

#include <windows.h>
#include <stdio.h>

typedef struct tagDEBUGHELPER
{
    DWORD dwVersion;
    BOOL (WINAPI *ReadDebuggeeMemory)(
        struct tagDEBUGHELPER *pThis,
        DWORD dwAddr,
        DWORD nWant,
        VOID* pWhere,
        DWORD *nGot
    );
    // from here only when dwVersion >= 0x20000
    DWORDLONG (WINAPI *GetRealAddress)(struct tagDEBUGHELPER *pThis);
    BOOL (WINAPI *ReadDebuggeeMemoryEx)(
        struct tagDEBUGHELPER *pThis,
        DWORDLONG qwAddr,
        DWORD nWant,
        VOID* pWhere,
        DWORD *nGot
    );
    int (WINAPI *GetProcessorType)(struct tagDEBUGHELPER *pThis);
} DEBUGHELPER;

extern "C" __declspec(dllexport) 
HRESULT WINAPI MyRectFormat(
    DWORD dwAddress,
    DEBUGHELPER *pHelper,
    int nBase,
    BOOL bUniStrings,
    char *pResult,
    size_t max,
    DWORD reserved
)
{
    RECT rc;
    DWORD dwGot;
    pHelper->ReadDebuggeeMemory(
        pHelper, dwAddress, sizeof(rc), &rc, &dwGot);

    if(dwGot != sizeof(rc))
        return E_FAIL;

    sprintf(
        pResult,
        "width=%d; height=%d",
        rc.right-rc.left, rc.bottom-rc.top
    );

    return S_OK;
}

BOOL APIENTRY DllMain(HANDLE, DWORD, LPVOID)
{
    return TRUE;
}

Чтобы избавиться от искажения имён, используется DEF-файл:

LIBRARY MyRectFormat
EXPORTS

    MyRectFormat

Наконец, полученную DLL необходимо скопировать в каталог с файлом autoexp.dat и добавить в него строчку:

tagRECT=$ADDIN(MyRectFormat.dll,MyRectFormat)

Секция [ExecutionControl]

В начале статьи мы рассмотрели команду Step In, которая позволяет "заходить" в функции. Но в некоторых случаях "заход" в функции нежелателен. В этом случае можно отключить его для определённых функций или классов с помощью опции NoStepInto. Её следует применять в секции [ExecutionControl] файла autoexp.dat. Если эта секция отсутствует, ее всегда можно добавить вручную.

В общем виде использование NoStepInto выглядит так:

[пространство_имён_или_класс::]имя_функции=NoStepInto

В качестве имени функции можно использовать символ "*" для обозначения всех функций в данном пространстве имён или классе. Зачем всё это нужно, спросите вы. Рассмотрим следующий пример.

extern CString strWndName;
extern CString strClassName;

CWnd wnd;
wnd.Create(strWndName, strClassName, 0, CRect(0, 0, 20, 20), NULL, 0);

Если вызвать команду Step Into на строчке wnd.Create(...);, то мы попадём сначала в конструктор CRect::CRect, затем два раза в оператор приведения CString::operator LPCTSTR и только потом в CWnd::Create. Поскольку ни конструктор, ни оператор приведения не делают ничего интересного, можно отключить "заход" в эти функции, чтобы они не мешали нам отлаживать программу. Для этого следует добавить в файл autoexp.dat строчки:

[ExecutionControl]
CRect::CRect=NoStepInto
CString::*=NoStepInto

Единственная неприятность состоит в том, что эта методика не работает с шаблонами. Возможно, именно из-за этого опция NoStepInto не была документирована.


Эта статья опубликована в журнале RSDN Magazine #0. Информацию о журнале можно найти здесь
    Сообщений 8    Оценка 655 [+1/-0]         Оценить