Исследуем формат EXE-файла
Целью работы компилятора является получение EXE-файла. Поэтому, исследуем его структуру.
EXE-файлы появились ещё в DOS и потом они с небольшими изменениями перекочевали в Windows. Формат EXE-файла под Windows называется PE-файлом. Он организован в виде линейного потока данных.
Формат PE-файла
Заголовок MS-DOS |
Программа-заглушка |
Заголовок PE |
Доп. заголовок PE |
Массив DataDir |
Заголовки сегментов |
Тела сегментов |
Остальные области данных |
Заголовок MS-DOS не нов, он используется начиная с MS-DOS версии 2. Если вы пытаетесь запустить Windows-программу под DOS-ом, то программа-заглушка, которая размещена ниже, сообщит о невозможности этого сделать. Если бы заголовок MS-DOS и программа-заглушка не были бы включены в PE-файл, то скорее всего бы это бы привело к сбою.
Залоговок MS-DOS (размер 40H байт)
Адрес | Тип | Имя | Описание |
---|---|---|---|
00h | word | Magic | Магическая сигнатура DOS-файла - два символа "MZ", явно от MZ-club :) |
02h | word | LastByteCount | Количество байт на последней странице файла |
04h | word | PageCount | Количество страниц в файле |
06h | word | RelocCount | Количество релокейшенов |
08h | word | HeaderSize | Размер заголовка в параграфах |
0Ah | word | MinAlloc | Мин. выделение памяти в параграфах |
0Ch | word | MaxAlloc | Макс. выделение памяти в параграфах |
0Eh | word | InitSS | Начальное (относительное) значение регистра SS |
10h | word | InitSP | Начальное значение регистра SP |
12h | word | CheckSum | Контрольная сумма |
14h | word | InitIP | Начальное значение регистра IP |
16h | word | InitCS | Начальное (относительное) значение регистра CS |
18h | word | RelocAddr | Адрес на релокейшены и программу-заглушку |
1Ah | word | OverlayCount | Количество оверлеев |
1Ch | word | Res1[4] | Зарезервировано |
24h | word | OEMIdentifier | Для OEMInfo |
26h | word | OEMInfo | Информация о программе |
28h | word | Res1[10] | Зарезервировано |
3Ch | dword | PEHeaderAddr | Адрес в файле заголовка PE |
Для Windows-программы заголовок MS-DOS не содержит релокейшины (пока даже и не знаю что это такое), то есть Relocations = 0, поэтому RelocAddr указывает сразу на программу-заглушку. Но нам важен заголовок PE, его адрес находиться в PEHeaderAddr.
Залоговок PE (размер 18H байт)
Адрес | Тип | Имя | Описание |
---|---|---|---|
00h | dword | Magic | Магическая сигнатура PE-файла 4550H или "PE", 0H, 0H |
04h | word | CPUType | Тип процессора |
06h | word | SectionCount | Количество сегментов |
08h | dword | DateTime | Дата/время создания/модификации линкером |
0Сh | dword | SymbolTableAddr | Адрес местонахождения таблицы символов |
10h | dword | SymbolTableSize | Размер таблицы символов |
14h | word | OptionalHeaderSize | Размер доп. заголовка PE |
16h | word | Flags | Предназначение программы |
Сразу за основным заголовком идёт дополнительный заголовок PE.
Доп. залоговок PE (размер 18H - 77H байт)
Адрес | Тип | Имя | Описание |
---|---|---|---|
18h | word | Magic | Всегда 10Bh |
1Ah | byte | MajorLinkVer | Версия линкера, создавшего данный файл |
1Bh | byte | MinorLinkVer | - |
1Ch | dword | CodeSize | Размер исполнительного кода |
20h | dword | InitDataSize | Размер инициализированных данных |
24h | dword | UnInitDataSize | Размер неинициализированных данных |
28h | dword | EntryPointAddr | Адрес, относительно ImageBase, по которому передаётся управление при запуске программы или адрес инициализации/завершения библиотеки |
2Ch | dword | CodeBase | Относительное смещение сегмента кода |
30 | dword | DataBase | Относительное смещение сегмента неинициализированных данных |
34h | dword | ImageBase | Предподчтительный адрес для загрузки исполнимого файла (по умолчанию 400000H) |
38h | dword | SectionAlign | Выравнивание программных секций (по умолчанию 1000H) |
3Ch | dword | FileAlign | Минимальная гранулярность сегментов, то есть размер сегментов должен быть кратен FileAlign, должен быть равен значению степени 2 между 200H и 10000H (по умолчанию 200H) |
40h | word | MajorOSVer | Старший номер версии OS, необходимый для запуска программы |
42h | word | MinorOSVer | Младший номер версии OS |
44h | word | MajorImageVer | Пользовательский старший номер версии, задается пользователем при линковке программы и им же и используется |
46h | word | MinorImageVer | Пользовательский младший номер версии, задается пользователем при линковке программы и им же и используется |
48h | word | MajorSubSysVer | Старший номер версии Win32 |
4Ah | word | MinorSubSysVer | Младший номер версии Win32 |
4Ch | dword | Res1 | - |
50h | dword | ImageSize | Виртуальный размер в байтах всего загружаемого образа, вместе с заголовками, кратен ObjectAlign |
54h | dword | HeaderSize | Общий размер всех заголовков: MS-DOS, PE, доп PE и всех сегментов |
58h | dword | CheckSum | Контрольная сумма (не используется и равна 0) |
5Ch | word | SubSystem | Подсистема, необходимая для запуска данного файла //(0 - неизвестная подсистема, 1 - не требует подсистему, 2 - Windows GUI, 3 - Windows консоль...) |
5Eh | word | DllFlags | Специальные флаги при загрузке, начиная с NT 3.5 не используются |
60h | dword | StackReserveSize | Память, требуемая для стека приложения, память резервируется, но выделяется только StackCommitSize байтов, следующая страница является охранной. Когда приложение достигает этой страницы, то страница становится доступной, а следующая страница - охранной, и так до достижения нижней границы, после чего Windows убивает программу с сообщением о конце стека |
64h | dword | StackCommitSize | Объем памяти, отводимый в стеке немедленно после загрузки |
68h | dword | HeapReserveSize | Максимальный возможный размер локального хипа |
6Ch | dword | HeapComitSize | Отводимый при загрузке хип |
70h | dword | LoaderFlags | Данный параметр устарел |
74h | dword | DataDirSize | Указывает размер массива DataDir, расположенный ниже (по умолчанию 10h) |
Далее идёт массив DataDir, 8-байтные элементы которого состоят из двух 4-х байтных: адрес и размер.
Массив DataDir (размер 78H - F8H байт)
Адрес | Тип | Имя | Описание |
---|---|---|---|
78h | qword | ExportDir | Каталог экспортируемых объектов |
80h | qword | ImportDir | Каталог импортируемых объектов |
88h | qword | ResourceDir | Каталог ресурсов |
90h | qword | ExceptionDir | Каталог исключений |
98h | qword | SecurityDir | Каталог безопастности |
A0h | qword | BaseRelocDir | Каталог переадресаций |
A8h | qword | DebugDir | Отладочный каталог |
B0h | qword | CopyrightDir | Каталог описаний |
B8h | qword | CpuSpecDir | Каталог значений, специфичных для процессора |
C0h | qword | TLSDir | Каталог TLS (Thread local storage - локальная память потоков) |
C8h | qword | ConfigDir | Каталог конфигураций загрузки |
D0h | qword | ResDir11 | - |
D8h | qword | ResDir12 | - |
E0h | qword | ResDir13 | - |
E8h | qword | ResDir14 | - |
F0h | qword | ResDir15 | - |
Элемент массива DataDir (размер 8 байт)
Адрес | Тип | Имя | Описание |
---|---|---|---|
00h | dword | Addr | Адрес каталога |
04h | dword | Size | Размер каталога |
Далее идёт подряд несколько сегментов, количество указано в SectionCount.
Заголовок сегмента (размер 2Ch байт)
Адрес | Тип | Имя | Описание |
---|---|---|---|
00h | char[8] | SectName | Имя секции, если имя < 8, то остаток заполнен нулями |
08h | dword | VirtualSize | Виртуальный размер секции, именно столько памяти будет отведено под секцию. Если VirtualSize превышает PhysicalSize, то разница заполняется нулями, так определяются секции неинициализированных данных (Physical Size = 0) |
0Ch | dword | VirtualAddr | Виртуальный адрес секции относительно ImageBase |
10h | dword | PhysicalSize | Размер секции (её инициализированной части) в файле, кратно полю FileAlign, должно быть <= VirtualSize |
14h | dword | PhysicalAddr | Физическое смещение тела сегмента относительно начала EXE файла |
18h | 0Ch | Res1 | Зарезервировано для OBJ файла, в экзешниках смысла не имеет |
28h | dword | SectFlags | Битовые флаги секции |
Практически любая программа под Windows работает с такими её DLL-ками: kernel32.dll, user32.dll, gdi32.dll и т.д.. Поэтому, EXE-шник должен уметь импортировать функции данных библиотек, то есть работать с каталогом импорта ImportDir. Каталог импорта сразу же начинается с таблицы импорта ImportDirTable, которая описывает остальную информацию об импорте. Такая таблица состоит из элементов ImportDirTableItem, указывающих, как минимум, на каждую импортируемую библиотеку. Последний элемент, указывающий на конец таблицы, заполнен нулями.
Элемент таблицы каталога импортируемых объектов ImportDirTableItem (размер 14h байт)
Адрес | Тип | Имя | Описание |
---|---|---|---|
00h | dword | FuncNameList | Список имён импортируемых функций |
04h | dword | Res1 | - |
08h | dword | Res2 | - |
0Ch | dword | LibName | Имя библиотеки |
10h | dword | FuncAddrList | Список адресов импортируемых функций |
Параметр LibName указывает на имя библиотеки, которое должно заканчиваться нулём. FuncNameList указывает на список адресов (0-ой адрес - конец списка), по которым находится сначала Hint - (укороченный идентификатор точки входа), а затем имя функции, заканчивающееся нулём. Параметр FuncAddrList указывает на точно такой же список адресов, находящийся (по моим наблюдениям) перед ImportDirTable.
Формат EXE-файла здесь описан не полностью. Остальное будет описано позже. Однако, этого уже достаточно для создания компилятора.
Для глубокого изучения EXE-шника, написана специальная программа "EXE-исследователь". Последнюю версию данной программы можно скачать на страничке Download