Сообщений 2    Оценка 80        Оценить  
Система Orphus

Форматы РЕ и COFF объектных файлов

Автор: Мэтт Питрек
Источник: Секреты системного программирования в Windows 95. Глава 8.
Опубликовано: 05.04.2001
Исправлено: 15.04.2009
Версия текста: 1.1

Формат исполняемого файла операционной системы в значительной степени отражает встроенные в операционную систему предположения и режимы поведения. Хотя освоение всех деталей формата исполняемого файла и не является одной из главных задач обучения программированию, тем не менее, из этого можно почерпнуть немало ценной информации об операционной системе. Динамическая компоновка, поведение загрузчика и управление памятью — это только три примера специфических свойств операционной системы, которые можно понять по мере изучения формата исполняемых файлов.

В этой главе мы совершим экскурс в переносимый исполняемый (РЕ — Portable Executable) формат файла, который фирма Microsoft разработала для использования во всех ее операционных системах Win32 (Windows NT, Windows 95, Win32s).

Возможно, читатель удивится тому, что я рассказываю о РЕ-формате в этой книге, тогда как несколько описаний этого формата можно найти на CD-ROM Microsoft Developer Network. Главная причина, по которой я решил описать РЕ-формат, — это тот факт, что внутри самой Windows 95 используются те же ключевые структуры данных, что и в файлах РЕ-формата. Так, например, Windows 95 отображает заголовок РЕ-файла в память и использует его для представления загружаемого модуля. Для того чтобы понять, как работает ядро Windows 95, необходимо разобраться с РЕ-форматом. Гарантирую, что это достаточно просто.

Другой причиной, по которой я решил описать РЕ-формат, является то, что описание РЕ-формата фирмы Microsoft, как и остальная документация этой фирмы, предполагает, что пользователь живет и дышит исполняемым форматом файла. Не будет преувеличением назвать документацию Microsoft отрывочной. Так что моей задачей в этой главе является "оживление" этой документации и соотнесение ее с привычными для каждого пользователя понятиями. По ходу дела я указал ряд способов, посредством которых РЕ-формат влияет на реализацию операционной системы и наоборот.

РЕ-формат играет и будет играть в обозримом будущем ключевую роль во всех операционных системах Microsoft, включая Cairo. Даже если вы программируете в Windows 3.1, используя Visual C++, вам все равно придется пользоваться РЕ-файлами (32-разрядные расширенные компоненты DOS в Visual C++ используют этот формат). А если вы собираетесь заняться любой работой, связанной с системным программированием низкого уровня в Windows 95, практические знания по РЕ-файлам просто необходимы.

При обсуждении РЕ-формата я не собираюсь тщательно перебирать груды шестнадцатеричных кодов или без конца объяснять назначения отдельных битов. Вместо этого я хочу изложить концепции, заложенные в РЕ-формат, и связать их с понятиями, которые ежедневно встречаются при программировании в Win32. Например, концепция локальных переменных для цепочек (а-ля __declspec (thread)) выводила меня из терпения до тех пор, пока я не увидел, с какой простотой и изяществом она реализована в исполняемом файле. Учитывая, что многие программисты имеют опыт работы в Win 16, я покажу, как связаны конструкции в РЕ-формате с их эквивалентами в 16-разрядных файловых форматах.

Вместе с новыми форматами исполняемых файлов Microsoft также ввела новые форматы объектных модулей и библиотек, создаваемые ее собственными компиляторами и ассемблерами, (Новый файловый формат LIB, по существу, представляет собой просто связку объектных файлов, упорядоченных с помощью индекса; когда в дальнейшем я ссылаюсь в этой книге на объектные файлы, я буду иметь в виду как COFF-объектные, так и LIB-файлы.) Эти новые объектные и LIB-файловые форматы имеют немало общих концепций с форматом РЕ. До настоящего времени не существовало общедоступного источника информации об объектных и LIB-файлах фирмы Microsoft, и даже к моменту написания этой книги информация очень скудна. Поэтому стоит осветить также форматы объектных файлов и LIB-файлов.

Общеизвестно, что Windows NT (первая из операционных систем Win32) унаследовала многое от VAX VMS и UNIX. Многие ведущие разработчики NT перед своим приходом в Microsoft программировали и работали именно над этими системами. Вполне естественно, что, когда им пришлось создавать NT, чтобы сохранить свое время и силы, они использовали ранее написанные и опробованные средства. Исполняемый формат и формат объектного модуля, который эти средства создавали и с которым они работали, называется COFF (Common Object File Format — стандартный формат объектного файла).

Относительно устаревшую (по компьютерным меркам) сущность COFF можно усмотреть в том, что некоторые поля в файлах имеют восьмеричный формат. COFF-формат был сам по себе неплохой отправной точкой, но нуждался в расширении, чтобы удовлетворить потребностям новых операционных систем, таких как Windows NT или Windows 95. Результатом такого усовершенствования явился РЕ-формат (не забывайте: РЕ означает Portable Executable — переносимый исполняемый). Этот формат называется переносимым, так как все реализации Windows NT в различных системах (Intel 386, MIPS, Alpha, Power PC и т.д.) используют один и тот же исполняемый формат. Конечно, имеются различия, например, связанные с двоичной кодировкой команд процессора. Нельзя запустить на Intel исполняемый РЕ-файл, откомпилированный в MIPS. Тем не менее существенно, что нет нужды полностью переписывать загрузчик операционной системы и программные средства для каждого нового процессора.

Microsoft стремилась усовершенствовать Windows NT, и это хорошо иллюстрируется тем, что Microsoft отказалась от своих существующих 32-разрядных средств и файловых форматов. Драйверы виртуальных устройств, написанные для Windows З.х, использовали другой 32-разрядный формат файла (LE-формат) задолго до появления NT на свет. Следуя принципу "Если не поломано, не надо и чинить", заложенному в Windows, Windows 95 использует как РЕ-, так и LE-формат. Это позволило Microsoft широко использовать существующие программы под Windows 3-х.

Вполне естественно ожидать совершенно другого исполняемого формата для совершенно новой операционной системы (какой является Windows NT), но другой вопрос— форматы объектных модулей (.OBJ и LIB). До появления 32-разрядной версии 1.0 Visual C++ все компиляторы Microsoft пользовались спецификацией Intel OMF (Object Module Format — формат объектного модуля). Компиляторы Microsoft для реализации Win32 создают объектные файлы в формате COFF. Некоторые конкуренты Microsoft, например Borland, отказались от формата COFF объектных файлов и продолжали придерживаться формата OMF Intel. В результате компании, производящие объектные и LIB-файлы, рассчитанные на использование с несколькими компиляторами, будут вынуждены возвратиться к системе поставок различных версий их продуктов для различных компиляторов (если они не сделали этого до сих пор).

Те пользователи, которые любят усматривать во всех действиях Microsoft скрытность, могут увидеть в смене объектных форматов стремление Microsoft воспрепятствовать своим конкурентам. Чтобы гарантировать "совместимость" с Microsoft вплоть до уровня объектных файлов, другие фирмы будут вынуждены конвертировать все свои 32-разрядные средства в форматы COFF OBJ и LIB. Подводя итог, можно сказать, что объектные и LIB-файловые форматы являются еще одним примером отказа Microsoft от существующих стандартов при выборе приоритетов развития этой фирмы.

Вместе с некоторыми определениями структур для объектных файлов формат COFF РЕ-формат задокументирован (в самом размытом смысле этого слова) в файле заголовка WINNT.H. (Я буду пользоваться именами полей из WINNT.H позже в этой главе.) Примерно посредине WINNT.H находится секция, озаглавленная "Image Format". Эта секция начинается с небольших фрагментов из старых добрых заголовков форматов DOS MZ и NE перед переходом к новой информации, связанной с РЕ. WINNT.H дает определения структур исходных данных, используемых РЕ-файлами, однако содержит всего лишь прозрачный намек на полезные комментарии, объясняющие назначение структур и флагов. Автор заголовочного файла для РЕ-формата (некий Michael J. O'Leary) определенно питает склонность к длинным, описательным именам, а также к глубоко вложенным структурам и макросам. Программируя с использованием WINNT.H, нередко можно встретить, например, такое выражение:

pNTHeader->OptionaIHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG].VirtualAddress;

Вы, вероятно, захотите не просто прочесть о том, из чего состоят РЕ-файлы, но и самим просмотреть несколько РЕ-файлов, чтобы на практике увидеть, как реализуются представленные здесь концепции. Если вы используете средства Microsoft для Win32, то программу DUMPBIN из Visual C++, а также Win32 SDK, можно применить для того, чтобы "препарировать" файлы РЕ и COFF OBJ/LIB и представить их в удобоваримом виде. DUMPBIN даже имеет замечательный параметр для дизассемблирования программных секций изучаемого файла. Интересно отметить, что фирма Microsoft, которая запрещает пользователям дизассемблировать свои программы, в данном случае предоставила средство для простого дизассемблирования программ и библиотек динамической компоновки (DLL). Если бы возможность дизассемблировать ЕХЕ и объектные файлы не была бы полезна, зачем понадобилось бы Microsoft снабжать этим DUMPBIN? В этом случае можно сказать: "Делай, как мы говорим, а не так, как мы делаем".

Пользователи Borland могут использовать TDUMP, чтобы просматривать РЕ-файлы, однако TDUMP не воспринимает объектные файлы формата COFF. Здесь нет ничего страшного, так как компилятор Borland вообще не создает объектные файлы формата COFF. Бросая свой вызов, я тоже написал программу просмотра РЕ- и COFF-OBJ/LIB-файлов (PEDUMP), которая, на мой взгляд, осуществляет более наглядный вывод, чем DUMPBIN. Несмотря на то, что моя программа не может дизассемблировать, во всем остальном она полностью функционально эквивалентна DUMPBIN и, кроме того, содержит некоторые новые черты, делающие ее достойной внимания. Исходный текст программы PEDUMP находится на специальной дискете — вот почему я не привожу ее здесь полностью. Вместо этого я предоставляю образец вывода PEDUMP, чтобы проиллюстрировать концепции по мере их изложения.

Программа PEDUMP

Программа PEDUMP является служебной, запускаемой из командной строки и предназначена для вывода РЕ-файлов и файлов формата COFF OBJ/LIB. Она использует возможности терминала Win32, чтобы обойтись без кропотливой работы с интерфейсом пользователя. PEDUMP имеет следующий синтаксис: PEDUMP [ключи] имя файла. Имеющиеся ключи можно увидеть, запустив PEDUMP без аргументов. PEDUMP использует следующие ключи:

/A  выводить все (по существу, активизируются все ключи)
/Н  включить шестнадцатеричный вывод каждой секции в конце дампа
/I  включить адреса переходников из Import Address Table
/L  включить информацию о количестве строк (как для РЕ-, так и для COFF-объектных файлов)
/R  показать смещения относительно базы (только для РЕ-файлов)
/S  показать таблицу символов (как для РЕ-, так и для COFF OBJ-файлов)

По умолчанию ни один из ключей не активизирован. В этом случае большая часть необходимой информации будет доступна и не будет слишком громадного объема выведенных данных.

PEDUMP осуществляет вывод в стандартный выходной файл (например, на экран), так что этот вывод можно переадресовать в какой-либо файл с помощью знака ">" (больше), введенного в командной строке.

Исходные файлы для PEDUMP включены вместе с ней. PEDUMP была создана с помощью компилятора Microsoft Visual C++ 2.0, хотя я также компилировал эту программу по мере ее усовершенствования, пользуясь Borland C++ 4.x.

Основные сведения о форматах Win32 и РЕ

Перед тем как начать обсуждение формата РЕ-файла, я хотел бы рассмотреть несколько новых основных идей, позволивших создать такой формат. В процессе этого обсуждения я буду использовать понятие модуль, подразумевая под ним текст программ, данные и ресурсы исполняемого файла или DLL, которые были загружены в память. Помимо текста программы и данных, которые использует непосредственно программа, модуль также включает вспомогательные данные, используемые Windows для того, чтобы определить, где расположены в памяти текст программы и данные. В Win 16 вспомогательные структуры данных находятся в базе данных модуля (сегмент, на который ссылается HMODULE). В Win32 эта информация содержится в заголовке РЕ-файла (структура IMAGE_NT_HEADERS), о котором вскоре я подробно расскажу.

Самое важное из того, что следует знать о РЕ-файлах, это то, что исполняемый файл на диске и модуль, получаемый после загрузки, очень похожи. Причиной этого является то, что загрузчик Windows должен создать из дискового файла исполняемый процесс без больших усилий. Точнее говоря, загрузчик попросту использует отображенные в память файлы Win32, чтобы загрузить соответствующие части РЕ-файла в адресное пространство программы. Здесь уместна аналогия со строительством сборных домиков. У вас есть относительно немного элементов, расставляя их по своим местам и скрепляя стандартными соединениями, вы достаточно быстро собираете целый дом, — вся работа состоит из простого защелкивания таких стандартных соединений. И такой же простой задачей, как подключение электричества и водопровода к сборному домику, является соединение РЕ-файла с внешним миром (т.е. подключение к нему DLL и т.д.).

Так же просто загружается и DLL. После того как ЕХЕ или .DLL модуль загружены, Windows обращается с ними так же, как и с другими отображенными в память файлами. Совершенно иная ситуация в 16-разрядной Windows. 16-разрядный NE-загрузчик файла считывает файл в память порциями и создает отдельные структуры данных для представления модуля в памяти. Когда необходимо загрузить сегмент программы или данных, загрузчик должен выделить новый сегмент из общей кучи, обнаружить, где хранятся исходные данные в исполняемом файле, отыскать это место, считать исходные данные и применить любой подходящий крепеж. Кроме того, каждый 16-разрядный модуль обязан запоминать все используемые в данный момент селекторы, независимо от того, выгружен ли сегмент.

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

Другим важным понятием, о котором необходимо знать, является RVA (Relative Virtual Address — относительный виртуальный адрес). Многие поля в РЕ-файлах задаются именно с помощью их RVA. RVA — это просто смещение данного элемента по отношению к адресу, с которого начинается отображение файла в памяти. Пусть, к примеру, загрузчик Windows отобразил РЕ-файл в память, начиная с адреса 0х400000 в виртуальном адресном пространстве. Если некая таблица в отображении начинается с адреса 0х401464, то RVA данной таблицы 0х1464:

(виртуальный адрес 0х401464) - (базовый адрес 0х400000) = RVA 0х1464

Чтобы перевести RVA в указатель памяти, просто прибавьте RVA к базовому адресу, начиная с которого был загружен модуль. Термин базовый адрес представляет еще одно важное понятие, о котором следует помнить. Базовый адрес — это адрес, с которого начинается отображенный в память ЕХЕ-файл или DLL. Для удобства Windows NT и Windows 95 используют базовый адрес модуля в качестве дескриптора образца модуля (HINSTANCE — instance handle). To, что в Win32 базовый адрес называется HINSTANCE, может вызвать недоразумения, так как термин дескриптор образца происходит из 16-разрядной Windows. Каждая копия приложения в Win16 получает свой собственный сегмент данных (и связанный с ним глобальный дескриптор), который отличает эту копию приложения от других; отсюда и название дескриптор образца.

В Win32 нет нужды различать отдельные копии приложений, так как у них нет общего адресного пространства. Однако термин HINSTANCE сохранен, чтобы отразить преемственность Win32 по отношению к Win16. В случае Win32 существенно то, что можно вызвать GetModuleHandle() для любой DLL, которая используется в процессе, и получить указатель, который можно использовать для доступа к компонентам модуля. Под компонентами модуля я подразумеваю его импортируемые и экспортируемые функции, его перемещения, его программные секции и секции данных и т.п.

Еще одним понятием, с которым следует ознакомиться для того, чтобы исследовать РЕ- и COFF OBJ-файлы, является секция. Секция в файлах РЕ или COFF OBJ примерно эквивалентна сегменту или ресурсам в 16-разрядном NE-файле. Секции содержат либо код программ, либо данные. Некоторые секции содержат код и данные, непосредственно объявляемые и используемые программами, тогда как другие секции данных создаются компоновщиками и библиотекарями специально для пользователя и содержат информацию, необходимую для работы операционной системы. В некоторых описаниях формата РЕ фирмы Microsoft секции также называются объектами. Однако этот последний термин имеет так много (возможно, противоречащих друг другу) значений, что я решил придерживаться термина секции для обозначения им областей программного кода и данных. Секции рассмотрены более подробно в этой главе, в разделе "Часто встречающиеся секции"; пока что читателю достаточно просто знать, что такое секция.

Перед тем как погрузиться в детали РЕ-файла, советую изучить рис. 8.1, представляющий общий формат РЕ-файла. Несмотря на то, что я буду объяснять каждый элемент отдельно, полезно для начала рассмотреть их все вместе.


Рис. 8.1. Общий формат РЕ-файла

Заголовок РЕ-файла

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

Как и в других исполняемых форматах от Microsoft, заголовок не находится в самом начале файла. Вместо этого несколько сотен первых байтов типичного РЕ-файла заняты под заглушку DOS. Эта заглушка представляет собой минимальную DOS-программу, которая выводит что-либо вроде: "Эта программа не может быть запущена под DOS". Все это предусматривает случай, когда пользователь запускает программу Win32 в среде, которая не поддерживает Win32, получая при этом приведенное выше сообщение об ошибке (звучит разочаровывающе, конечно). После того как загрузчик Win32 отобразил в память РЕ-файл, первый байт отображения файла соответствует первому байту заглушки DOS. И это не так уж плохо. С каждой запускаемой Win32 программой пользователь получает дополнительную DOS-программу, загруженную просто так! (В Win 16 заглушка DOS не загружается в память.)

Как и в других исполняемых форматах Microsoft, настоящий заголовок можно обнаружить, найдя его стартовое смещение, которое хранится в заголовке DOS. Файл WINNT.H содержит определение структуры для заголовка заглушки DOS, что делает очень простым нахождение начала заголовка РЕ-файла. Поле e_lfanew собственно и содержит относительное смещение (или, если хотите, RVA) настоящего заголовка РЕ-файла. Чтобы установить указатель в памяти на заголовок РЕ-файла, достаточно просто сложить значение в поле с базовым адресом отображения:

//Пренебрегаем для ясности преобразованием типов и указателей...
pNTHeader = dosHeader + dosHeader->e_lranew;

Настоящее веселье начинается, когда указатель установлен на основной заголовок РЕ-файла. Основной заголовок РЕ-файла представляет структуру типа IMAGE_NT_HEADERS, определенную в файле WINNT.H. Структура IMAGE_NT_HEADERS в памяти — это то, что Windows 95 использует в качестве своей базы данных модуля в памяти. Каждый загруженный ЕХЕ-файл или DLL представлены в Windows 95 структурой IMAGE_NT_HEADERS. Эта структура состоит из двойного слова и двух подструктур, как показано ниже:

DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER OptionalHeader;

Поле Signature (сигнатура — подпись), представленное как ASCII код, — это РЕ\0\0 (два нулевых байта после РЕ). Если поле e_lfanew в заголовке DOS указало вместо обозначения РЕ обозначение NE в этом месте, значит, вы работаете с файлом Win 16 NE. Аналогично, если указано обозначение LE в поле Signature, то это файл VxD (VirtualDeviceDriver— драйвер виртуального устройства). Обозначение LX указывает на файл старой соперницы Windows 95 — OS/2.

За двойным словом — сигнатурой РЕ, в заголовке РЕ-файла следует структура типа IMAGE_FILE_HEADER. Поля этой структуры содержат только самую общую информацию о файле. Структура не изменилась по сравнению с исходными COFF-реализациями. Эта структура является частью заголовка РЕ-файла, кроме того, появляется в самом начале объектных COFF-файлов, создаваемых компиляторами Microsoft Win32. Далее приводятся поля IMAGE_FILE_HEADER.

WORD    Machine

Это центральный процессор, для которого предназначен файл. Определены следующие идентификаторы процессоров:

Intel I386  0х14С
Intel I860  0xl4D
MIPSR3000   0х162
MIPS R4000  0х166
DEC Alpha AXP   0х184
PowerPC         0x1F0 (little endian)
Motorola 68000  0х268
PA RISC         0х290 (Precision Architecture)

WORD NumberOfSections

Количество секций в ЕХЕ- или OBJ-файле.

DWORD TimeDateStamp

Время, когда файл был создан компоновщиком (или компилятором, если это OBJ-файл). В этом поле указано количество секунд, истекших с 16:00 31 декабря 1969 года.

DWORD PointerToSymbolTable

Файловое смещение COFF-таблицы символов. Это поле используется только в OBJ- и РЕ-файлах с информацией COFF-отладчика. РЕ-файлы поддерживают разнообразные отладочные форматы, так что отладчики должны ссылаться ко входу IMAGE_DIRECTORY_ENTRY_DEBUG в каталоге данных (будет определен позднее).

DWORD NumberOfSymbols

Количество символов в COFF-таблице символов. См. предыдущее поле.

WORD SheOfOptionalHeader

Размер необязательного заголовка, который может следовать за этой структурой. В исполняемых файлах — это размер структуры IMAGE_OPTIONAL_HEADER, которая следует за этой структурой. В объектных файлах, по утверждению Microsoft, это поле всегда содержит нуль. Однако при просмотре библиотеки вывода KERNEL32.LIB можно обнаружить объектный файл с ненулевым значением в этом поле, так что относитесь к высказыванию Microsoft с некоторым скептицизмом.

WORD Characteristics

Флаги, содержащие информацию о файле. Здесь описываются некоторые важные поля (другие поля определены в WINNT.H).

0х0001  Файл не содержит перемещений.
0х0002  Файл представляет исполняемое отображение (т.е. это не OBJ- или LIB-файл).
0х2000  Файл является библиотекой динамической компоновки (DLL), а не программой.

Третьим компонентом заголовка РЕ-файла является структура типа IMAGE_OPTIONAL_HEADER. Для РЕ-файлов эта часть является обязательной. Формат COFF разрешает индивидуальные реализации для определения структуры дополнительной информации, помимо стандартного IMAGE_FILE_HEADER. В полях в IMAGE_OPTIONAL_HEADER разработчики РЕ-формата поместили то, что они посчитали важным дополнением к общей информации в IMAGE_FILE_HEADER.

Для пользователя не является критическим знание всех полей в IMAGE_OPTIONAL_HEADER, Наиболее важными полями являются поля ImageBase и Subsystem. По желанию читатель может бегло просмотреть или пропустить следующее описание полей.

WORD Magic

Слово-сигнатура, определяющее состояние отображенного файла. Определены следующие значения:

0х0107   Отображение ПЗУ 
0х010В   Нормальное исполняемое отображение (Значение для большей части файлов)

BYTE MajorLinker Version; BYTE MinorLinker Version

Версия компоновщика, который создал данный файл. Числа должны быть представлены в десятичном виде, а не в шестнадцатеричном. Типичная версия компоновщика 2.23.

DWORD SizeOfCode

Суммарный размер программных секций, округленный к верхней границе. Обычно большинство файлов имеют только одну программную секцию, так что это поле обычно соответствует размеру секции .text.

DWORD sizeOfInitializedData

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

DWORD SizeOfUninltializedData

Размер секций, под которые загрузчик выделяет место в виртуальном адресном пространстве, но которые не занимают никакого места в дисковом файле. В начале работы программы эти секции не обязаны иметь каких-либо определенных значений — отсюда название неинициализированные данные (Uninitialized Data). Неинициализированные данные обычно находятся в секции под названием .bss.

DWORD AddressOfEntryPoint

Адрес, с которого отображение начинает выполнение. Это RVA, который можно найти в секции .text. Это поле применимо как для ЕХЕ-файла, так и для DLL.

DWORD BaseOfCode

RVA, с которого начинаются программные секции файла. Программные секции кода обычно идут в памяти перед секциями данных и после заголовка РЕ-файла. Этот RVA обычно равен 0х1000 для ЕХЕ-файлов, созданных компоновщиками Microsoft. Для TLINK32 (Borland) значение этого поля равно 0х10000, так как по умолчанию этот компоновщик выравнивает объекты на границу в 64 Кбайт в отличие от 4 Кбайт в случае компоновщика Microsoft.

DWORD BaseOfData

RVA, с которого начинаются секции данных файла. Секции данных обычно идут последними в памяти, после заголовка РЕ-файла и программных секций.

DWORD ImageBase

Когда компоновщик создает исполняемый файл, он предполагает, что файл будет отображен в определенное место в памяти. Вот именно этот адрес и хранится в этом поле. Знание адреса загрузки позволяет компоновщику провести оптимизацию. Если загрузчик действительно отобразил файл в память по этому адресу, то программа перед запуском не нуждается ни в какой настройке. Я расскажу об этом немного подробнее при обсуждении перемещений относительно базы. В исполняемых файлах NT 3.1 адрес отображения по умолчанию равен 0х10000. В случае DLL этот адрес по умолчанию равен 0х400000. В Windows 95 адрес 0х10000 нельзя использовать для загрузки 32-разрядных файлов ЕХЕ, так как он лежит в пределах линейной области адресного пространства, общего для всех процессов. Поэтому для Windows NT 3.5 Microsoft изменила для исполняемых файлов Win32 базовый адрес по умолчанию, сделав его равным 0х400000. Более старые программы, которые были скомпонованы в предположении, что базовый адрес равен 0х10000, загружаются Windows 95 дольше, потому что загрузчик должен применить базовые поправки. Я опишу базовые поправки немного позже.

DWORD SectionAlignment

После отображения в память каждая секция будет обязательно начинаться с виртуального адреса, кратного данной величине. С учетом подкачки страниц минимальная величина этого поля 0х1000 используется компоновщиком Microsoft по умолчанию. TLINK в Borland C++ использует по умолчанию 0х10000 (64 Кбайт).

DWORD FileAlignment

В случае РЕ-файла исходные данные, которые входят в состав каждой секции, будут обязательно начинаться с адреса, кратного данной величине. Значение, устанавливаемое по умолчанию, равно 0х200 байт и, вероятно, выбрано так для того, чтобы начало секции всегда совпадало с началом дискового сектора (0х200 байт — это как раз размер дискового сектора). Это поле эквивалентно размеру выравнивания сегмента/ресурса в NE-файлах. В отличие от NE-файлов, РЕ-файлы не состоят из сотен секций, так что память, теряемая при выравнивании секций файла, обычно очень незначительна.

WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion

Самая старая версия операционной системы, которая может использовать данный исполняемый файл. Назначение этого поля не совсем ясно, так как поля подсистемы (приведены ниже), похоже, имеют такое же предназначение. В большей части файлов Win32 в этом поле содержится значение, соответствующее версии 1.0 .

WORD MajorImageVersion; WORD MinorImageVersion

Определяемое пользователем поле. Это поле позволяет иметь различные версии ЕХЕ-файлов и DLL. Эти поля устанавливаются с помощью ключа компоновщика /VERSION, например:

LINK/VERSION:2.0 myobj.obj

WORD MajorSubsystem Version; WORD MinorSubsystem Version

Это поле содержит самую старую версию подсистемы, позволяющую запускать данный исполняемый файл. Типичное значение в этом поле 4.0 (обозначает Windows 4.0, что равносильно Windows 95).

DWORD Reserved

Это поле, по-видимому, всегда равно нулю.

DWORD SizeOfImage

Представляет общий размер всех частей отображения, находящихся под контролем загрузчика. Эта величина равна размеру области памяти, начиная с базового адреса отображения и заканчивая адресом конца последней секции. Адрес конца секции выровнен на ближайшую верхнюю границу секции.

DWORD SizeOfHeaders

Размер заголовка РЕ-файла и таблицы секции (объекта). Исходные данные для секций начинаются сразу после всех составляющих частей заголовка.

DWORD Checksum

Предположительно отвечает контрольной сумме контроля циклическим избыточным кодом (CRC-контроль) для данного файла. Как и для других исполняемых форматов Microsoft, это поле обычно игнорируется и устанавливается в нуль. Однако для всех DLL драйверов, DLL, загруженных во время загрузки ОС, и серверных DLL эта контрольная, сумма должна иметь правильное значение. Алгоритм для контрольной суммы можно найти в IMAGEHLP.DLL. Исходники IMAGEHLP.DLL поставляются в WIN32 SDK.

WORD Subsystem

Тип подсистемы, которую данный исполняемый файл использует для своего пользовательского интерфейса. WINNT.H определяет следующие значения:

NATIVE == 1 Подсистема не требуется (например, для драйвера устройства) 
WINDOWS_GUI = 2 Запускается в подсистеме Windows GUI 
WINDOWS_CUI = 3 Запускается в подсистеме Windows character (терминальное приложение) 
OS2_CUI = 5 Запускается в подсистеме OS/2 (только приложения OS/2 1.x) 
POSIX_CUI = 7   Запускается в подсистеме Posix

WORD DllCharacteristics (обозначен как вышедший из употребления в NT 3.5)

Набор флагов, показывающий, при каких обстоятельствах будет вызываться функция инициализации DLL (например, DllMain()). Эта величина, по-видимому, всегда должна устанавливаться в нуль, однако операционная система вызывает функцию инициализации DLL для всех четырех случаев. Определены следующие значения:

1 — Вызов, когда DLL впервые загружена в адресное пространство процесса
2 — Вызов, когда цепочка заканчивает работу 
4 — Вызов, когда цепочка начинает работу 
8 — Вызов при выходе из DLL

DWORD SizeOfStackReserve

Объем виртуальной памяти, резервируемой под начальный стек цепочки. Однако не вся эта память выделяется (см. следующее поле). По умолчанию это поле устанавливается в 0х100000 (1 Мбайт). Если пользователь указывает 0 в качестве размера стека в CreateThread(), получившаяся цепочка будет иметь стек того же размера.

DWORD SizeQfStackCommit

Количество памяти, изначально выделяемой под исходный стек цепочки. Это поле по умолчанию равно 0х1000 байт (1 страница) для компоновщиков Microsoft, тогда как TLINK32 делает его равным 0х2000 (2 страницы).

DWORD SizeOfHeapReserve

Объем виртуальной памяти, резервируемой под изначальную кучу программы. Этот дескриптор кучи можно получить, вызвав GetProcessHeap(). Однако не вся эта память выделяется (см. следующее поле).

DWORD SizeOfHeapCommit

Объем виртуальной памяти, изначально выделяемой под кучу процесса. По умолчанию компоновщик делает это поле равным 0х1000 байт.

DWORD LoaderFlags (обозначен как вышедший из употребления в NT 3.5)

Как следует из WINNT.H, эти поля, по-видимому, связаны с поддержкой отладчика. Мне никогда не приходилось видеть исполняемый файл, у которого хотя бы один из этих битов был активизирован. Мне также неясно, как с помощью компоновщика устанавливать их. Определены следующие значения:

1 — Запускать ли команду прерывания перед запуском процесса?
2 — Запускать ли отладчик программы после процесса?

DWORD NumberOfRvaAndShes

Количество входов в массиве DataDirectory (см. описание следующего поля). Современные программные средства всегда делают это значение равным 16.

IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]

Массив структур типа IMAGE_DATA_DIRECTORY. Начальные элементы массива содержат стартовый RVA и размеры важных частей исполняемого файла. В настоящее время некоторые элементы в конце массива не используются. Первый элемент массива — это всегда адрес и размер экспортированной таблицы функций (если она присутствует). Второй элемент массива — адрес и размер импортированной таблицы функций и т.д. Для того чтобы увидеть полный перечень определений элементов массива, см. IMAGE_DIRECTORY_ENTRY_xxx директивы #define в WINNT.H.

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

Большая часть элементов массива описывает все данные в секции. Тем не менее элемент IMAGE_DIRECTORY_ENTRY_DEBUG охватывает только небольшую часть байтов в секции .rdata. За более подробной информацией по этому вопросу обращайтесь к разделу "Секция .rdata" в этой главе.

Таблица секций

Между заголовком РЕ-файла и исходными данными для секций отображения находится таблица секций. Эта таблица содержит информацию о каждой секции отображения. Секции в отображении упорядочены по их стартовому адресу, а не в алфавитном порядке.

К настоящему моменту необходимо четко разъяснить, что же такое секция. В NE-файле программный код и данные хранятся в различных сегментах в файле. Часть заголовка NE-файла представляет собой массив структур — по одной для каждого сегмента, используемого программой. Каждая структура массива содержит информацию об одном сегменте. Хранимая информация включает тип сегмента (программа или данные), его размер и его расположение, где бы он ни находился в файле. В РЕ-файле таблица секций аналогична таблице сегментов в NE-файле.

Однако, в отличие от таблицы сегментов NE-файла, таблица секций РЕ-файла не хранит значение селектора для каждого куска программного кода или данных. Вместо этого каждый элемент таблицы секций хранит адрес, по которому исходные данные файла были отображены в память. Несмотря на то, что секции аналогичны 32-разрядным сегментам, они на самом деле не являются индивидуальными сегментами. Вместо этого секция просто отвечает диапазону памяти виртуального адресного пространства процесса.

Другим отличием РЕ-файлов от NE-файлов является то, как они управляют вспомогательными данными, которые используются не программой, а операционной системой. В качестве двух примеров можно привести перечень DLL, используемых исполняемыми файлами, и местонахождение таблицы привязки (fixup). В NE-файлах ресурсы не считаются сегментами. И хотя они имеют приписанные им селекторы, информация о ресурсах не хранится в таблице сегментов заголовка NE-файла. Вместо этого ресурсы сведены в отдельную таблицу в конце заголовка NE-файла. Информации об импортированных и экспортированных функциях тоже не гарантируется выделение своего собственного сегмента, она накапливается в пределах заголовка NE-файла.

Другая ситуация в случае РЕ-файла. Все, что считается важным программным кодом или данными, хранится в полнокровной секции. Таким образом, информация об импортированных функциях хранится в своей собственной секции так же, как и таблица экспортируемых модулем функций. То же самое справедливо и для данных настройки. Любая программа или данные, которые могут понадобиться программе или операционной системе, получают свою собственную секцию.

Вскоре я начну рассматривать отдельные секции, но сначала мне нужно описать данные, с помощью которых операционная система работает с секциями. Сразу после заголовка РЕ-файла в памяти следует массив из IMAGE_SECTION_HEADER. Количество элементов этого массива задается в заголовке РЕ-файла (поле IMAGE_NT_HEADER.FileHeader.NumberOfSections). Программа PEDUMP выводит таблицу секций, все поля и атрибуты секций. Рис. 8.2 показывает вывод с помощью PEDUMP таблицы секций для типичного ЕХЕ-файла. Рис. 8.3 показывает вывод с помощью PEDUMP таблицы секций для типичного OBJ-файла.

01 .text VirtSize:  00005AFA  VirtAddr:      00001000
raw data offs:      00000400  raw data size: 00005C00
relocation offs:    00000000  relocations:   00000000
line # offs:        00009220  line #'s:      0000020C
characteristics:    60000020
  CODE MEM_EXECUTE MEM_READ

02 .bss VirtSize:   00001438  VirtAddr:      00007000
raw data offs:      00000000  raw data size: 00001600
relocation offs:    00000000  relocations:   00000000
line # offs:        00000000  line #'s:      00000000
characteristics:    C0000080
  UNINITIALIZED_DATA MEM_READ MEM_WRITE

03 .rdata VirtSize: 0000015C  VirtAddr:      00009000
raw data offs:      00006000  raw data size: 00000200
relocation offs:    00000000  relocations:   00000000
line # offs:        00000000  line #'s:      00000000
characteristics:    40000040
  INITIALIZED_DATA MEM_READ

04 .data VirtSize:  0000239C  VirtAddr:      0000A000
raw data offs:      00006200  raw data size: 00002400
relocation offs:    00000000  relocations:   00000000
line # offs:        00000000  line #'s:      00000000
characteristics:    C0000048
  INITIALIZED_DATA MEM_READ MEM_WRITE

05 .idata VirtSize: 0000033E  VirtAddr:      0000D000
raw data offs:      00008600  raw data size: 00000400
relocation offs:    00000000  relocations:   00000000
line # offs:        00000000  line #'s:      00000000
characteristics:    C0000040
  INITIALIZED DATA MEN_READ MEM_WRITE

06 .reloc VirtSize: 000006CE  VirtAddr:      0000E000
raw data offs:      00008A00  raw data size: 00000800
relocation offs:    00000000  relocations:   00000000
line # offs:        00000000  line #'s:      00000000
characteristics:    42000040
  INITIALIZED DATA MEM_DISCARDABLE MEM_READ

Рис. 8.2. Типичная таблица секций ЕХЕ-файла

01 .drectve PhysAddr: 00000000  VirtAddr:      00000000
raw data offs:        000000DC  raw data size: 00000026
relocation offs:      00000000  relocations:   00000000
line # offs:          00000080  line #'s:      00000000
characteristics:      00100A00
  LNK_INFO LNK_REMOVE

02 .debug$S PhysAddr: 00000026  VirtAddr:      00000000
raw data offs:        00000102  raw data size: 000016D0
relocation offs:      000017D2  relocations:   00000032
line # offs:          00000080  line #'s:      00000000
characteristics:      42100048
  INITIALIZED_DATA MEM_DISCARDABLE MEM_READ

03 .data PhysAddr:    000016F6  VirtAddr:      00000000
raw data offs:        000019C6  raw data size: 00000D87
relocation offs:      0000274P  relocations:   00000045
line # offs:          00000000  line #'s:      00000000
characteristics:      C0480048
  INITIALIZED_DATA MEM_READ MEM_WRITE

04 .text PhysAddr:    0000247D  VirtAddr:      00000000
raw data offs:        000029FF  raw data size: 000010DA
relocation offs:      00003AD9  relocations:   000000E9
line # offs:          000043F3  line #'s:      000000D9
characteristics:      60500020
  CODE MEM_EXECUTE MEM_READ

05 .debug$T PhysAddr: 00003557  VirtAddr:      00000000
raw data offs:        00004909  raw data size: 00000030
relocation offs:      00000008  relocations:   00000000
line # offs:          00000000  line #'s:      00000000
characteristics:      42100048
  INITIALIZED_DATA MEM_DISCARDABLE MEM_READ

Рис. 8.3 Типичная таблица секций объектного файла

Каждый IMAGE_SECTION_HEADER представляет собой полную базу данных об одной секции файла ЕХЕ или OBJ и имеет следующий формат.

BYTE Name[IMAGE_SIZEOF_SHORT_NAME]

Это 8-байтовое имя в стандарте ANSI (не Unicode), которое именует секцию. Большинство имен секций начинается с точки (например, .text), но это не обязательно, вопреки тому, в чем пытаются вас уверить отдельные документы по РЕ-файлам. Пользователь может давать имена своим собственным секциям с помощью либо сегментной директивы в ассемблере, либо с помощью директив #pragma data_seg и #pragma code_seg компилятора Microsoft C/C++. (Пользователи Borland C++ должны использовать #pragma codeseg.) Необходимо отметить, что, если имя секции занимает 8 полных байтов, отсутствует завершающий байт NULL. (Программа TDUMP в Borland C++ 4.Ox упускает из виду это обстоятельство и изрыгает последующий мусор из некоторых РЕ ЕХЕ-файлов.) Если вы приверженец printf(), то можете использовать "%.8s", чтобы не копировать строку-имя в другой буфер для завершения ее нулевым байтом.

union {
DWORD    PhysicalAddress 
DWORD    VirtualSize 
} Misc;

Это поле имеет различные назначения в зависимости от того, встречается ли оно в ЕХЕ- или OBJ-файле. В ЕХЕ-файле оно содержит виртуальный размер секции программного кода или данных. Это размер до округления на ближайшую верхнюю границу файла. Поле SizeOfRawData дальше в этой структуре содержит это округленное значение. Интересно, что Borland TLINK32 меняет местами значение этого поля и поля SizeOfRawData и, тем не менее, остается правильным компоновщиком. В случае OBJ-файлов это поле указывает физический адрес секции. Первая секция начинается с адреса 0. Чтобы получить физический адрес следующей секции, надо прибавить значение в SizeOfRawData к физическому адресу данной секции.

DWORD VirtualAddress

В случае ЕХЕ-файлов это поле содержит RVA, куда загрузчик должен отобразить секцию. Чтобы вычислить реальный начальный адрес данной секции в памяти, необходимо к виртуальному адресу секции, содержащемуся в этом поле, прибавить базовый адрес отображения. Средства Microsoft устанавливают по умолчанию RVA первой секции равным 0х1000. Для объектных файлов это поле не несет никакого смысла и устанавливается в 0.

DWORD SizeOfRawData

В ЕХЕ-файлах это поле содержит размер секции, выровненный на ближайшую верхнюю границу размера файла. Например, допустим, что размер выравнивания файла 0х200. Если поле VirtualSize указывает, что длина секции Ох35А байт, то в данном поле будет указано, что размер секции 0х400 байт. Для OBJ-файлов это поле содержит точный размер секции, сгенерированной компилятором или ассемблером. Другими словами, для OBJ-файлов оно эквивалентно полю VirtualSize в ЕХЕ-файлах.

DWORD PointerToRawData

Это файловое смещение участка, где находятся исходные данные для секции. Если пользователь сам отображает в память РЕ- или COFF-файл (вместо того, чтобы доверить загрузку операционной системе), это поле важнее, чем поле VirtualAddress. Причиной является то, что в этом случае получится абсолютно линейное отображение всего файла, так что данные для секций будут находиться по этому смещению, а не по RVA, указанному в поле VirtualAddress.

DWORD PointerToRelocations

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

DWORD PointerToLinenumbers

Файловое смещение таблицы номеров строк. Таблица номеров строк ставит в соответствие номера строк исходного файла адресам, по которым можно найти код, сгенерированный для данной строки. В современных отладочных форматах, таких как формат CodeView, информация о номерах строк хранится как часть информации отладчика. В отладочном формате COFF, однако, информация о номерах строк концептуально отлична от информации о символьных именах/типах. Обычно только секции с программным кодом (например, .text или CODE) имеют номера строк. В ЕХЕ-файлах номера строк собраны в конце файла после исходных данных для секций. В объектных файлах таблица номеров строк для секции следует за исходными данными секции и таблицей перемещений для этой секции. Я рассматриваю формат таблиц номеров строк позже, в разделе "Информация COFF-отладчика".

WORD NumberOfRelocations

Количество перемещений в таблице поправок для данной секции (поле PointerToRelocations приведено выше). Это поле используется, по-видимому, только в объектных файлах.

WORD NumberOfLinenumbers

Количество номеров строк в таблице номеров строк для данной секции (поле PointerToLinenumbers приведено выше).

DWORD Characteristics

То, что большая часть программистов называет флагами (flags), формат COFF/PE называет характеристиками (characteristics). Это поле представляет собой набор флагов, которые указывают на атрибуты секции (программа/данные, предназначен для чтения, предназначен для записи и т.п.). За полным перечнем всех возможных атрибутов секции обращайтесь к IMAGE_SCN_XXX_XXX defines в файле заголовка WINNT.H. Некоторые из самых важных флагов приведены в табл. 8.1.

ФлагИспользование
0х00000020Эта секция содержит программный код. Как правило, устанавливается вместе с флагом (0х80000000)
0х00000040Данная секция содержит инициализированные данные. Почти для всех секций, кроме исполняемых и .bss секций, этот флаг установлен
0х00000080Данная секция содержит неинициализированные данные (например, .bss секции)
0х00000200Данная секция содержит комментарии или какой-нибудь другой вид информации. Типичное использование такой секции — это секция .drectve, создаваемая компилятором и содержащая команды для компоновщика
0х00000800Содержимое данной секции не должно быть помещено в конечный ЕХЕ-файл. Такая секция используется компилятором/ассемблером для передачи информации компоновщику
0х02000000Данную секцию можно отбросить, так как она не используется программой, после того как последняя загружена. Чаще всего встречается отбрасываемая секция — это секция базовых поправок (.reloc)
0х10000000Данная секция является совместно используемой. При использовании с DLL данные в такой секции используются совместно всеми процессами, использующими эту DLL. По умолчанию секции данных не являются совместно используемыми, т.е. каждый процесс, использующий DLL, имеет свою собственную отдельную копию такой секции данных. Говоря более техническим языком, совместно используемая секция дает указание менеджеру памяти устанавливать отображение страниц для этой секции так, что все процессы, использующие DLL, ссылаются на одну и ту же физическую страницу в памяти. Чтобы сделать секцию совместно используемой, установите атрибут SHARED во время компоновки. Например: LINK/SECTION:MYDATA,RWS... указывает компоновщику, что секция с названием MYDATA должна быть доступной для чтения, записи и совместно используемой. По умолчанию сегменты данных DLL Borland C++ имеют атрибуты совместного использования
0х20000000Данная секция является исполняемой. Этот флаг обычно устанавливается каждый раз, когда устанавливается флаг "Программа" (Contains Code) (0х00000020)
0х40000000Данная секция предназначена для чтения. Этот флаг почти всегда установлен для секций ЕХЕ-файлов
0х80000000Данная секция предназначена для записи. Если этот флаг не установлен в секции ЕХЕ-файла, загрузчик должен отметить отображенные в память страницы как предназначенные только для чтения или только для исполнения. Типичные секции с этим атрибутом — это .data и .bss

Таблица 8.1. Флаги COFF-секций

Интересно отметить, чего не хватает в информации, хранящейся в каждой секции. Во-первых, следует обратить внимание на отсутствие любых атрибутов PRELOAD. Файловый формат NE позволяет пользователю определять атрибуты PRELOAD для сегментов, которые должны быть загружены сразу во время загрузки модуля. Файловый формат OS/2 2.0 LX имеет нечто похожее, что позволяет пользователю давать указание о том, что предварительно должно быть загружено до восьми страниц. РЕ-формат, напротив, не имеет ничего подобного. Исходя из этого, приходится заключить, что Microsoft абсолютно уверена в исполнимости требований загрузки страниц для своих реализаций Win32.

В РЕ-формате также отсутствует таблица поиска промежуточных страниц. Эквивалент IMAGE_SECTION_HEADER в файловом формате OS/2 LX не указывает непосредственно, где находятся в файле данные и программный код секции. Вместо этого файл формата OS/2 LX содержит таблицу поиска страниц, определяющую атрибуты и расположение в файле определенных диапазонов страниц внутри секции. РЕ-формат обходится без всего этого и гарантирует, что данные из секции будут храниться непрерывно в файле. Сравнивая два формата, можно сказать, что LX-метод более гибок, тогда как стиль РЕ намного проще в работе. Имея опыт написания программ просмотра файлов и дизассемблеров для обоих форматов, я могу лично поручиться за это!

Другим благоприятным отличием РЕ-формата от более старого NE-формата является то, что расположения элементов хранятся в виде простых смещений типа DWORD. В NE-формате расположение практически любого элемента хранилось в виде величины сектора. Чтобы посчитать действительное файловое смещение, нужно было сначала найти размер выравнивания в заголовке NE-файла и перевести его в размер сектора (обычно 16 или 512 байт). Затем нужно было умножить размер сектора на указанное смещение сектора, чтобы получить действительное файловое смещение. Если что-нибудь по случайности не хранится в виде секторного смещения в NE-файле, оно, вероятно, хранится как смещение относительно заголовка NE-файла. Ввиду того, что заголовок NE-файла не находится в начале файла, пользователю приходится привлекать в свою программу файловое смещение заголовка NE-файла. В противоположность этому РЕ-файлы определяют положение различных элементов, используя простые смещения относительно того положения, в которое файл был отображен в памяти. В общем, с РЕ значительно проще работать, чем с форматами NE, LX или LE (при условии, что можно использовать отображаемые в память файлы).

Часто встречающиеся секции

После того как читатель получил общее представление о том, что такое секция и как секции размещаются, можно перейти к более глубокому изучению секций, часто встречающихся в ЕХЕ- и объектных файлах. И хотя этот перечень ни в коем случае нельзя назвать исчерпывающим, он, тем не менее, включает секции, с которыми пользователь сталкивается ежедневно (даже если он об этом и не подозревает). Секции представлены в порядке важности и в соответствии с вероятной частотой их появления.

Секция .text

В этой секции собран весь программный код общего назначения, генерируемый компилятором или ассемблером. Поскольку РЕ-файлы работают в 32-разрядном режиме и не привязаны к 16-разрядным сегментам, нет необходимости разбивать программный код из разных исходных файлов по разным секциям. Вместо этого компоновщик объединяет все секции .text из различных объектных файлов в одну большую секцию .text в ЕХЕ-файле. Компилятор Borland C++ помещает весь программный код в сегмент с названием CODE. Таким образом, РЕ-файлы, созданные с помощью Borland C++, имеют секцию с названием CODE вместо секции .text. (См. в этой главе раздел "Секции CODE и .icode Borland" для уточнения деталей.)

Я был удивлен, обнаружив дополнительный программный код в секции .text, помимо того, который я создал компилятором или использовал из библиотек поддержки выполнения программы. В РЕ-файле, в случае вызова функции из другого модуля (например, GetMessage() из USER32.DLL), инструкция CALL, сгенерированная компилятором, не передает управление непосредственно данной функции в DLL. Вместо этого инструкция CALL передает управление команде JMP DWORD PTR[XXXXXXXX], также находящейся в секции .text. Команда JMP перескакивает к адресу, хранящемуся в двойном слове в секции .idata. Это двойное слово в секции .idata содержит настоящий адрес точки входа функции операционной системы, как показано на рис. 8.4.

После некоторых размышлений я понял, почему вызовы DLL реализованы таким образом. Стянув все вызовы данной функции DLL в одно место, загрузчик не будет "латать" каждую инструкцию, вызывающую DLL. Все, что остается загрузчику, это поместить правильный адрес целевой функции в двойное слово в секции .idata. Не нужно "латать" никаких инструкций CALL. Это представляет большое отличие от ситуации с NE-файлами, в которых каждый сегмент содержит перечень привязок, которые должны применяться к сегменту. Если какой-нибудь сегмент вызывает данную функцию из DLL 20 раз, загрузчику нужно будет 20 раз копировать адрес функции в этот сегмент. Недостатком РЕ-метода является то, что пользователь не может инициализировать переменную истинным адресом функции DLL. Например, пользователь может полагать, что нечто вроде:

FARPROC pfnGetMessage = GetMessage;

поместит адрес GetMessage в переменную pfnGetMessage. В Win 16 это сработает, а в Win32 — нет. В Win32 переменная pfnGetMessage в итоге будет содержать адрес переходника JMP DWORD PTR [ХХХХХХХХ] в секции .text, о котором было упомянуто раньше. Если бы пользователь захотел сделать вызов с помощью указателя функции, то все произошло бы так, как пользователь и ожидал. Если вы захотите, однако, прочитать байты в начале GetMessage(), вам это не удастся (если вы не проделали дополнительной работы, проследовав за "указателем" .idata самостоятельно). Я вернусь к обсуждению этой темы позже в разделе "Импорт РЕ-файлов".


Рис. 8.4. Вызов импортируемых функций из РЕ-файла

После того как я написал начальный вариант этой главы, появился Visual C++ 2.0; эта версия содержала новинку в вызове импортированных функций. Если заглянуть в системные файлы заголовков из Visual C++ 2.0 (например, WINBASE.H), можно обнаружить отличие от заголовков Visual C++ 1.0. В Visual C++ 2.0 прототипы функций операционной системы в системных DLL включают __declspec(dllimport) как часть их определения. Оказалось, что __declspec(dllimport) имеет полезное свойство при вызове импортированных функций. Когда пользователь вызывает импортированную функцию, прототипированную с помощью __declspec(dllimport), компилятор не создает вызов инструкции JMP DWORD PTR[XXXXXXXX] где-нибудь еще в модуле. Вместо этого компилятор генерирует вызов функции в виде CALL DWORD PTR[XXXXXXXX]. Адрес [ХХХХХХХХ] находится в секции .idata. Это тот же самый адрес, который был бы использован, если бы использовался наш старый знакомый JMP DWORD PTR[XXXXXXXX]. Насколько я знаю, все версии Borland C++, вплоть до 4.5, не имеют этого свойства.

Секции Borland CODE и .icode

Компилятор и компоновщик Borland C++ не работают с COFF-форматом объектных файлов. Вместо этого Borland предпочел придерживаться 32-разрядной версии формата Intel OMF. Хотя Borland мог бы заставить компилятор генерировать сегменты с именем .text, эта фирма предпочла CODE в качестве имени сегмента по умолчанию. Чтобы определить имя секции в РЕ-файле, компоновщик Borland (TLINK32.EXE) извлекает имя сегмента из объектного файла и обрезает его до 8 символов (в случае необходимости). Из-за этого РЕ-файлы Borland C++ имеют секцию CODE, а не секцию .text.

Разница в именах секций, конечно, не является главным, — существует более важное отличие в том, как Borland РЕ-файлы компонуются с другими модулями. Как было сказано раньше, при обсуждении секции .text, все вызовы объектных файлов идут через переходник JMP DWORD PTR [ХХХХХХХХ]. В системе Microsoft этот переходник приходит в ЕХЕ-файл из секции .text импортируемой библиотеки. Менеджер библиотек создает импортируемую библиотеку (и переходник), когда пользователь присоединяет внешнюю DLL. В результате компоновщик не обязан "знать", как создавать эти переходники самому. Библиотека импорта — это в действительности только некоторое количество программного кода и данных для компоновки в РЕ-файл.

Система оперирования с импортированными функциями Borland иная и представляет просто обобщение действий, которые проводились для 16-разрядных NE-файлов. Библиотеки импорта, используемые компоновщиком Borland, в действительности представляют перечень имен функций и DLL, в которых они находятся. Таким образом, TLINK32 отвечает за определение того, какие привязки предназначены для внешних DLL, и за генерацию соответствующего переходника JMP DWORD PTR[XXXXXXXX]. В Borland C++ 4.0 TLINK32 хранит переходники, которые он создает, в секции с именем .icode. В Borland C++ 4.02 TLINK32 изменен, чтобы собрать все переходники JMP DWORD PTR[XXXXXXXX] в секции CODE.

Секция .data

Как по умолчанию программный код попадает в секцию .text, так и инициализированные данные попадают в секцию .data. Инициализированные данные состоят из тех глобальных и статических переменных, которые были проинициализированы во время компиляции. Они также включают строковые литералы (например, строку "Hello World" в программе C/C++). Компоновщик объединяет все секции .data из разных объектных и LIB-файлов в одну секцию .data в ЕХЕ-файле. Локальные переменные расположены в стеке цепочки и не занимают места в секциях .data и .bss.

Секция DATA

Borland C++ использует по умолчанию имя DATA для секции данных. Она эквивалентна секции .data в компиляторе Microsoft (см. предыдущий пункт "Секция .data").

Cекция .bss

В секции .bss хранятся неинициализированные статические и глобальные переменные. Компоновщик объединяет все секции .bss из разных объектных и LIB-файлов в одну секцию .bss в ЕХЕ-файле. В таблице секций поле RawDataOffset для секции .bss устанавливается в 0, показывая, что эта секция не занимает никакого места в файле. TLINK32 не создает секцию .bss. Вместо этого он расширяет виртуальный размер секции DATA так, чтобы вместить неинициализированные данные.

Секция .CRT

Еще одна секция для инициализированных данных, используемая библиотеками поддержки выполнения программы Microsoft C/C++ (отсюда и название .CRT — C/C++ runtime libraries). Данные из этой секции используются для таких целей, как вызов конструкторов статических классов C++ перед вызовом main или WinMain.

Секция .rsrc

Секция .rsrc содержит ресурсы модуля. На ранних стадиях развития NT выходной .RES-файл 16-разрядного RC.EXE имел формат, который не воспринимался компоновщиком Microsoft. Программа CVTRES переводила эти .RES-файлы в объектные файлы COFF-формата, помещая данные ресурсов в секцию .rsrc внутри объектного файла. После этого компоновщик мог рассматривать объектный файл ресурсов как еще один объектный файл для компоновки, что позволяет компоновщику не вникать во что-либо особенное о ресурсах. Более современные компоновщики Microsoft оказались способными обрабатывать файлы .RES непосредственно. Я расскажу о формате секции ресурсов позже в этой главе, в разделе "Ресурсы РЕ-файлов".

Секция .idata

Секция .idata содержит информацию о функциях (и данных), которые модуль импортирует из других DLL. Эта секция эквивалентна справочной таблице модуля в NE-файле. Коренное отличие состоит в том, что каждая функция, импортируемая РЕ-файлом, перечислена в этой секции. Чтобы отыскать эквивалентную информацию в NE-файле, пользователю пришлось бы рыться в поправках в конце исходных данных для каждого из сегментов. Я расскажу более подробно о формате таблицы импорта позже в этой главе, в разделе "Импорт РЕ-файлов".

Секция .edata

Секция .edata представляет перечень функций и данных, которые РЕ-файл экспортирует для использования другими модулями. Ее эквивалент для NE-файла — это комбинация таблицы входа, таблицы резидентных имен и таблицы нерезидентных имен. В отличие от Win 16, здесь редко возникает необходимость экспортировать что-либо из ЕХЕ-файлов, так что обычно секцию .edata можно увидеть только в DLL. Исключением являются ЕХЕ-файлы, созданные Borland C++, которые, по-видимому, всегда экспортируют функцию (_GetExceptDLLinfo) для внутреннего использования библиотекой поддержки исполнения программы.

Формат таблицы экспорта обсуждается позже в разделе "Экспорт РЕ-фаилов" в этой главе. При использовании средств Microsoft данные из секции .edata попадают в РЕ-файл через файл .ЕХР. Другими словами, компоновщик не создает эту информацию сам. Вместо этого он полагается на менеджера библиотек (LIB32), сканирующего OBJ-файлы и создающего файл .ЕХР, который компоновщик добавляет в свой перечень модулей для компоновки. Да, именно так! Эти надоедливые файлы .ЕХР — в действительности всего лишь OBJ-файлы с другим расширением. Используя программу PEDUMP (представлена позже в этой главе) с ключом /S (показать таблицу символов), можно увидеть функции, экспортируемые через файлы .ЕХР.

Секция .reloc

Секция .reloc содержит таблицу базовых поправок (base relocation). Базовая поправка — это настройка по отношению к инструкции или значению инициализированной переменной; ЕХЕ-файлы или DLL нуждаются в такой поправке, если загрузчик не может загрузить файл по адресу, который предполагался компоновщиком. Если загрузчику удается загрузить отображение по указанному компоновщиком базовому адресу, загрузчик игнорирует поправочную информацию в этой секции.

Если вам хочется попытать счастья и вы надеетесь, что загрузчик всегда сможет загрузить отображение по указанному компоновщиком базовому адресу, используйте ключ /FIXED, чтобы компоновщик удалил эту информацию. Хотя это и сохраняет место в исполняемом файле, однако это же может сделать данный файл неработающим на других платформах Win32. Пусть, например, вы создали ЕХЕ-файл для NT и расположили его по адресу 0х10000. Если вы дали указание компоновщику удалить поправки, данный ЕХЕ-файл не будет работать в Windows 95, так как там адрес 0х10000 не доступен (наименьший адрес загрузки в Windows 95 — 0х400000, т.е. 4 Мбайт).

Необходимо отметить, что инструкции JMP и CALL, генерируемые компилятором, используют смещения относительно этих инструкций, а не действительные смещения в 32-разрядном сегменте. Если отображение необходимо загрузить по базовому адресу, отличному от указанного компоновщиком, не нужно изменять эти инструкции, поскольку они используют относительную адресацию. В результате поправок не так много, как могло бы показаться. Поправки обычно требуются только для инструкций, использующих 32-разрядные смещения для данных. Допустим, имеются следующие объявления глобальных переменных:

int i;
int *ptr = &i;

Если компоновщик указал в качестве базового адреса отображения 0х10000, адрес переменной i будет в итоге содержать что-нибудь наподобие 0х12004. В памяти, используемой под указатель ptr, компоновщик поместит значение 0х12004, поскольку это— адрес переменной i. Если загрузчик решит (по каким-либо причинам) загрузить файл по базовому адресу 0х70000, адресом переменной i будет в таком случае 0х72004. Однако значение переменной ptr перед инициализацией теперь будет неправильным, так как / сейчас находится на 0х60000 байт выше в памяти.

Вот здесь-то поправочная информация и вступает в игру. Секция .reloc — это перечень перемещений, т.е. мест в отображении, в которых необходимо принимать в учет различие между принятым компоновщиком адресом загрузки и реальным адресом загрузки. Я расскажу подробнее о поправках в разделе "Базовые поправки РЕ-файлов".

Секция .tls

Когда используется директива компилятора "__declspec(thread)", определяемые данные не попадают ни в секцию .data, ни в секцию .bss. Вместо этого их копия в итоге оказывается в секции .tls. Имя секции .tls происходит от thread local storage (локальная память потока). Эта секция связана с семейством функций TlsAlloc().

Локальную память потока можно представить как отдельный набор глобальных переменных для каждого потока. Это означает, что каждый поток может иметь свой набор величин статических данных, однако программный код использует эти данные, безотносительно к тому, какой поток исполняется. Рассмотрим программу, имеющую несколько потоков, которые работают над одной и той же задачей, т.е. исполняют один и тот же программный код. Если пользователь объявил переменную локального хранения потока, например:

__declspec(thread) int i = 0; //Это объявление глобальной переменной.

то каждый поток будет иметь свою собственную копию переменной i.

Также возможен явный запрос на использование локальной памяти потока во время исполнения программы с помощью функций TlsAlloc, TlsSetValue и TlsGetValue. (В главе 3 подробно рассказано о функциях TlsXXX.) В большинстве случаев гораздо проще объявлять данные в программе с помощью __declspec (thread), чем распределять память для потока и запоминать указатель на нее в слоте, выделенном функцией TlsAlloc().

Необходимо упомянуть об одной отрицательной стороне секции .tls и переменных __declspec (thread). В NT и Windows 95 механизм локального хранения потока не будет работать для DLL, если эта DLL загружена динамически с помощью LoadLibrary(). Для ЕХЕ или неявно загруженной DLL все будет работать прекрасно. Если нет возможности явно скомпоновать DLL, а для потока необходима его локальная память, то придется ее распределить динамически с помощью TlsAlloc() и TlsGetValue(). Необходимо отметить, что на самом деле блоки памяти для потока не хранятся в секции .tls во время исполнения программы. Другими словами, переключая потоки, менеджер памяти не изменяет физическую страницу памяти, отображенную в секцию .tls модуля. Вместо этого секция .tls представляет просто данные, используемые для инициализации настоящих блоков данных для потока. Инициализация областей данных для потока производится совместными усилиями операционной системы и библиотек поддержки выполнения программы. Это требует дополнительных данных — каталога TLS, хранящегося в секции .rdata.

Секция .rdata

Секция .rdata имеет, как минимум, четыре предназначения. Во-первых, в ЕХЕ-файлах, созданных с помощью компоновщика Microsoft Link, секция .rdata содержит каталог отладки (в объектных файлах такого каталога нет). В ЕХЕ-файлах, созданных с помощью компоновщика TLINK32, каталог отладки находится в секции .debug. Каталог отладки представляет массив структур IMAGE_DEBUG_DIRECTORY. Эти структуры содержат информацию о типе, размере и местонахождении различных видов отладочной информации, содержащейся в файле. Есть три главных вида отладочной информации: CodeView, COFF и FPO. На рис. 8.5 показан вывод типичного каталога отладки с помощью программы PEDUMP.

Type       Size      Address   FilePtr   Charactr  TimeData  Version
--------------------------------------------------------------------
COFF       000065C5  00000000  00009200  00000000  2CF8CF3D  0.00
(unknown)  00000114  00000000  0000F7C8  00000000  2CFSCF3D  0.00
FPO        000004B0  00000000  0000F8DC  00000000  2CF8CF3D  0.00
CODEVIEW   0000B0B4  00000000  0000FD8C  00000000  2CFBCF3D  0.00

Рис. 8.5. Типичный каталог отладки

Каталог отладки не обязательно должен находиться в начале секции .rdata. Для того чтобы обнаружить начало каталога отладки, следует использовать RVA, содержащийся в седьмой строке (IMAGE_DIRECTORY_ENTRY_DEBUG) каталога данных. (Каталог данных находится в конце заголовка РЕ-файла.) Чтобы определить количество входов в каталоге отладки для компоновщика Microsoft, нужно разделить размер этого каталога (находится в поле размера в указанной выше строке каталога данных) на размер структуры IMAGE_DEBUG_DIRECTORY. В случае же компоновщика TLINK32 соответствующее поле размера уже содержит количество строк каталога отладки, а не общую длину в байтах. Программа PEDUMP обрабатывает обе ситуации.

Другой важной частью секции .rdata является строка описания. Если пользователь определяет элемент DESCRIPTION в файле .DEF в своей программе, то в секции .rdata появляется строка описания. В NE-формате строка описания всегда является первой строкой нерезидентной таблицы имен. Строка описания предназначена для хранения полезного текста, описывающего файл. К сожалению, я не обнаружил простого способа найти ее. Я встречал РЕ-файлы, в которых строка описания находилась перед таблицей отладки, и файлы, в которых она была после таблицы отладки. Мне неизвестен ни один последовательный метод нахождения строки описания (даже более того — определения того, имеется ли она вообще).

Кроме того, секция .rdata используется для GUID при OLE-программировании. Библиотека импорта UUID.LIB содержит набор 16-разрядных GUID, используемых в случаях ID-интерфейсов. Эти GUID в итоге оказываются в секции .rdata ЕХЕ-файла или DLL.

Последнее применение секции .rdata, о котором мне известно,— это место для хранения каталога TLS (ThreadLocalStorage — локальная память цепочки). Каталог TLS — это специальная структура данных, используемая библиотекой поддержки выполнения программы для явного обеспечения локальной памяти цепочки для переменных, объявленных в программе. Формат каталога TLS можно найти на CD-ROM MSDN (Microsoft Developer Network) в спецификации Portable Executable and Common Object File Format. Наибольший интерес в каталоге TLS представляют указатели начала и конца копии данных, используемых для инициализации каждого блока локальной памяти цепочки. RVA каталога TLS находится в элементе IMAGE_DIRECTORY_ENTRY_TLS в каталоге данных заголовка РЕ-файла. Реальные данные, используемые для инициализации блоков TLS, можно найти в секции .tis (описана выше).

Секции .debug$S и .debug$T

Секции .debug$S и .debug$T есть только в COFF-объектных файлах; и они содержат информацию о символах CodeView и их типах. Названия этих секций произошли от названий сегментов, используемых для этой цели предыдущими компиляторами Microsoft ($$SYMBOLS и $$TYPES). Единственное назначение секции .debug$T — хранить путь к файлу .PDB, содержащему информацию CodeView о типах для всех объектных файлов проекта. Информацию CodeView для создаваемого ЕХЕ-файла компоновщик помещает в файл .PDB.

Секция .drective

Эта секция есть только в объектных файлах. Она содержит текст команд для компоновщика. Например, следующие строки появлялись в секции .drective в любом объектном файле, который я компилировал с помощью компилятора Microsoft Visual C++:

-defaultlib:LIBC -defaultlib:OLDNAMES.

При использовании в программе __declspec(export) компилятор просто вырабатывает эквивалент командной строки в секции .drectve (например, exportMyFunction).

Секции, содержащие символ $ (для LIB и объектных файлов)

В объектных файлах секции, содержащие $ (например, .idata$2) обрабатываются компоновщиком по-особому. Компоновщик объединяет все секции, имеющие одинаковые символы в имени перед символом $. Именем получившейся секции считается то, что находится перед символом $. Таким образом, если компоновщик встречает секции с именами .idata$2 и ,idata$6, он объединяет их в одну секцию с именем .idata.

Упорядочение объединяемых секций происходит в соответствии с символами после $. Компоновщик соблюдает лексический порядок, так что секция .idata$2 будет идти перед секцией .idata$6, а секция .data$A — перед секцией .data$B.

Для чего же используется символ $? Чаще всего он используется библиотеками импорта, которые в секциях .idata$x хранят различных порции суммарной секции .idata. Это достаточно интересно. Компоновщик не должен создавать секцию .idata с нуля. Вместо этого итоговая секция .idata создается из секций объектных или LIB-файлов, которые компоновщик рассматривает как любую другую секцию, подлежащую компоновке.

Разнообразные секции

Работая с программой PEDUMP, я время от времени встречал и другие секции. Например, Windows 95 GDI32.DLL содержит секцию данных под названием _GPFIX, назначение которой предположительно связано с обработкой ошибок GP.

Отсюда можно извлечь двойной урок. Не обязательно придерживаться использования только стандартных секций, производимых компилятором или ассемблером. Если вам необходима отдельная секция, не бойтесь использовать ее. При работе с компилятором Microsoft C/C++ пользуйтесь #pragma code_seg и #pragma data_seg. Пользователи компилятора Borland могут использовать #pragma codeseg и #pragma dataseg. В ассемблере можно просто создать 32-разрядный сегмент с именем, отличным от имен стандартных секций. Компоновщик TLINK32 объединяет сегменты программного кода одного класса, так что следует либо присваивать каждому сегменту программного кода свое уникальное имя класса, либо отключить упаковку сегментов программного кода. Другой урок: необычные имена секций часто позволяют глубже взглянуть на назначение и реализацию конкретного РЕ-файла.

Импортирование в РЕ-файлах

Раньше я описывал, каким образом вызовы функций из внешних DLL не обращаются к этим DLL непосредственно. Вместо этого инструкция CALL передает управление инструкции JMP DWORD PTR[XXXXXXXX] где-то в секции .text исполняемого файла (или в секции .icode, если используется Borland C++ 4.0). Если используется _declspec(dllimport) в Visual C++, вызов функции принимает вид CALL DWORD PTR[XXXXXXXX]. В обоих случаях адрес, который ищет инструкция JMP или CALL, хранится в секции .idata. Инструкция JMP или CALL передает управление по этому адресу, являющемуся предполагаемым адресом цели. Если вы не все поняли, вернитесь к рис. 8.4.

Перед загрузкой в память информация, хранящаяся в секции .idata РЕ-файла, содержит информацию, необходимую для того, чтобы загрузчик мог определить адреса целевых функций и пристыковать их к отображению исполняемого файла. После загрузки секция .idata содержит указатели функций, импортируемых ЕХЕ-файлом или DLL. Заметьте, что все массивы и структуры, обсуждаемые в этом разделе, содержатся в секции .idata.

Секция .idata (или таблица импорта, как мне больше нравится ее называть) начинается с массива, состоящего из IMAGE_IMPORT_DESCRIPTOR. Каждый элемент (IMAGE_IMPORT_DESCRIPTOR) соответствует одной из DLL, с которой неявно связан данный РЕ-файл. Количество элементов в массиве нигде не учитывается. Вместо этого последняя структура массива IMAGE_IMPORT_DESCRIPTOR имеет поля, содержащие NULL. Структура IMAGEJMPORT_DESCRIPTOR имеет следующий формат.

DWORD Characteristics/OriginalFirstThunk

В этом поле содержится смещение (RVA) массива двойных слов. Каждое из этих двойных слов в действительности является объединением IMAGE_THUNK_DATA. Каждое двойное слово IMAGE_THUNK_DATA соответствует одной функции, импортируемой данным ЕХЕ-файлом или DLL. Я опишу формат IMAGE_THUNK_DATA DWORD немного позже. Если используется утилита BIND, то этот массив двойных слов не изменяется, а модифицируется массив двойных слов FirstThunk (описан вкратце).

DWORD TimeDateStamp

Отметка о времени и дате, указывающая, когда был создан данный файл. Обычно это поле содержит 0. Тем не менее утилита Microsoft BIND обновляет это поле датой и временем из DLL, на которую указывает данный 1MAGE_IMPORT_DESCRIPTOR.

DWORD ForwarderChain

Это поле имеет отношение к передаче, когда одна DLL передает ссылку на какую-то свою функцию другой DLL. Например, в Windows NT KERNEL32.DLL посылает несколько своих экспортируемых функций NTDLL.DLL. Приложение может посчитать это вызовом функции в KERNEL32.DLL, но в итоге это будет вызов в NTDLL.DLL. Это поле содержит указатель в массив FirstThunk (описан вкратце). Функция, указанная этим полем, будет послана в другую DLL. К сожалению, формат посылки функции лишь вкратце описан в документации Microsoft. За дополнительной информацией о посылке обращайтесь к разделу "Передача экспорта" в этой главе.

DWORD Name

Это RVA строки символов ASCII, оканчивающейся нулем и содержащей имена импортируемых DLL (например, KERNEL32.DLL или USER32.DLL).

PIMAGE_THUNK_DATA FirstThunk;

RVA-смещение массива двойных слов IMAGE_THUNK_DATA. В большинстве случаев двойное слово рассматривается как указатель на структуру IMAGE_IMPORT_BY_NAME. Однако можно импортировать функцию также и по порядковому номеру.

Важными частями IMAGE_IMPORT_DESCRIPTOR являются имя импортируемой DLL и два массива элементов IMAGE_THUNK_DATA DWORD. Каждое двойное слово IMAGE_THUNK_DATA соответствует одной импортируемой функции. В ЕХЕ-файлах оба эти массива (на них указывают поля Characteristics и FirstThunk) идут параллельно друг другу и оканчиваются элементом-указателем NULL в конце каждого массива.

Зачем нужны два параллельных массива указателей на структуры IMAGE_THUNK_DATA? Первый массив (на него указывает поле Characteristics) оставляется неизменным. Иногда его называют таблицей имен-намеков (hint-name table). Второй массив, на который указывает поле FirstThunk в IMAGE_IMPORT_DESCRIPTOR, переписывается РЕ-загрузчиком. Загрузчик последовательно перебирает IMAGE_THUNK_DATA и находит адрес функции, на которую ссылается последний. Затем загрузчик записывает в двойное слово IMAGE_THUNK_DATA адрес импортируемой функции.

Раньше я упоминал о том, что вызовы функций DLL происходят через переходник "JMP DWORD PTR[CXXXXXXX]". [XXXXXXXX] в переходнике ссылается на один из элементов массива FirstThunk. Поскольку массив, состоящий из IMAGE_THUNK_DATA, переписывается загрузчиком и в итоге содержит адреса всех импортируемых функций, он называется "Таблица адресов импорта". Рис. 8.6 показывает оба этих массива.

Для пользователей Borland есть некоторая дополнительная тонкость в этом описании. В РЕ-файле, созданном TLINK32, отсутствует один из массивов. В таких файлах поле Characteristics в IMAGE_IMPORT_DESCRIPTOR (т.е. в массиве имен-намеков) равно нулю (очевидно, загрузчики Win32 не нуждаются в этом массиве). Таким образом, во всех РЕ-файлах вообще обязан быть только массив, на который указывает поле FirstThunk (таблица адресов импорта).

На этом можно было бы и закончить изложение, если бы при написании программы PEDUMP я не столкнулся с одной интересной проблемой. В постоянной погоне за оптимизацией Microsoft "оптимизировала" массивы IMAGE_THUNK_DATA в системных DLL под Windows NT (например, KERNEL32.DLL). После этой оптимизации IMAGE_THUNK_DATA не содержит информации, необходимой для нахождения импортируемой функции. Вместо этого двойные слова IMAGE_THUNK_DATA уже содержат адреса импортируемых функций. Другими словами, для загрузчика нет необходимости выискивать адреса функций и переписывать массив переходников с адресами импортируемых функций. Массив уже содержит адреса импортируемых функций еще до загрузки. (Утилита BIND из Win32 SDK осуществляет такую оптимизацию.) К сожалению, это вызывает трудности при работе программ просмотра, предполагающих, что массив содержит смещения RVA для элементов IMAGE_THUNK_DATA. Вы можете подумать: "А почему не использовать таблицу имен-намеков?" Это было бы идеальным решением, если бы таблица имен-намеков существовала в файлах Borland. Программа PEDUMP работает в обеих ситуациях, однако, по понятным причинам, результат получается несколько беспорядочным.


Рис. 8.6. Как РЕ-фаил импортирует функции

Поскольку таблица адресов импорта находится обычно в доступной для записи секции, то не представляет труда перехватить вызовы, сделанные ЕХЕ или DLL другой DLL. Для этого нужно просто "залатать" соответствующий элемент таблицы адресов импорта так, чтобы он указывал на желаемую функцию-перехватчик. Не нужно модифицировать код ни в вызывающей, ни в вызываемой функции. Эта возможность представляется очень полезной. В главе 10 я расскажу о программе-шпионе Win32 API, которая опирается на этот прием.

Интересно заметить, что в РЕ-файлах Microsoft таблица импорта не полностью синтезируется компоновщиком. Вместо этого все элементы, необходимые для вызова функций из других DLL, располагаются в библиотеке импорта. При компоновке DLL менеджер библиотеки (LIB.EXE) сканирует компонуемые объектные файлы и создает библиотеку импорта. Эта библиотека отличается от библиотек импорта, используемых 16-разрядными компоновщиками NE-файлов. Библиотека импорта, создаваемая 32-разрядными LIB-файлами, имеет секцию .text и несколько секций .idata$. Секция .text в библиотеке содержит переходник JMP DWORD PTR[XXXXXXXX], о котором я упоминал раньше. Имя этого переходника хранится в таблице символов объектного файла. Имя символа идентично имени экспортируемой DLL функции (например _DispatchMessage@4).

Одна из секций .idata$ в библиотеке импорта содержит двойное слово — переходник. Другая секция .idata$ резервирует место для "номера намека", за которым следует имя импортируемой функции. Этих два поля составляют структуру IMAGE_IMPORT_BY_NAME. При компоновке РЕ-файла, использующего библиотеку импорта, секции библиотеки импорта добавляются к перечню секций объектного файла, подлежащих обработке компоновщиком. Ввиду того, что переходник в библиотеке импорта имеет такое же имя, как и импортируемая функция, компоновщик воспринимает переходник как импортируемую функцию и настраивает вызовы импортируемой функции так, чтобы они указывали на переходник. Поэтому переходник в библиотеке импорта трактуется как импортируемая функция.

Помимо обеспечения порций кода для переходника импортируемой функции библиотека импорта поставляет части секции .idata (или таблицы импорта) РЕ-файла. Эти части поступают из разных секций .idata$, помещаемых библиотекарем в библиотеку импорта. Короче говоря, компоновщик не различает импортированные функции и функции из другого объектного файла. Компоновщик просто следует своим предписаниям при создании и объединении секций, и все происходит вполне естественно.

IMAGE_THUNK_DATA DWORD

Как я уже упоминал, каждое двойное слово IMAGE_THUNK_DATA соответствует импортируемой функции. Интерпретация двойного слова зависит от того, был ли файл уже загружен в память и была ли функция импортирована по имени или по номеру (импортирование по имени встречается чаще).

При импортировании функции по номеру (что бывает редко) старший бит (0х80000000) двойного слова IMAGE_THUNK_DATA данного ЕХЕ-файла устанавливается в 1. Например, рассмотрим IMAGE_THUNK_DATA со значением 0х80000112 в массиве GDI32.DLL. Этот IMAGE_THUNK_DATA импортирует 112-ю экспортируемую функцию из GDI32.DLL. Проблема при импортировании по номеру состоит в том, что фирма Microsoft не позаботилась, чтобы поддержать соответствие номеров экспорта функций Win32 API для Windows NT, Windows 95 и Win32s.

Если функция импортируется по имени, то ее двойное слово IMAGE_THUNK_DATA содержит RVA структуры IMAGE_IMPORT_BY_NAME. Это простая структура выглядит следующим образом.

WORD Hint

Наилучшая догадка о том, какой номер экспорта у импортируемой функции. В отличие от NE-файлов эта величина не обязана быть верной. Загрузчик использует ее в качестве начального предполагаемого значения для бинарного поиска экспортируемой функции.

BYTE[?]

Строка ASCIIZ с именем импортируемой функции. Окончательная интерпретация двойного слова IMAGE_THUNK_DATA происходит после того, как РЕ-файл загружен загрузчиком Win32. Загрузчик Win32 использует исходную информацию из двойного слова IMAGE_THUNK_DATA для поиска адреса импортируемой функции (либо по имени, либо по номеру). Затем загрузчик записывает в двойное слово IMAGE_THUNK_DATA адрес импортируемой функции.

Сравнение IMAGE_IMPORT_DESCRIPTOR и IMAGE_THUNK_DATA

Теперь, после того как вы увидели и структуру IMAGEJMPORTJDESCRIPTOR, и структуру IMAGE_THUNK_DATA, будет просто составить отчет обо всех импортируемых функциях, которые использует ЕХЕ-файл или DLL. Для этого нужно просто перебрать последовательно все элементы массива IMAGE_IMPORT_DESCRIPTOR (каждый из которых соответствует одной импортируемой DLL). Для каждого элемента IMAGE_IMPORT_DESCRIPTOR найдите массив двойных слов IMAGE_THUNK_DATA и интерпретируйте его соответствующим образом. Рис. 8.7 показывает вывод программы PEDUMP для этой операции. (Функции без имен импортируются по номеру.)

Imports Table:
 USER32.dll
 Hint/Name Table: 0001F50C
 TimeDateStamp:   2EB9CE9B
 ForwarderChain:  FFFFFFFF
 First thunk RVA: 0001FC24
 Ordn Name
 268  GetScrollInfo
 133  DispatchMessageA
 333  IsRectEmpty
 431  SendMessageCallbackA
 255  GetMessagePos
 // Остальная часть таблицы опущена...

GDI32.dll
 Hint/Name Table: 0001F178
 TimeDateStamp:   2EB9CE9B
 ForwarderChain:  FFFFFFFF
 First thunk RVA: 0001F890
 Ordn Name
 31   CreateCompatibleDC
 389  SetTextColor
 276  SetBkColor
 99   ExtTextOutA
 9    BitBlt
 // Остальная часть таблицы опущена...

MPR.dll
 Hint/Name Table: 0001F2F8
 TimeDateStamp:   2EAF4824
 ForwarderChain:  FFFFFFFF
 First thunk RVA: 0001FA08
 Ordn Name
 26
 35
 34
 33
 55
 // Остальная часть таблицы опущена...

KERNEL32.dll
 Hint/Name Table: 0001F1CC
 TimeDateStamp:   2EB9DA61
 ForwarderChain:  FFFFFFFF
 First thunk RVA: 0001F8E4
 Ordn Name
 636  SetEvent
 348  GetTimeFormatA
 375  GlobalGetAtomNameA
 301  GetProcAddress
 572  RtlZeroMemory
 // Остальная часть таблицы опущена...

COMCTL32.dll
 Hint/Name Table: 0801FODC
 TimeDateStamp:   2EAD4AE5
 ForwarderChain:  FFFFFFFF
 First thunk RVA: 0001F7F4
 Ordn Name
 152
 21   ImageList_Draw
 354
 352
 28   ImageList_GetIconSize
 // Остальная часть таблицы опущена...

ADVAPI32.dll
 Hint/Name Table: 0001F0A0
 TimeDateStamp:   2EA8A148
 ForwarderChain:  FFFFFFFF
 First thunk RVA: 0001F7B8
 Ordn Name
 149  RegQueryValueA
 119  RegCloseKey
 142  RegOpenKeyExA
 13]  RegEnumKeyExA
 126  RegDeleteKeyA
 // Остальная часть таблицы опущена...

Рис. 8.7. Типичная таблица импорта в ЕХЕ-файле (EXPLORER.EXE)

Экспорт в РЕ-файлах

Противоположностью импорту функций является их экспорт для использования ЕХЕ-файлами или другими DLL. Информация об экспортируемых функциях хранится в секции .edata РЕ-файла. Как правило, ЕХЕ-файлы, созданные Microsoft LINK, ничего не экспортируют и поэтому не имеют секции .edata. ЕХЕ-файлы, созданные с помощью TLINK32, напротив, обычно экспортируют один символ и имеют секцию .edata. Большинство DLL экспортируют функции и имеют секцию .edata. Главными компонентами секции .edata (или, другими словами, таблицы экспорта) являются таблицы имен функций, адреса точек входа и номера экспорта. В NE-файле эквивалентами таблицы экспорта являются таблица элементов, таблица резидентных имен и таблица нерезидентных имен. В NE-файлах эти таблицы хранятся как часть заголовка NE-файла, а не в сегментах или ресурсах.

В начале секции .edata расположена структура IMAGE_EXPORT_DIRECTORY. После этой структуры сразу идут данные, на которые указывают поля структуры IMAGE_EXPORT_DIRECTORY. IMAGE_EXPORT_DIRECTORY выглядит следующим образом.

DWORD Characteristics

Это поле, по-видимому, никогда не используется и всегда устанавливается в 0.

DWORD TimeDateStamp

Отметка о времени и дате, указывающая время создания файла.

WORD MajorVersion;WORD MinorVersion

Эти поля, по-видимому, никогда не используются и всегда устанавливаются в 0.

DWORD Name

RVA строки ASCIIZ с именем этой DLL (например, MYDLL.DLL).

DWORD Base

Начальный номер экспорта для функций, экспортируемых данным модулем. Например, если номера экспортируемых функций 10, 11 и 12, то это поле будет содержать 10.

DWORD NumberOfFunctions

Количество элементов в массиве AddressOfFunctions. Это также число экспортируемых данным модулем функций. Обычно это значение такое же, как и в поле NumberOfNames (см. следующее описание), хотя они могут быть и различными.

DWORD NumberOfNames

Количество элементов в массиве AddressOfNames. Это значение соответствует количеству функций, экспортируемых по имени, которое обычно (хотя и не всегда) равно общему количеству экспортируемых функций.

PDWORD * AddressOfFunctions

Это поле является RVA и указывает на массив адресов функций. Адреса функций — это RVA точек входа для каждой экспортируемой модулем функции.

PDWORD * AddressOfNames

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

PWORD * AddressOfNameOrdinals

Это поле является RVA и указывает на массив слов. По существу, в этих словах хранятся номера экспорта всех экспортируемых из данного модуля по имени функций. Однако не забудьте прибавить начальный номер экспорта, указанный в поле Base (описано выше).

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

Наиболее важным из всех массивов, на которые указывает IMAGE_EXPORT_DIRECTORY, является массив, на который указывает поле AddressOfFunctions. Это массив двойных слов, в котором каждое двойное слово содержит RVA одной из экспортируемых функций. Номер экспорта каждой экспортируемой функции соответствует ее положению в массиве, Так, например, принимая, что номера экспорта начинаются с 1, адрес, по которому хранится адрес функции с номером экспорта, равным 1, содержится в первом элементе массива. Адрес, по которому хранится адрес функции с номером экспорта, равным 2, содержится во втором элементе массива и т.д.

Необходимо помнить о двух моментах относительно массива AddressOfFunctions. Во-первых, нельзя забывать о том, что отсчет номеров экспорта начинается с числа, содержащегося в поле Base структуры IMAGE_EXPORT_DIRECTORY. Так, если поле Base содержит 10, то первое двойное слово в массиве AddressOfFunctions соответствует номеру экспорта 10, второе — 11 и т.д. Во-вторых, следует иметь в виду, что номера экспортов могут иметь пропуски. Допустим, что явно экспортируются две функции с номерами 1 и 3. Несмотря на то, что экспортированы только две функции, массив AddressOfFunctions обязан содержать три элемента. Любые элементы массива, не отвечающие экспортируемым функциям, содержат 0.

Когда загрузчик Win32 связывает вызов функции, экспортируемой по номеру, он выполняет совсем незначительный объем работ. Он просто использует номер функции как индекс в массиве AddressOfFunctions модуля-цели. Конечно, загрузчик должен учесть, что наименьший номер экспорта может быть не равен 1, и должен поправить индексацию соответствующим образом.

Чаще всего ЕХЕ-файлы и DLL под Win32 импортируют функции по имени, а не по номеру. Здесь выходят на сцену два других массива, на которые указывает структура IMAGE_EXPORT_DIRECTORY. Массивы AddressOfNames и AddressOfNameOrdinals существуют для того, чтобы загрузчик мог быстро найти номер экспорта по заданному имени функции. Массивы AddressOfNames и AddressOfNameOrdinals содержат одинаковое количество элементов (заданное в поле NumberOfNames структуры IMAGE_EXPORT_DIRECTORY). Массив AddressOfNames — это массив указателей на имена функций, а массив AddressOfNameOrdinals — массив индексов для массива AddressOfFunctions.

Давайте посмотрим, как загрузчик Win32 обрабатывает вызов функции, импортируемой по имени. Сначала загрузчик будет искать строки, на которые указывает массив AddressOfNames. Допустим, он находит искомую строку в третьем элементе. Затем загрузчик использует найденный индекс для поиска соответствующего элемента в массиве AddressOfNameOrdinals (в данном случае это третий элемент). Последний массив — это просто набор слов, где каждое слово играет роль индекса в массиве AddressOfFunctions. Наконец последний шаг — взять значение в массиве AddressOfNameOrdinals и использовать его в качестве индекса для массива AddressOfFunctions.

В программе на С нахождение адреса функции, импортируемой по имени, будет выглядеть примерно следующим образом:

WORD namelndex = FindIndexOfString(AddressOfNames, "GetMessageA");
WORD function Index = AddressOfNameOrdinaIs[nameIndex];
DWORD functionAddress = AddressOfFunctions[functionlndex - OrdinalBase];

Рис. 8.8 демонстрирует формат секции экспорта и три ее массива.


Рис. 8.8. Типичная таблица экспорта из ЕХЕ-фаила

Рис. 8.9 показывает вывод программой PEDUMP секции экспорта в KERNEL32.DLL.

Name:             KERNEL32.dll
Characteristics:  00000000
TimeDateStamp:    2C4857D3
Version:          0.00
Ordinal base:     00000001
# of functions:   0000021F
# of Names:       0000021F

Entry Pt   Ordn   Name
00005090   1      AddAtomA
00005100   2      AddAtomW
00025540   3      AddConsoleAliasA
00025500   4      AddConsoleAliasW
00026AC0   5      AllocConsole
00001000   6      BackupRead
00001E90   7      BackupSeek
00002100   8      BackupWrite
0002520C   9      BaseAttachCompleteThunk
00024C50   10     BasepDebugDump
// Остальная часть таблицы опущена...

Рис. 8.9. Распечатка секции экспорта для библиотеки KERNEL32.DLL с помощью программы PEDUMP

Если вы просматриваете экспорт в системных DLL (например, KERNEL32.DLL или USER32.DLL), вы можете случайно обнаружить, что часто две функции отличаются только одним символом в конце имени, например CreateWindowExA и CreateWindowExW. Вот так "явно" осуществлена поддержка уникода (Unicode). Функции, оканчивающиеся на А, являются ASCII-совместимыми (или ANSI-) функциями. Функции, оканчивающиеся на W, — это Unicode-версии этих функций. Программируя, пользователь не указывает явно, какую функцию надо вызывать. Вместо этого соответствующая функция выбирается в WINDOWS.H с помощью директивы препроцессора #ifdefs. Это иллюстрируется следующим отрывком из NT WINDOWS.H:

#ifdef UNICODE
#efine DefWindowProc DefWindowProcW
#else
#define DefWindowProc DefWindowProcA
#end if  // !UNICODE

Передача экспорта

Иногда в DLL полезно экспортировать функцию, а ее программный код иметь в другой DLL. При таком сценарии одна DLL может передавать функцию другой DLL. Если загрузчик Win32 встречает вызов передаваемой функции, он разрешает ссылку на функцию в ту DLL, которая содержит настоящий программный код.

Проиллюстрируем сказанное примером. Рассмотрим следующий отрывок из вывода программой PEDUMP Windows NT3.5 KERNEL32.DLL:

00043FC3 335 HeapAlloc (forwarder -> NTDLL.RtlAIlocateHeap) 
00044005 339 HeapFree (forwarder-> NTDLL.RtIFreeHeap) 
0004402C 341 HeapReAlloc (forwarder -> NTDLL.RtlReAllocateHeap) 
0004404D 342 HeapSize (forwarder-> NTDLL.RtlSizeHeap) 
0004466F 442 RtlFiIIMeiriory (forwarder -> NTDLL.RtlFiIIMernory) 
00044691 443 RtIMoveMemory (forwarder -> NTDLL. RtlMoveMemory) 
000446AF 444 RtI Unwind (forwarder-> NTDLL.RtI Unwind) 
000446CD 445 RtlZeroMemory (forwarder -> NTDLL. RtlZeroMemory)

Каждая функция в этом выводе передается функции в NTDLL. Таким образом, программа, вызывающая функцию HeapAlloc, в действительности вызывает функцию RtlAllocateHeap из NTDLL.DLL. Аналогично, вызов HeapFree является в действительности вызовом функции RtlHeapFree из NTDLL.

Каким образом можно узнать, что функция передается? Единственным указанием на то, что функция передается, является наличие ее адреса в таблице экспорта (секция .edata). В этом случае так называемый адрес функции в действительности является RVA строки, содержащей передаваемую DLL и имя функции. Например, в предыдущем выводе RVA для НеарАПос равно Ox43FC3. Смещение Ox43FC3 в KERNEL32.DLL попадает в секцию .edata. Это смещение имеет строка NTDLL.RtlAllocateHeap. Функция DumpExportsSection в программе PEDUMP показывает, как обнаружить передаваемые функции.

Хотя передача экспорта кажется очень приятным свойством, Microsoft не дает описания того, как использовать передачу в пользовательских DLL. До сих пор я встречал использование передачи только одной DLL (вышеупомянутая Windows NT KERNEL32.DLL). Даже несмотря на то, что мне не встречались DLL с передачами экспорта под Windows 95, загрузчик Windows 95, тем не менее, поддерживает это свойство, о чем я рассказывал в главе 3.

Ресурсы РЕ-файла

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

Перемещения по иерархии каталогов ресурсов похожи на перемещения по жесткому диску. Здесь есть главный каталог (корневой), имеющий свои подкаталоги. Подкаталоги имеют свои собственные подкаталоги. В этих подкаталогах находятся файлы. Файлы аналогичны исходным данным ресурсов, содержащим такие элементы, как диалоговые шаблоны. В РЕ-файлах как корневой каталог, так и его подкаталоги являются структурами типа IMAGE_RESOURCE_DIRECTORY. Структура IMAGE_RESOURCE_DIRECTORY имеет следующий формат.

DWORD Characteristics

Теоретически это поле может содержать флаги ресурсов, но, по-видимому, оно всегда равно 0.

DWORD TimeDateStamp

Отметка о времени и дате создания ресурса.

WORD MajorVersion; WORD MinorVersion

Теоретически эти поля могли бы содержать номер версии ресурса. По-видимому, они всегда равны 0.

WORD NumberOfNamedEntries

Количество элементов массива (описан позже), использующих имена и следующих за этой структурой. За дополнительной информацией обращайтесь к описанию поля DirectoryEntries.

WORD NumberOfIdEntries

Количество элементов массива, использующих целые ID и следующих за этой структурой и всеми поименованными элементами. За дополнительной информацией обращайтесь к описанию поля DirectoryEntries.

IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[]

Это поле формально не является частью структуры IMAGE_RESOURCE_DIRECTORY. За этим полем сразу следует массив структур IMAGE_RESOURCE_DIRECTORY_ENTRY. Количество элементов в массиве равно сумме полей NumberOfNamedEntries и NumberOfIdEntries. Элементы каталога, имеющие идентификаторы-имена (а не целые ID), находятся в начале массива.

Элемент каталога может указывать либо на подкаталог (т.е, на другую IMAGE_RESOURCE_DIRECTORY), либо на IMAGE_RESOURCE_DATA_ENTRY, которая описывает, где в файле находятся исходные данные ресурсов. Как правило, необходимо пройти как минимум три уровня каталогов, перед тем как попасть на IMAGE_RESOURCE_DATA_ENTRY для данного ресурса. Каталог верхнего уровня (только один) всегда находится в начале секции ресурсов (.rsrc). Подкаталоги каталога верхнего уровня соответствуют различным типам ресурсов, находящихся в файле. Например, если РЕ-файл включает диалоги, таблицы строк и меню, этими тремя подкаталогами будут соответственно каталог диалогов, каталог таблицы строк и каталог меню. Каждый из этих "типов" подкаталогов будет в свою очередь иметь "ID''-подкаталоги. Для каждого образца заданного типа ресурса будет существовать один ID-подкаталог. Если в приведенном выше примере есть четыре диалоговых окна, каталог диалогов будет иметь четыре ID-подкаталога. Каждый ID-подкаталог будет иметь либо строковое имя (например, MyDialog), либо целый ID, используемый для идентификации ресурса в RC-файле. Рис. 8.10 наглядно иллюстрирует иерархию каталогов ресурсов.


Рис. 8.10. Иерархия ресурсов типичного РЕ-файла

Рис. 8.11 показывает вывод программой PEDUMP ресурсов файла CLOCK.EXE в Windows NT 3.5. На втором уровне отступов можно видеть пиктограммы, меню, диалоги, таблицы строк, пиктограммы групп и ресурсы версий. На третьем уровне — две пиктограммы (с ID 1 и 2), два меню (с именами CLOCK и GENERICMENU), два диалога (один с именем ABOUTBOX, а другой — с целым ID, равным 0х64) и т.д. На четвертом уровне отступов — данные для значка 1 с RVA 0х9754 длиной 0x130 байт. Аналогично данные для меню CLOCK имеют смещение Ох952С и занимают 0хЕА байт.

Resources
ResDir (0) Named:00 ID:06 TimeDate:2E601E3C Vers:0.00 Char:0
   ResDir (ICON) Named:00 ID:02 TimeDate:2E601E3C Vers:0.00 Char:0
      ResDir (1) Named:00 ID:01 TimeDate:2E601E3C Vers:0.00 Char:0
         ID: 0000409 DataEntryOffs: 000001E0
         Offset: 09754 Size: 00130 CodePage: 0
      ResDir (2) Named:00 iD:01 TimeDate:2E601E3C Vers:0.00 Char:0
         ID: 00000409 DataEntryOffs: 000001F0
         Offset: 09884 Size: 002E8 CodePage: 0
   ResDir (MENU) Named:02 ID:00 TimeDate:2E601E3C Vers:0.00 Char:0
      ResDir (CLOCK) Named:00 ID:01 TimeDate:2E601E3C Vers:0.00 Char:0
         ID: 00000409 DataEntryOffs: 00000200
         Offset: 0952C Size: 000EA CodePage: 0
      ResDir (GENERICMENU) Named:00 ID:01 TimeDate:2E601E3C Vets:O,00 Char:0
         ID: 00000409 DataEntryOffs: 00000210
         Offset: 09618 Size: 0003A CodePage: 0
   ResDir (DIALOG) Named:01 IO:01 TimeDate:2E601E3C Vets:O,00 Char:0
      ResDir (ABOUTBOX) Named:00 ID:01 TimeDate:2E601E3C Vets:O,00 Char:0
         ID: 00000409 DataEntryOffs: 00000220
         Offset: 09654 Size: 000FE CodePage: 0
      ResDir (64) Named:00 ID:01 TimeDate:2E601E3C Vers:0.00 Char:0
         ID: 00000409 DataEntryOffs: 00000230
         Offset: 092C0 Size: 0026A CodePage: 0
   ResDir (STRING) Named:00 ID:02 TimeDate:2E601E3C Vers:0.00 Char:0
      ResDir (1) Named:00 ID:01 TimeDate:2E681E3C Vers:0.00 Char:0
         ID: 00000409 DataEntryOffs: 00000240
         Offset: 09EA8 Size: 000F2 CodePage: 0
      ResDir (2) Named:00 ID:O1 TimeDate:2E601E3C Vers:O.00 Char:O
         ID: 00000409 DataEntryOffs: 00000250
         Offset: 0 9F9C Size: 00046 CodePage: 0
   ResDir (GROUP ICON) Named:01 ID:00 TimeDate:2E601E3C Vers:0.00 Char:0
      ResDir (CCKK) Named:00 ID:01 TimeDate:2E601E3C Vers:0.00 Char:0
         ID: 00000409 DataEntryOffs: 00000260
         Offset: 09B6C Size: 00022 CodePage: 0
   ResDir (VERSION) Named:0 ID:01 TimeDate:2E601E3C Vers:0,00 Char:0
      ResDir (1) Named:00 ID:01 TimeDate:2E601E3C Vers:0.00 Char:0
         ID: 00000409 DataEntryOffs: 00000270
         Offset: 09B90 Size: 00318 CodePage: 0

Рис. 8.11. Иерархия ресурсов для CLOCK.EXE

Чтобы продвинуться дальше в обсуждении форматов ресурсов, мне необходимо рассказать о формате индивидуальных типов ресурсов (диалоги, меню и т.д.). Этот рассказ занял бы целую главу. Но я решил сэкономить деревья, из которых делают бумагу. Если вам интересно, читайте файл RESFMT.TXT из Win32 SDK, в котором есть детальное описание всех типов ресурсов. Программа PEDUMP показывает иерархию ресурсов, но не затрагивает индивидуальные образцы ресурсов.

Базовые поправки РЕ-файла

Компоновщик, создавая ЕХЕ-файл, предполагает, где в памяти будет отображен файл, и затем помещает предполагаемые адреса элементов программного кода и данных в исполняемый файл. Если, однако, исполняемый файл загружается куда-нибудь в другое место в виртуальном адресном пространстве, адреса, проставленные компоновщиком, оказываются неверными. Информация, хранящаяся в секции .reloc, позволяет загрузчику РЕ-файла исправить эти адреса в загружаемом модуле. Когда загрузчику удастся загрузить файл по базовому адресу, указанному компоновщиком, информация в секции .reloc игнорируется за ненадобностью. Элементы секции .reloc называются базовыми поправками, потому что их использование зависит от значения базового адреса загружаемого отображения.

В отличие от поправок в NE-файлах, базовые поправки РЕ-файлов чрезвычайно просты. Они не ссылаются на внешние DLL или на другие секции модуля. Вместо этого базовые поправки сводятся к перечню тех мест в отображении, где нужно прибавить некоторую величину.

Вот пример того, как работают базовые поправки. Предположим, что ЕХЕ-файл скомпонован в допущении, что базовый адрес равен 0х400000. Пусть указатель, содержащий адрес какой-либо строки, имеет смещение 0х2134 в отображении. Строка начинается с физического адреса 0х404002, так что указатель содержит это значение. В момент загрузки загрузчик решает, что модуль нужно отобразить в память, начиная с физического адреса 0х600000. Разность между предполагаемым компоновщиком базовым адресом и реальным адресом загрузки называется дельта. В нашем случае дельта равна 0х200000 (0х600000-0х400000). Поскольку все отображение оказывается на 0х200000 байт выше в памяти, адрес строки теперь 0х604002. Указатель на строку теперь содержит неверное значение. Чтобы исправить его, к нему необходимо прибавить дельту (в нашем случае 0х200000).

Чтобы загрузчик Windows сделал это исправление, исполняемый файл содержит базовую поправку для того места в памяти, в котором находится указатель (его смещение в отображении равно 0х2134). Чтобы разрешить базовую поправку, загрузчик добавляет дельту к исходному значению, находящемуся по адресу, указанному в базовой настройке. В нашем случае загрузчик должен прибавить 0х200000 к исходному значению указателя (0х404002) и поместить это значение (0х604002) обратно в указатель. Раз строка действительно находится по адресу 0х604002, все снова становится правильным. Рис. 8.12 показывает весь этот процесс.


Рис. 8.12. Базовые поправки РЕ-файла

Формирование данных базовых поправок выглядит несколько странно. Поправки упаковываются сериями смежных кусков различной длины. Каждый кусок описывает поправки для одной четырехкилобайтовой страницы отображения и начинается со структуры IMAGE_BASE_RELOCATION, которая выглядит следующим образом.

DWORD VirtualAddress

Это поле содержит стартовый RVA для данного куска поправок. Смещение каждой поправки, которая следует дальше, добавляется к этой величине для получения истинного RVA, к которому должна быть применена данная поправка.

DWORD SizeOfBlock

Размер данной структуры плюс все последующие поправки типа WORD. Чтобы определить количество поправок в данном блоке, нужно из значения этого поля вычесть размер IMAGE_BASE_RELOCATION (8 байт) и затем разделить на 2 (размер типа WORD). Например, если это поле содержит значение 44, то в блоке имеется 18 поправок:

(44 - sizeof(IMAGE_BASE_RELOCATION))sizeof(WORD) = 18

WORD TypeOffset

На самом деле это не отдельное слово, а массив слов, количество элементов в котором вычисляется по формуле, приведенной в описании предыдущего двойного слова. Младших 12 разрядов каждого из этих слов представляют поправочное смещение, которое должно быть прибавлено к значению в поле VirtualAddress из заголовка данного блока поправок. Старших 4 разряда каждого слова являются типом поправки. Для РЕ-файлов, исполняемых на процессорах серии Intel, существуют только два типа поправок:

  • 0 (IMAGE_REL_BASED_ABSOLUTE). Эта поправка не имеет смысла и используется как заполнитель для выравнива­ния на границу следующего двойного слова.
  • 3 (IMAGE_REL_BASED_HIGHLOW). Поправка подразумевает прибавление как старших, так и младших 16 разрядов дельты к двойному слову, на которое указывает вычисленный RVA.
  • Есть также другие поправки, определенные в WINNT.H, большая часть которых рассчитана на архитектуры процессо­ров, отличные от i386.

    Рис. 8.13 показывает некоторые базовые поправки, выведенные программой PEDUMP. Заметьте, что значения RVA, показанные на рисунке, были уже заранее определены полем VirtualAddress структуры IMAGE_BASE_RELOCATION.

    Virtual Address: 00001000 size: 0000012C
     00001032 HIGHLOW
     0000106D HIGHLOW
     000010AF HIGHLOW
     000010C5 HIGHLOW
     // Остальная часть опущена...
    Virtual Address: 00002000 size: 0000009C
     000020A6 HIGHLOW
     00002110 HIGHLOW
     00002136 HIGHLOW
     00002156 HIGHLOW
     // Остальная часть опущена...
    Virtual Address: 00003000 size: 00000114
     0000300A HIGHLOW
     0000301E HIGHLOW
     0000303B HIGHLOW
     0000306A HIGHLOW
     // Остальные поправки опущены...
    

    Рис. 8.13. Базовые поправки в ЕХЕ-файле

    COFF-таблица символов

    Если вас интересуют только те части РЕ-файла, которые используются операционной системой, вы можете пропус­тить этот и следующий ("COFF-отладочная информация") разделы и продолжить чтение с раздела "Различия между РЕ-файлами и объектными COFF-файлами".

    В любом объектном файле в COFF-стиле, созданном компилятором Microsoft, есть таблица символов. В отличие от информации Code View, эта таблица символов не является дополнительным грузом, использующимся только при необхо­димости скомпоновать исполняемый файл с отладочной информацией. Напротив, эта таблица содержит информацию обо всех общеиспользуемых и внешних символах, на которые ссылается модуль. Информация о привязке, выдаваемая компи­лятором, относится к определенным элементам в этой таблице символов. Формат COFF-таблицы символов удивительно прост - по сравнению с очень запутанным форматом Microsoft/Intel OMF с его LNAME, PUBDEF и EXTDEF.

    Если при компиляции отладочная информация не включается, то в таблице символов объектного файла будет нахо­дится лишь небольшое количество символов. Если же включить отладочную информацию (с помощью /Zi), то компиля­тор добавит дополнительную информацию о начале, конце и длине каждой функции модуля. Если затем провести компо­новку либо с /DEBUGTYPE:COFF, либо с /DEBUGTYPE:BOTH, компоновщик поместит в получившийся ЕХЕ-файл таб­лицу символов в COFF-стиле.

    Зачем нужна COFF-информация, если есть намного более полная информация CodeView? Если используется систем­ный отладчик NT (NTSD) или отладчик NT Kernel - KD (Kernel Debugger), то в игре участвует только COFF. К тому же если ваша РЕ-программа терпит катастрофу в Windows NT, DRWTSN32 может использовать эту информацию для "разбора полетов".

    И в ЕХЕ-файлах, и в объектных файлах расположение и размер COFF-таблицы символов определены в структуре IMAGE_FILE_HEADER (см. раздел "Заголовок РЕ-файла" раньше в этой главе, чтобы освежить в памяти сведения об этой структуре). Таблица символов специально сделана простой и состоит из массива структур IMAGE_SYMBOL. Коли­чество элементов в этом массиве задается значением поля NumberOfSymbols структуры IMAGE_FILE_HEADER. Рис. 8.14 показывает пример вывода символов программой PEDUMP.

    Symbol Table - 433 entries (* = auxiliary symbol)
    Indx   Name               Value     Section      cAux   Type     Storage
    ---- ------------------   --------  ----------   -----  -------  --------
    0000 .file                0000005B  sect:DEBUG   aux:1  type:00  st:FILE
        * EXEDUMP.c
    0002 .debug$S             0001B457  sect:7       aux:1  type:00  st:STATIC
        * Section: 0000 Len:   017C8 Relocs: 002C    LineNums: 0000
    0004 .data                0000B040  sect:4       aux:1  type:00  st:STATIC
        * Section: 0000 Len:   006CA Relocs: 0020    LineNums: 0000
    0006 _SzRelocTypes        0000B1E0  sect:4       aux:0  type:00  st:EXTERNAL
    0007 _SzResourceTypes     0000B148  sect:4       aux:0  type:00  st:EXTERNAL
    0008 _SzDebugFormats      0000B088  sect:4       aux:0  type:00  st:EXTERNAL
    0009 _PCOFFDebugInfo      0000B040  sect:4       aux:0  type:00  st:EXTERNAL
    000A .text                000026A0  sect:1       aux:1  type:00  st:STATIC
        * Section: 0000 Len:   00CE0 Relocs: 00A3    LineNums: 00D0
    000C _DumpDebugDirectory  000026A0  sect:1       aux:1  type:20  st:EXTERNAL
        * tag: 000E size: 01A4 Line #'s: 00009220  next fn: 0013
    000E .bf                  00000000  sect:4       aux:1  type:00  st:FUNCTION
    0010 .lf                  0000001A  sect:4       aux:0  type:00  st:FUNCTION
    0011 .ef                  000001A4  sect:4       aux:1  type:00  st:FUNCTION
    0013 _GetResourceTypeName 00002844  sect:1       aux:1  type:20  st:EXTERNAL
        * tag: 0015 size: 004A Line #'s: 000092BC  next fn: 001A
    0015 .bf                  000001A4  sect:4       aux:1  type:00  st:FUNCTION
    0017 .lf                  00000006  sect:4       aux:0  type:00  st:FUNCTION
    0018 .ef                  000001EE  sect:4       aux:1  type:00  st:FUNCTION
    // Остальные символы опущены...
    

    Рис. 8.14. Типичная COFF-таблица символов

    Каждая структура IMAGE_SYMBOL имеет следующий формат:

    typedef struct _IMAGE_SYMBOL { 
    union {
    BYTE   ShortName[8];
    struct {
    DWORD  Short;  // В случае равенства 0 следует использовать LongName. 
    DWORD  Long;        // Смещение в таблице строк. 
    } Name;
    PBYTE  LongName[2];
    } N;
    DWORD  Value;
    SHORT  SectionNumber;
    WORD  Type;
    BYTE  StorageClass;
    BYTE   NumberOfAuxSymbols;
    } IMAGE_SYMBOL;
    typedef IMAGE_SYMBOL UNALIGNED *PIMAGE_SYMBOL;

    Давайте изучим каждое из этих полей детально.

    union (Symbol name union)

    Символьное имя можно представить двумя способами, в зависимости от его длины. Если оно не длиннее 8 знаков, то член объединения ShortName содержит символьное имя в формате ASCIIZ. Следует быть осторожным в случае, когда символьное имя содержит в точности 8 знаков; при этом строка не оканчивается нулем. Если поле Name.Short не равно нулю, следует использовать член объединения ShortName. Другой способ представления символьного имени применяет­ся, когда поле Name.Short равно 0. В этой ситуации поле Name.Long является байтовым смещением в таблице строк. Таб­лица строк - это не что иное, как массив ASCIIZ-строк, следующих одна за другой в памяти. Эта таблица начинается сразу за таблицей символов. Чтобы посчитать адрес начала таблицы строк, нужно просто умножить количество символов на размер структуры IMAGE_SYMBOL и прибавить результат к стартовому адресу таблицы символов. Длина таблицы строк в байтах находится в двойном слове, имеющем смещение 0 в таблице строк.

    DWORD Value

    Это поле содержит значение, связанное с символом. Для нормальных символов и символов данных (т.е. функции и глобальные переменные) поле Value содержит RVA элемента, на который ссылается данный символ. Это значение интер­претируется иначе для некоторых других символов. В табл. 8.2 представлен краткий перечень некоторых назначений поля Value для специальных символов.

    Таблица 8.2. Специальные символы в COFF-таблицах символов

    Имя символаИспользование
    .fileИндекс символьной таблицы следующего символа .file
    .dataСтартовый RVA области данных. Эта область определяется исходным файлом, заданным предыдущим символом .file
    .textСтартовый RVA области программного кода. Эта область определяется исходным файлом, заданным предыдущим символом .file
    .IfКоличество элементов в таблице номеров строк для какой-либо функции, функция задается предыдущим символом, определяющим функцию

    SHORT SectionNumber

    Поле SectionNumber содержит номер секции, которой принадлежит символ. Например, символы для глобальных пе­ременных будут, как правило, иметь в этом поле номер секции .data. Помимо стандартных секций РЕ-файла, определены три других специальных значения.

  • 0 (IMAGE_SYM_UNDEFINED). Символ не определен. Такой номер секции используется в объектных файлах для представления символов, находящихся вне модуля, например внешних функций и внешних глобальных переменных.
  • -1 (IMAGE_SYM_ABSOLUTE). Этот символ является абсолютной величиной и не связан ни с какой конкретной сек­цией. Примерами являются локальные и регистровые переменные.
  • -2 (IMAGE_SYM_DEBUG). Данный символ используется только отладчиком и не виден из программы. Символы .file, задающие имя исходного файла, - примеры такой символьной секции.
  • WORD Type

    Тип символа. Файл WINNT.H определяет достаточно широкий спектр типов символов (int, struct, enum и т.д.). (См. полный перечень в директивах #defines IMAGE_SYM_TYPE_xxx.) К сожалению, средства Microsoft, по-видимому, не ге­нерируют символов всех возможных типов. Вместо этого все глобальные переменные и функции имеют тип NULL или тип функции, возвращающей NULL.

    BYTE StorageClass

    Класс памяти символа. Как и для типов символов файл WINNT.H определяет достаточно широкий спектр классов па­мяти: automatic, static, register, label и т.д. (См. полный перечень в директивах #define IMAGE_SYM_CLASS_xxx.) Опять-таки, как и в случае типов, средства Microsoft создают только небольшое количество информации. Все глобальные переменные и функции имеют класс памяти внешний. По всей видимости, не существует способа создать символы для локальных переменных, регистровых переменных и т.д.

    BYTE NumberOfAuxSymbols

    На самом деле я немного обманул читателя. Таблица символов не является в точности массивом структур IMAGE_SYMBOL. Если символ имеет ненулевое значение в записи NumberOfAuxSymbols, то за символом следует такое же число структур IMAGE_AUX_SYMBOL. Например, за символом .file следует столько структур IMAGE_AUX_SYMBOL, сколько требуется, чтобы хранить полный путь к исходному файлу.

    К счастью, размер структуры IMAGE_AUX_SYMBOL такой же, как и у структуры IMAGE_SYMBOL, так что пользо­ватель все же может рассматривать таблицу символов как массив структур IMAGE_SYMBOL. Запомните, что индекс символа должен рассматриваться как индекс массива, даже если некоторые элементы являются вспомогательными запи­сями. Чтобы вычислить индекс следующего регулярного символа, нужно прибавить количество вспомогательных струк­тур, используемых символом. Например, пусть символ имеет индекс 1. Если он использует три вспомогательных символа, то индекс следующего регулярного символа будет равен 4.

    IMAGE_AUX SYMBOL представляет собой запутанное объединение полей. Чтобы определить, какие члены объеди­нения использовать, необходимо знать тип регулярного символа, связанного с данным вспомогательным символом. И хо­тя я так и не понял, какие поля объединения должны быть использованы в каждом случае, я уяснил себе два следующих.

    Символы, имеющие класс памяти IMAGE_SYM_CLASS_FILE, используют член объединения File в структуре IMAGE_AUX_SYMBOL.

    Символы, имеющие класс памяти IMAGE_SYM_CLASS_STATIC, используют член объединения Section в структуре IMAGE_AUX_SYMBOL.

    Все мои знания, касающиеся интерпретации вспомогательных символов, в совокупности содержатся в функции DumpAuxSymbols() в исходном файле COMMON.С в программе PEDUMP. Если вы самостоятельно обнаружите что-нибудь новенькое, смело вносите эти дополнения в эту функцию.

    Изучая информацию внутри секции символов, можно заметить, что символы расположены не хаотически. Напротив, они сгруппированы по объектным модулям (или по исходным файлам, если вам так больше нравится), из которых они появились. Первой записью в COFF-таблице символов является запись .file. Значение записи .file - это индекс в таблице символов, указывающий на следующую запись .file. Следуя по этой цепочке записей .file, можно последовательно пере­брать все объектные модули в ЕХЕ-файле. Сразу за записью .file следуют другие записи, относящиеся к данному исход­ному файлу. Например, все общедоступные символы (глобальные переменные и функции), объявленные в исходном фай­ле, идут сразу за записью .file, отвечающей данному исходному файлу. Для нормального исходного модуля "иерархия" записей выглядит следующим образом:

    Source File record                   // Имя исходного файла.
    Data Section record (e.g., ".data")  // Данные, объявленные в файле.
    GlobalVariablei record               // Информация о переменных.
    GlobalVariable2 record               // Остальные записи глобальных переменных
    Code Section record (e.g., ".text")  // Программный код, объявленный в файле.
    Function1 record                     // Информация о функции.
    .BF record                           // Информация о начале функции.
    .LF record                           // Информация о длине функции.
    .EF record                           // Информация о конце функции.
    Function2 record
    .BF record
    .LF record
    .EF record                           // Остальные записи функции

    COFF-отладочная информация

    Для среднего программиста термин отладочная информация включает как символьную информацию, так и информа­цию о номерах строк. В COFF-формате записи, относящиеся к символам и записи, относящиеся к номерам строк, нахо­дятся в разных областях файла. (В форматах фирмы Borland и в формате Code View для таблиц символов этих два вида информации поступают из одной и той же части файла.) Я обсудил вначале COFF-таблицу символов потому, что она име­ется как в объектных, так и в ЕХЕ-файлах. К тому же очень рано в процессе изучения РЕ-формата приходится иметь дело с полем PointerToSymbolTable в структуре IMAGE_FILE_HEADER. По этим причинам я решил рассказать о таблице сим­волов отдельно.

    Вся COFF-таблица символов ЕХЕ-файла состоит из трех частей: заголовка, информации о номерах строк и таблицы символов. Они не обязательно расположены по соседству в памяти, но компоновщик Microsoft выстраивает их таким об­разом. Полная COFF-таблица символов выглядит так:

    Структура IMAGE_COFF_SYMBOLS_HEADER
    Таблицы номеров строк
    Таблица символов (обсуждалась раньше)
    

    Структура IMAGE_COFF_SYMBOLS_HEADER рассчитана на то, чтобы помочь отладчикам быстро найти необходи­мую им информацию. Эта структура содержит указатели на таблицы номеров строк и символов, а также на информацию, находящуюся где-либо в другом месте в файле.

    Чтобы отыскать структуру IMAGE_COFF_SYMBOLS_HEADER, нужно заглянуть в массив структур IMAGE_DEBUG_DIRECTORY в секции .rdata в файле. Структура IMAGE_DEBUG_DIRECTORY, содержащая в поле Type значение 1 (IMAGE_DEBUG_TYPE_COFF), содержит указатель на COFF-таблицу символов. Повторим еще раз вкратце: ка­талог данных (в конце заголовка РЕ-файла) содержит RVA массива структур IMAGE_DEBUG_DIRECTORY. Каждому типу отладочной информации, находящейся в файле, соответствует одна структура IMAGE_DEBUG_DIRECTORY. Если одна из этих структур IMAGE_DEBUG_DIRECTORY ссылается на отладочную информацию в COFF-стиле, она содержит RVA структуры IMAGE_COFF_SYMBOLS_HEADER. Структура IMAGE_COFF_SYMBOLS_HEADER в свою очередь содержит указатели на COFF-таблицу символов и информацию о номерах строк. Структура IMAGE_COFF_SYMBOLS_HEADER имеет следующий формат:

    typedef struct _IMAGE_COFF_SYMBOLS_HEADER {
    DWORD NumberOfSymbols;
    DWORD LvaToFirstSymboI;
    DWORD NumberOfLinenumbers;
    DWORD LvaToFirstLinenumber;
    DWORD RvaToFirstByteOfCode;
    DWORD RvaToLastByteOfCode;
    DWORD RvaToFirstByteOfData;
    DWORD RvaToLastByteOfData;
    } IMAGE_COFF_SYMBOLS_HEADER, *PIMAGE_COFF_SYMBOLS_HEADER;

    Рассмотрим подробнее поля структуры IMAGE_COFF_SYMBOLS_HEADER.

    DWORD NumberOfSymbols

    Количество символов в COFF-таблице символов. Данное поле содержит такое же значение, как и поле IMAGE_FILE_HEADER.NumberOfSymbols, что обсуждалось раньше в разделе "Заголовок РЕ-файла".

    DWORD LvaToFirstSymbol

    Байтовое смещение COFF-таблицы символов по отношению к началу рассматриваемой структуры. Прибавление этой величины к RVA структуры IMAGE_COFF_SYMBOLS_HEADER даст результат, совпадающий со значением поля IMAGE_FILE_HEADER.PointerToSymbolTable.

    DWORD NumberOfLinenumbers

    Количество элементов в таблице номеров строк (рис. 8.15).

    Line Numbers
    SymIndex: C (DumpDebugDirectory)
     Addr: 016A9 Line: 0008
     Addr: 016B5 Line: 0009
     Addr: 016BF Line: 000A
     Addr: 016C4 Line: 000E
     // Остальные номера строк для функции опущены...
    SymIndex: 13 (GetResourceTypeName)
     Addr: 0184A Line: 0001
     Addr: 01854 Line: 0002
     Addr: 0186F Line: 0003
     Addr: 01874 Line: 0004
     // Остальные номера строк для функции опущены...
    SymIndex: 1A (GetResourceNameFromLd)
     Addr: 01897 Line: 0004
     Addr: 018A1 Line: 0006
     Addr: 018B6 Line: 0007
     Addr: 018BB Line: 000A
     // Остальные номера строк опущены...
    

    Рис. 8.15. Типичный пример информации из таблицы номеров строк в ЕХЕ-фачле

    DWORD LvaToFirstLinenumber

    Байтовое смещение COFF-таблицы номеров строк по отношению к началу рассматриваемой структуры.

    DWORD RvaToFirstByteOfCode

    RVA первого байта исполняемого программного кода в отображении. Это поле обычно содержит значение равное RVA секции .text. Это значение также можно найти, просматривая таблицу секций исполняемого файла.

    DWORD RvaToLastByteOfCode

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

    DWORD RvaToFirstByteOfData

    RVA первого байта данных в отображении. Значение этого поля обычно равно RVA секции .bss.

    DWORD RvaToLastByteOfData

    RVA последнего байта доступных программе данных в отображении. Область, охватываемая полями FirstByteOfData и LastByteOfData, может перекрывать несколько секций (например, .bss, .rdata и .data).

    COFF-таблица номеров строк

    COFF-таблица номеров строк, на которую указывает структура IMAGE_COFF_SYMBOLS_HEADER, является очень простой - это просто массив структур IMAGE_LINENUMBER. Каждая структура ставит в соответствие одной строке программного кода исходного файла ее RVA в исполняемом отображении. Рис. 8.15 показывал образец таблицы номеров строк, выведенной программой PEDUMP. Структура IMAGE_LINENUMBER имеет два поля - объединение и слово.

    union{
    DWORD SymbolTableIndex;
    DWORD VirtualAddress;
    }Type;

    Если поле Linenumber (см. ниже) ненулевое, то его следует трактовать как RVA строки программного кода. Если поле Linenumber равно нулю, то данное поле содержит индекс в таблице символов. Символьная запись, на которую ссылается этот индекс, обозначает функцию. Все записи номеров строк для этой функции следуют вслед за этой специальной запи­сью. Из рассмотрения вывода программы PEDUMP видно, что таблица номеров строк состоит из записи индекса таблицы символов, за которой идут обычные записи номеров строк, а после них - другая запись индекса таблицы символов и т. д.

    WORD Linenumber

    Содержит номер строки относительно начала функции. Это поле не является номером строки в файле. Чтобы пере­вести его в удобный для использования номер строки в файле, следует найти в таблице символов номер начальной строки соответствующей функции. Соответствующая функция - это функция, имеющая 0 в данном поле в самой последней за­писи номера строки. Если что-либо осталось непонятным, смотрите вывод программы PEDUMP на рис. 8.15.

    Если необходим доступ только к номерам строк данной программной секции, то следует искать лишь соответствую­щий диапазон элементов, относящихся к номерам строк, в таблице секций. Структура IMAGE_SECTION_HEADER дан­ной секции содержит файловое смещение и счетчик номеров своих строк внутри таблицы. Объектные файлы COFF-формата тоже содержат информацию о номерах строк в формате, который был только что описан. В объектных файлах отсутствует структура IMAGE_COFF_SYMBOLS_HEADER, поэтому пользователю придется искать записи номе­ров строк с помощью структур IMAGE_SECTION_HEADER.

    Различия между РЕ-файлами и объектными COFF-файлами

    Во время всего предыдущего обсуждения я неоднократно отмечал, что многие структуры и таблицы имеют одинако­вый вид как в объектных COFF-файлах, так и в РЕ-файлах, созданных из этих объектных файлов. Как объектные COFF-файлы, так и РЕ-файлы имеют в своем начале (или около него) структуру IMAGE_FILE_HEADER. За этим заго­ловком следует таблица секций, содержащая информацию обо всех секциях файла. Оба формата также имеют одинаковые форматы таблиц номеров строк и символьных таблиц, хотя РЕ-файлы могут содержать дополнительные символьные таб­лицы не в COFF-стиле. Степень сродства двух форматов можно увидеть в исходном программном коде программы PEDUMP. Самый большой файл в этой программе - COMMON.C. Этот исходный файл содержит все подпрограммы, ко­торые могут использоваться как частями программы, осуществляющими ре-вывод, так и частями, осуществляющими вы­вод объектных файлов.

    Эта схожесть двух файловых форматов не случайна. Ее цель - максимально упростить работу компоновщика. Теоре­тически создание ЕХЕ-файла из одного объектного файла должно сводиться к вставке нескольких таблиц и изменению парочки файловых смещений в отображении. Имея это в виду можно представлять себе объектный COFF-файл как заро­дыш РЕ-файла. Отсутствуют или отличаются лишь несколько деталей, и я перечисляю их здесь.

  • Объектные COFF-файлы начинаются сразу с IMAGE_FILE_HEADER. Перед заголовком нет части кода DOS, и нет сигнатуры РЕ перед IMAGE_FILE_HEADER.
  • В объектных файлах отсутствует IMAGE_OPTIONAL_HEADER. В РЕ-файлах эта структура следует сразу за IMAGE_FILE_HEADER. Интересно отметить, что некоторые объектные файлы внутри файлов COFF LIB все-таки со­держат IMAGE_OPTIONAL_HEADER.
  • В объектных файлах нет базовых поправок. Вместо этого они имеют привязки, основанные на таблице символов. Я не за­трагиваю формата поправок COFF-файлов, так как они весьма запутаны. Если вы захотите сами покопаться в этой кон­кретной области, поля PointerToRelocations и NumberOfRelocations в строках таблицы секций указывают на поправки для каждой секции. Поправки представляют собой массив структур IMAGE_RELOCATION, определенный в файле WINNT.H. Если активизировать соответствующий ключ, программа PEDUMP может показать поправки объектного файла.
  • Информация CodeView в объектном файле хранится в двух секциях - .debug$S и .debug$T. Компоновщик, обрабаты­вая объектные файлы, не помещает эти секции в РЕ-файл. Вместо этого он собирает все эти секции и создает единую таблицу символов, которая хранится в конце файла. Формально таблица символов не является секцией (т.е. в таблице секций РЕ-файла нет элемента, соответствующего ей).
  • Файлы COFF LIB

    Разобравшись в объектных COFF-файлах, нетрудно разобраться в использовании файлов COFF LIB. По существу, файлы COFF LIB - это просто набор объектных COFF-файлов плюс некоторые начальные секции, позволяющие быстро устанавливать расположение нужных объектных файлов, содержащихся внутри библиотеки. Скудная документация по формату COFF LIB называет LIB-файлы архивами. Этого названия буду придерживаться в дальнейшем и я.

    Все LIB-файлы начинаются с одной и той же восьмибайтовой сигнатуры. Эта сигнатура определена в файле WINNT.H:

    #define IMAGE_ARCHIVE_START         "!<arch>\n"

    Остальная часть файла представляет ряд записей переменной длины. Каждая запись начинается структурой IMAGE_ARCHIVE_MEMBER_HEADER:

    typedef struct MAGE ARCHIVE_MEMBER_HEADER {
    BYTE            Name[16];
    BYTE            Date[12];
    BYTE            UserlD[6];
    BYTE            GrouplD[6];
    BYTE            Mode[8];
    BYTE            Size[10];
    BYTE            EndHeader[2];
    } IMAGE_ARCHIVE_MEMBER_HEADER, *PIMAGE_ARCHIVE_MEMBER_HEADER;

    Каждая структура IMAGE_ARCHIVE_MEMBER_HEADER отвечает либо объектному файлу внутри библиотеки, либо одной записи из небольшого набора специальных записей. Эти специальные записи находятся в начале библиотеки и су­ществуют для того, чтобы компоновщик в дальнейшем мог быстро отыскивать объектные файлы в LIB-файле. Исходные данные для члена архива следуют сразу за структурой IMAGE_ARCHIVE_MEMBER_HEADER, с которой начинается ка­ждая запись. Для большинства членов архива записей исходные данные точно такие же, как и в объектном файле. Дейст­вительно, когда программа PEDUMP проводит вывод LIB-файлов, она вызывает те же процедуры, что и при обработке объектного файла. Рис. 8.16 показывает формат LIB-файлов.

    Рассмотрим поля структуры IMAGE_ARCHIVE_MEMBER_HEADER.

    BYTE Name[16]

    Имя члена архива. Если символ "/" появляется после ASCII-строки (например, FOO.OBJ/), то строка перед символом "/" представляет имя члена. Если имя начинается с символа "/", за которым следует десятичное число (например, /104), то число является смещением имени члена архива внутри члена Longnames LIB-файла. В предыдущем примере имя члена начинается со 104-го байта от начала области Longname.

    Имеются также специальные имена для специальных членов архива:

    #define IMAGE_ARCHIVE_LINKER MEMBER            "/                " 
    #define 1MAGE_ARCHIVE_LONGNAMES_MEMBER         "//          "


    Рис. 8.16. СОFF-формат LIB-файлов

    Для объектных файлов внутри библиотеки импорта это поле представляет имя DLL, содержащей импортируемые функции.

    BYTE Date[12]

    Дата и время создания члена. Это число хранится в десятичном ASCII-виде.

    BYTE UserID[6]

    Десятичное ASCII-представление идентификатора пользователя. По-видимому, всегда является строкой NULL.

    BYTE GroupID[6]

    Десятичное ASCII-представление идентификатора группы. По-видимому, всегда является строкой NULL.

    BYTE Mode[8]

    Десятичное ASCII-представление файлового режима. По-видимому, всегда равно нулю.

    BYTE Size[10]

    Размер данных члена, представленный в десятичной ASCII-форме. Формат данных зависит от их типа (указан в уже описанном поле Name).

    BYTE EndHeader[2]

    ASCII строка \n.

    Члены компоновщика

    Всякий LIB-файл имеет две секции членов компоновщика, выполняющих функцию оглавления для остальной части файла. Оба члена имеют имя "/" и различаются по порядку следования в файле. Первый член компоновщика - это пер­вый член архива с именем "/", а второй член компоновщика - это второй член архива с именем "/".

    Оба члена компоновщика - это, по существу, перечни общедоступных символов в LIB-файле вместе со смещениями внутри файла членов - объектных модулей, которые содержат общедоступные символы. Два члена компоновщика име­ют различные форматы. Зачем нужны две копии одинаковой информации? Первый член компоновщика хранит свою информацию в том порядке, в каком объектные модули идут далее в LIB-файле. Это приводит к неоптимальным поискам. Второй член компоновщика хранит свои символы в алфавитном порядке, что делает его намного более полезным для компоновщика. В соответствии с документацией Microsoft компоновщик игнорирует первый член компоновщика и всегда использует второй член.

    Первый член компоновщика имеет следующий формат.

    DWORD NumberOfSymbols

    Число общедоступных символов в данной библиотеке. Это число представлено в формате big-endian (отражает насле­дие COFF-формата для машин, отличных от машин i386). Функция ConvertBigEndian в файле LIBDUMP.C программы PEDUMP осуществляет переключение из формата big-endian в формат little-endian, используемый i386.

    DWORD Offsef[NumberOfSymbols]

    Массив файловых смещений других членов архива. Эти смещения имеют формат big-endian. Каждый из этих членов - это член типа OBJ. Каждый элемент этого массива соответствует имени символа в перечне последующих строк ASCII.

    BYTE StringTable[?]

    Это неразрывная серия строк в стиле С в памяти.

    По существу, каждый элемент массива Offset соответствует одному общедоступному символу, имя которого появляет­ся в области StringTable. Например, третий элемент массива Offsets отвечает третьей строке в области StringTable. Вывод программы PEDUMP поясняет это:

    First Linker Member:
     Symbols:  00000006
     MbrOffs   Name
     --------  -----------
     00000180  _DumpCAP@0
     00000180  _StartCAP@0
     00000180  _StopCAP@0
     ...
    

    Формат второго члена компоновщика запутаннее из-за добавления массива, необходимого для быстрого поиска сим­волов. Второй член компоновщика имеет следующий формат.

    DWORD NumberOfMembers

    Это двойное слово содержит количество членов в архиве объектных модулей, следующих дальше в файле.

    DWORD Offsets[NumberOfSymbols]

    Массив файловых смещений других членов архива. В отличие от первого члена компоновщика эти смещения заданы в естественном формате машины (т.е. в формате little-endian для i386).

    DWORD NumberOfSymbols

    Количество общедоступных символов в массиве StringTable (следовательно, и количество общедоступных символов в библиотеке). Это поле к тому же содержит количество элементов в следующем далее массиве Indices.

    WORD Indices[NumberOfSymbols]

    Данный массив содержит индексы (отсчет начинается с 1) массива Offsets (описан на два поля раньше). Этот массив идет параллельно строкам массива StringTable.

    BYTE SiringTable[NumberOfSymbols]

    Это неразрывная серия строк в стиле С в памяти.

    Для того чтобы отыскать объектный файл по его символу, используя второй член компоновщика, компоновщик сна­чала просматривает массив StringTable и вычисляет относительный индекс строки в массиве. Затем компоновщик исполь­зует этот индекс для поиска слова в массиве Indices. Наконец, компоновщик вычитает 1 из этого слова в массиве Indeces и использует результат как индекс массива Offsets. Найденное двойное слово в массиве Offsets как раз и будет смещением в объектном файле, содержащем общедоступный символ. Функция DumpSecondLinkerMember из файла LIBDUMP.C про­граммы PEDUMP показывает этот процесс в действии.

    Член Longnames

    Данные в секции архивного члена Longnames - это просто набор строк в стиле С, следующих одна за другой. Строка помещается в секцию Longnames, если она слишком велика, чтобы уложиться в 16 байт, зарезервированных для поля Name в структуре IMAGE_ARCHIVE_MEMBER_HEADER. В этом случае поле Name содержит символ "/", за которым следует десятичное ASCII-представление смещения строки в секции Longnames.

    Резюме

    Для Win32 Microsoft проделала коренные изменения в форматах объектных и исполняемых файлов. Эти изменения позволили Microsoft сэкономить время, так как использовалась работа, выполненная для других операционных систем. Главная цель такого усовершенствования файловых форматов - улучшить совместимость с различными платформами. COFF-формат объектных файлов существовал до создания Win32. РЕ-формат является расширением COFF-формата и разработан для использования на платформах Win32.

    Ценная часть как объектных, так и исполняемых файлов начинается со структуры IMAGE_FILE_HEADER. За этой структурой (и, возможно, еще одной дополнительной структурой) следует таблица секций. В таблице секций указаны ме­стонахождение и атрибуты всех секций файла. Секцией называется совокупность логически связанных программного ко­да и данных. Чтобы обеспечить быстрое нахождение информации, РЕ-файл содержит каталог данных, указывающий на важные позиции в файле (например, расположение таблицы экспорта файла). Помимо заголовка (или заголовков), таблиц секций и исходных данных секций объектные COFF- и РЕ-файлы могут также содержать информацию о именах символов и номерах строк. Эта информация хранится в конце файла после всех заголовков и данных для секций.


    Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.
        Сообщений 2    Оценка 80        Оценить