Объектный файл представляет наиболее популярный формат, в который компилятор компилирует исходный код на некотором языке программирования. Объектный файл обычно начинается с заголовка. Заголовок содержит определенную информацию о подписи, которая идентифицирует файл как действительный объектный файл, а также несколько других значений, которые определяют расположение различных структур данных внутри файлы. Кроме заголовка объектный файл обычно делится на несколько разделов, каждый из которых содержит данные приложения, машинные инструкции, записи таблицы символов, данные релокаций/перемещений и другие метаданные. В некоторых случаях фактический код и данные программы представляют собой лишь небольшую часть всего файла объектного кода.
Существуют различные форматы объектных файлов, но один из наиболее распространенных и популярных - формат COFF (Common Object File Format)
Однако конечным файлом программы, как правило, является не объектный файл, а исполняемый файл в специальном формате. Большинство операционных систем используют специальный формат исполняемых файлов. Часто формат исполняемого файла подобен формату объектного файла, принципиальное отличие состоит в том, что в исполняемом файле обычно нет неразрешенных внешних ссылок.
В дополнение к машинному коду и двоичным данным исполняемые файлы содержат другие метаданные, в том числе информацию об отладке, информацию о компоновке динамически подключаемых библиотек и сведения о том, как операционная система должна загружать различные разделы файла в память. В зависимости от процессора и ОС исполняемые файлы могут также содержать информацию о перемещениях/релокациях, чтобы ОС могла исправлять абсолютные адреса при загрузке файла в память. Обычно объектные и исполнимые файлы содержат одинаковую информацию и поэтому их форматы часто во многом совпадают. Например, Linux использует формат ELF (Executable and Linkable Format), Windows - формат PE, который во многом аналогичен формату COFF.
Вначале каждого файла COFF идет заголовок файла COFF. На ОС Windows и Linux он имеет следующую структуру:
// версия для Windows в winnt.h typedef struct _IMAGE_FILE_HEADER { WORD Machine; WORD NumberOfSections; DWORD TimeDateStamp; DWORD PointerToSymbolTable; DWORD NumberOfSymbols; WORD SizeOfOptionalHeader; WORD Characteristics; } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER; // версия для Linux в coff.h struct COFF_filehdr { char f_magic[2]; /* число magic number */ char f_nscns[2]; /* количество разделов */ char f_timdat[4]; /* время и дата */ char f_symptr[4]; /* указатель на таблицу символов symtab */ char f_nsyms[4]; /* число записей в таблице символов symtab*/ char f_opthdr[2]; /* размер дополнительного заголовка - sizeof(optional hdr) */ char f_flags[2]; /* флаги */ };
Основные элементы заголовка файла:
f_magic/Machine
Идентифицирует систему, для которой был создан этот файл COFF. Это значение является сигнатурой, которая указывает, содержит ли COFF-файл данные или машинные инструкции, подходящие для текущей операционной системы и процессора. Возможные значения:
Значение | Описание |
0x14c | Intel 386 |
0x8664 | x86-64 / AMD AMD64 |
0x162 | MIPS R3000 |
0x168 | MIPS R10000 |
0x169 | MIPS little endian WCI v2 |
0x183 | old Alpha AXP |
0x184 | Alpha AXP |
0x1a2 | Hitachi SH3 |
0x1a3 | Hitachi SH3 DSP |
0x1a6 | Hitachi SH4 |
0x1a8 | Hitachi SH5 |
0x1c0 | ARM little endian |
0x1c2 | Thumb |
0x1c4 | ARMv7 |
0x1d3 | Matsushita AM33 |
0x1f0 | PowerPC little endian |
0x1f1 | PowerPC with floating-point support |
0x200 | Intel IA64 |
0x266 | MIPS16 |
0x268 | Motorola 68000 series |
0x284 | Alpha AXP 64-bit |
0x366 | MIPS with FPU |
0x466 | MIPS16 with FPU |
0xebc | EFI bytecode |
0x9041 | Mitsubishi M32R little endian |
0xaa64 | ARM64 little endian |
0xc0ee | CLR pure MSIL |
f_nscns/NumberOfSections
Количество разделов в файле
f_timdat/TimeDateStamp
Содержит метку времени в стиле Unix (количество секунд с 1 января 1970 г.), которая указывает на дату и время создания файла.
f_symptr/PointerToSymbolTable
Содержит смещение в файле (то есть количество байтов от начала файла), которое указывает, где в файле начинается таблица символов. Таблица символов — это структура данных, в которой указаны имена и другая информация обо всех внешних, глобальных и других символах, используемых кодом в файле COFF. Линкеры используют таблицу символов для разрешения внешних ссылок.
f_nsyms/NumberOfSymbols
Количество записей в таблице символов.
f_opthdr/SizeOfOptionalHeader
Указывает на размер необязательного заголовка, который следует сразу за заголовком файла (то есть первый байт необязательного заголовка следует сразу за полем
f_flags/Characteristics
в структуре заголовка файла). Компоновщик или другая программа может использовать это значение, чтобы определить, где в файле заканчивается
необязательный заголовок и начинаются заголовки разделов. Заголовки разделов следуют сразу за необязательным заголовком, но размер необязательного заголовка не является
фиксированным. Различные реализации файла COFF могут иметь разные необязательные структуры заголовков. Если необязательный заголовок отсутствует в файле COFF, поле
f_opthdr/SizeOfOptionalHeader
будет содержать ноль, а заголовок первого раздела будет следовать сразу за заголовком файла.
f_flags/Characteristics
Битовая маска, которая содержит определенные логические флаги, например, является ли файл исполняемым, содержит ли он информацию о символах и содержит ли он информацию о номере строки (для использования отладчиками).
Необязательный заголовок COFF содержит информацию, относящуюся к исполняемым файлам. Этот заголовок может отсутствовать, если файл содержит объектный код, который не является исполняемым. Данный необязательный заголовок всегда присутствует в файлах Linux COFF и Microsoft PE/COFF, даже если файл не является исполняемым. Структуры Windows и Linux для этого необязательного заголовка файла принимают следующие формы в C:
// Microsoft PE/COFF Optional Header (winnt.h) typedef struct _IMAGE_OPTIONAL_HEADER { // // Стандартные поля // WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; DWORD BaseOfData; // // Дополнительные поля для NT // DWORD ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; DWORD SizeOfStackReserve; DWORD SizeOfStackCommit; DWORD SizeOfHeapReserve; DWORD SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32; // Linux/COFF Optional Header format (from coff.h) typedef struct { char magic[2]; /* тип файла */ char vstamp[2]; /* версия */ char tsize[4]; /* размер раздела .text/.code */ char dsize[4]; /* размер раздела .data с инициализированными данными */ char bsize[4]; /* размер раздела .bss с неинициализированными данными */ char entry[4]; /* точка вхожа */ char text_start[4]; /* начало раздела .text */ char data_start[4]; /* начало раздела .data */ } COFF_AOUTHDR;
Первое, на что следует обратить внимание, что эти структуры не идентичны. Версия для Microsoft содержит значительно больше информации, чем версия для Linux.
Поле f_opthdr/SizeOfOptionalHeader
существует в заголовке файла для определения фактического размера необязательного заголовка. Основные компоненты заголовка:
magic/Magic
Предоставляет еще одно значение подписи для файла COFF. Это значение подписи определяет тип файла (то есть COFF), а не систему, в которой он был создан. Линкеры используют значение этого поля, чтобы определить, действительно ли они работают с COFF-файлом (а не с каким-то произвольным файлом, который может запутать компоновщика).
vstamp/MajorLinkerVersion/MinorLinkerVersion
Указывает номер версии формата COFF, чтобы компоновщик, написанный для более старой версии формата файла, не пытался обрабатывать файлы, предназначенные для более новых компоновщиков.
tsize/SizeOfCode
Содержит размер раздела кода (.code/.text). Если файл COFF содержит более одного раздела кода, значение этого поля не определено, хотя обычно оно определяет размер первого раздела кода
dsize/SizeOfInitializedData
Содержит размер раздела данных (раздел .data). Это поле не определено, если в файле есть два или более разделов данных. Обычно в этом поле указывается размер первого раздела данных, если разделов данных несколько.
bsize/SizeOfUninitializedData
Содержит размер раздела .BSS
— раздела неинициализированных данных. Это поле может быть не определено,
если разделов .BSS два и более, в таких случаях в этом поле обычно указывается размер первого раздела BSS в файле.
entry/AddressOfEntryPoint
Содержит начальный адрес исполняемой программы (точку входа). Это не фактический адрес памяти, а смещение относительно начала файла.
text_start/BaseOfCode
Содержит начальный адрес, где начинается раздел кода. При наличии двух или более разделов кода это поле не определено, но обычно указывает смещение к первому разделу кода.
data_start/BaseOfData
Содержит начальный адрес, где начинается раздел данных. При наличии двух или более разделов данных это поле не определено, но обычно оно указывает смещение к первому разделу данных.
Для хранения адреса начала раздела .bss
с неинициализированными данными отдельного поля не требуется. Формат файла COFF предполагает, что загрузчик
операционной системы автоматически выделяет память для раздела BSS, когда программа загружается в память. А некоторые компиляторы фактически объединяют разделы BSS и
DATA вместе из соображений производительности.
Заголовки разделов следуют за необязательным заголовком в файле COFF. В отличие от файла и дополнительных заголовков, файл COFF может содержать несколько заголовков разделов.
Поле f_nscns/NumberOfSections
в заголовке файла указывает точное количество заголовков разделов (и, следовательно, разделов) в COFF-файле.
Поскольку размер необязательного заголовка является переменным (и, по сути, может быть даже равен 0, если он отсутствует), вам нужно добавить поле
f_opthdr/SizeOfOptionalHeader
в заголовок файла к размеру заголовка файла, чтобы получить начальное смещение заголовка первого раздела.
Заголовки разделов имеют фиксированный размер, поэтому, получив адрес первого заголовка раздела, можно легко вычислить адрес любого другого, умножив желаемый номер заголовка раздела на размер заголовка раздела и добавив результат к базовому смещению заголовка первого раздела.
Определения структур на языке C для заголовков разделов Windows и Linux:
// определение для Windows (из файла winnt.h) typedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; union { DWORD PhysicalAddress; DWORD VirtualSize; } Misc; DWORD VirtualAddress; DWORD SizeOfRawData; DWORD PointerToRawData; DWORD PointerToRelocations; DWORD PointerToLinenumbers; WORD NumberOfRelocations; WORD NumberOfLinenumbers; DWORD Characteristics; } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER; // Определение для Linux (из файла coff.h) struct COFF_scnhdr { char s_name[8]; /* название раздела */ char s_paddr[4]; /* физический адрес */ char s_vaddr[4]; /* виртуальный адрес */ char s_size[4]; /* размер раздела */ char s_scnptr[4]; /* указатель файла на непосредственные данные раздела */ char s_relptr[4]; /* указатель файла на релокацию */ char s_lnnoptr[4]; /* указатель файла на номера строк */ char s_nreloc[2]; /* количество записей о релокации */ char s_nlnno[2]; /* количество записей номеров строк */ char s_flags[4]; /* флаги */ };
Компоненты заголовка раздела
s_name/Name
хранит имя раздела. Как видно из определения Linux, это поле ограничено восемью символами, и, соответственно, имена разделов будут иметь длину не более восьми символов. (Обычно, если в исходном файле указано более длинное имя, компилятор/ассемблер усекает его до 8 символов при создании COFF-файла.) Если длина имени раздела составляет ровно восемь символов, эти восемь символов займут все 8 байтов этого поля, нулевой байт, который обычно служит в качестве завершения строки, не будет добавляться. Если имя раздела короче восьми символов, за именем будет следовать концевой нулевой байт.
Значение этого поля часто имеет вид .text, CODE, .data или DATA. Но стоит учитывать, что имя не определяет тип раздела. Так, можно создать раздел .code/.text
и
назвать его "DATA", либо создать раздел данных (.data
) и назвать его ".text" или "CODE". Поле s_flags/Characteristics
определяет фактический тип этого раздела.
s_paddr/PhysicalAddress/VirtualSize
Редко используется большинством инструментов. В Unix-подобных операционных системах (таких как Linux) для этого поля обычно устанавливается то же значение,
что и для поля VirtualAddress
. Различные инструменты Windows устанавливают для этого поля разные значения (включая ноль).
s_vaddr/VirtualAddress
Содержит адрес загрузки секции в память (то есть ее адрес виртуальной памяти). Обратите внимание, что это адрес памяти времени выполнения, а не смещение в файле. Загрузчик программы использует это значение, чтобы определить, куда загрузить раздел в память.
s_size/SizeOfRawData
Хранит размер раздела в байтах.
s_scnptr/PointerToRawData
Хранит смещение файла до начала данных раздела
s_reptr/PointerToRelocations
Хранит смещение файла до списка релокаций для этого раздела.
s_lnnoptr/PointerToLinenumbers
Хранит смещение файла к записям номеров строк для текущего раздела.
s_nreloc/NumberOfRelocations
Хранит количество записей перемещения для этого раздела.
s_nlnno/NumberOfLinenumbers
Хранит количество записей номеров строк для раздела. Информация о номере строки используется отладчиками
s_flags/Characteristics
Битовая маска, которая хранит характеристики этого раздела. В частности, в этом поле будет указано, требует ли раздел перемещения/релокаций, содержит ли он код, доступен ли он только для чтения и т. д.