Точки прерывания на системных или экспортируемых функциях
Размещение точки прерывания на первой инструкции функции — очень мощная техника. Однако попытка установить точку прерывания на функции, которую программа импортирует из DLL, ни к чему не приведет. Со стороны отладчика здесь все в порядке, нужно только дать ему некоторую контекстную информацию о том, где он может найти функцию. Кроме того, важна еще одна небольшая деталь: имя функции зависит от того, загружены ли отладочные символы DLL. Имейте в виду, что точку прерывания на системных DLL-функциях можно устанавливать только в Microsoft Windows 2000. Недостаток защиты для "копирования-при-записи" (обсуждавшийся в главе 4) и есть та причина, по которой нельзя устанавливать точки прерывания на системных функциях Windows 98, которые загружены выше 2 Гбайтной границы памяти. Чтобы заставить эту технику работать в Windows 2000, нужно использовать формат COFF (см. главу 4) и включить в отладчике загрузку экспорта. Для этого, работая в IDE Visual C++, убедитесь, что на вкладке Debug диалогового окна Options (открываемого командой Tools|Options) установлен флажок Load COFF & Exports.
Чтобы показать, как устанавливаются точки прерывания на системной DLL, установим такую точку на функцию LoadLibrary из KERNEL32.DLL. Поскольку уже известно, как нужно устанавливать контекст для позиционной точки прерывания, то ясно, что первая часть контекста записывается как {,,KERNEL32.DLL} и идентифицирует модуль функции. Отладчик Visual C++ придерживается иерархического подхода к символической информации, при котором более полные наборы символов имеют приоритет над менее полными. Так, файлы программных баз данных (PDB), которые включают всю возможную информацию — номера исходных строк, имена функций, переменных и типов1, всегда имеют приоритет над COFF/DBG-файлами, которые содержат только символические имена общих (public) функций2. COFF/DBG-файлы имеют приоритет над экспортируемыми именами, которые являются разновидностью псевдосимволов. Для того чтобы подтвердить, что отладчик загружает символы для DLL, нужно контролировать вкладку Debug окна Output.
Если в окне Output выводится сообщение "Loaded symbols for 'DLL name" (Загружены символы для 'имя DLL'), значит имеются полные символы для этой DLL. Наоборот, если выводится "Loaded 'DLL name, no matching symbolic information found" (Загружена 'имя DLL', соответствующая символьная информация не найдена) или "Loaded exports for 'DLL name " (Загружены эксперты для 'имя DLL"), значит символы не были загружены.
Здесь обсуждается тема отладочных символов, поэтому вспомним, что отладочные символы Windows 2000 нужно всегда устанавливать. Хотя они и не помогут реализовать полную обратную разработку операционной системы, потому что содержат символические имена только для общих (public) компонентов. Но если символы загружены, то вы, по крайней мере, будете видеть, в каких функциях вы находитесь при просмотре стека или окна Disassembly. Причем запомните, нужно обновлять символы операционной системы каждый раз, когда устанавливается очередной пакет обслуживания (Service Pack) операционной системы. Отладочные символы для Windows 2000 находятся на компакт-диске Customer Support Diagnostics (Диагностика поддержки клиента). Visual Studio для Windows NT 4 включает программу Windows NT Symbols Setup, которая устанавливает соответствующие символы.
Это и есть полный набор символов. — Пер.
И поэтому содержат неполный набор символов. — Пер.
Окно Output открывает в нижней части окна Microsoft Visual C++ команда VievjOutput или <Alt>+<2>. - Пер.
Если отладочные символы не загружены, то в качестве строки для установки точки прерывания нужно использовать имя, экспортированное из DLL. Можно проверить это имя, запустив утилиту DUMPBIN с ключом /EXPORTS:
DOMPBIN /EXPORTS DLL-Name
Если вы выполните DUMPBIN для KERNEL32.DLL, то увидите не функцию LoadLibrary, а две функции с похожими именами — LoadLibraryA
и LoadLibraryw. Суффиксы указывают на набор применяемых данной функцией символов: суффикс А происходит от ANSI (American National Standards Institute), a w — от wide (или Unicode).
Windows 2000 использует кодировку Unicode для интернационализации. Если программа компилировалась с параметром UNICODE, то нужно выбрать версию LoadLibraryw, а если нет, то LoadLibraryA. Однако функция LoadLibraryA это просто оболочка, которая распределяет память для конвертирования ANSI-строки в Unicode и вызывает LoadLibraryw, поэтому технически можно использовать также и LoadLibraryw. Если наверняка известно, что программа собирается вызывать только одну из этих функций, то можно просто установить точку прерывания на этой функции. Если такой уверенности нет, установите точки прерывания сразу на двух функциях. Если символы не загружены, то синтаксис точки прерывания для прерывания на функции LoadLibrary выглядит либо так: (,,KERNEL32.DLL}LoadLibraryA, либо так: {,,KERNEL32.DLL)LoadLibraryw.
Если приложение ориентировано только на Windows 2000, то нужно везде использовать Unicode. Это обеспечит значительное повышение производительности. Мэтт Пьетрек в декабрьской (за 1997 год) колонке "Under the Hood" в Microsoft Systems Journal сообщил, что ANSI-оболочки обеспечивают значительный рост производительности. Кроме того, поддержка Unicode облегчает интернационализацию программы.
Если символы загружены, то необходимо выполнить некоторые вычисления, чтобы согласовать декорированные имена символов. Для этого нужно только знать соглашение о вызове экспортируемой функции и прототип функции. Подробности соглашений о вызовах приведены в главе 6. Ниже показан прототип функции LoadLibrary из WINBASE.H с некоторыми макросами, расширенными для ясности:
_declspec (dllimport)
HMODULE
_stdcall
LoadLibraryA(
LPCSTR IpLibFileName
);
Макрос WINBASEAPI расширяется в стандартное соглашение о вызовах _stdcall, которое, между прочим, является соглашением о вызовах для всех системных API-функций. Стандартные вызовы функций декорируются префиксом с символом подчеркивания и суффиксом с символом "@", за которым следует число байт, помещенных в стек.
К счастью, вычислить это число довольно просто: оно равно сумме байт, отводимых в стеке для всех параметров. Для семейства CPU Intel Pentium нужно просто сосчитать число параметров и умножить его на 4. Для функции LoadLibrary, которая имеет один параметр, окончательное декорированное имя выгладит так: _LoadLibraryA@4. Вот еще два примера, которые показывают, как должны выглядеть окончательные декорированные имена функций:
- _createProcessA040 — для функции CreateProcess, которая имеет 10 параметров;
- _TlsAiioc@o — для функции TisAiioc, которая не имеет параметров.