Вступление
Практически сразу, после PC-DOS 1.0, вместе с .COM файлами, (или файлами команд),
появились .EXE файлы (полн. «EXEcutable» или «исполняемые»). Сегодня речь пойдет именно об этом.
Поскольку история происходит снова в Microsoft, запутаться можно очень легко, в любом месте.
Во-первых речь пойдет только о первом формате сборки, то есть о знаменитом MZ-заголовке и его подопечных.
Небольшой обзор
Согласно википедии:
.EXE (полн. англ. executable — исполняемый) — расширение исполняемых файлов, применяемое в операционных системах DOS, Windows, Symbian OS, OS/2 и в некоторых других, соответствующее ряду форматов. Кроме объектного кода может содержать различные метаданные (ресурсы, цифровая подпись).
А сама программа .EXE в понимании PC-DOS это
Исполняемый файл — это размеченный образ, содержащий в себе таблицы данных, секции кода, данных, и фиксированную точку входа.
По сравнению с командами это уже не монолит кода. Это вполне себе книга, которая имеет название, содержание и главы.
Я приведу таблицу, и там детальнее покажу все особенности.
|
Характеристики |
|
|
|---|---|---|
|
Точка входа |
Везде (любое место) |
Фиксированная |
|
Максимальный размер |
64 КБ |
1 МБ |
|
Структура данных |
|
Разграничен |
|
Опознавательные знаки |
|
Цифровая подпись |
|
Сегментация |
|
Есть |
Теперь подробнее объясню смысл идеи «область жизни»;
Те кто знаком с PC/MS-DOS любым образом, сразу держат в голове факт, что команда при вызове загружалась в «Program Memory» область и занимала какое-то там место, но помещалась там абсолютно вся. Из любой точки памяти процесса можно было «ткнуть» (или взять указатель) на любое место памяти процесса, без лишних телодвижений.
В чуть позже, это будет называться «НеДалёкие указатели» (англ. «Near Pointers»).
Поскольку, размер образ .EXE файла может быть значительно больше, (уже за пределами ОЗУ), — в ОЗУ полностью он просто не поместится, а в «Program Memory» уж и подавно нет. Спрашивается сразу же:
— Что же делать?
Оверлеи
Перед тем, как тронуть спецификацию программы, я обязан описать то, что в этой документации может вызвать примитивные вопросы.
Согласно словарям и старым источникам:
Overlay — это метод программирования, позволяющий создавать программы, занимающие больше памяти, чем установлено в системе
Метод предполагает разделение программы на фрагменты. Размер каждого оверлея/фрагмента ограничен, согласно размеру доступной памяти.
Место в памяти, куда будет загружен оверлей называется регионом (region или destination region) или областью перекрытия. Хотя часто программы используют только один блок памяти для загрузки различных оверлеев, возможно определение нескольких регионов различного размера.
Менеджер оверлеев, иногда являющийся частью ОС, подгружает запрашиваемый оверлей из внешней памяти в регион.
Говорят, что применение оверлеев требует очень внимательного отношения к размеру каждой части программы. Программирование при помощи оверлеев является более сложным, чем при использовании виртуальной памяти.
Я никогда не пробовал такое делать, но в будущем обязательно вернусь к этому и попробую что-то сделать сам. В добавок, заранее документации Microsoft дают знать, что «В PC-DOS двоичные файлы, содержащие оверлеи, часто имели расширение .OVL.»
Чуть-чуть прыгну вперёд статьи.
В структуре MZ заголовка есть поле, которое говорит номер части программы.
Эта информация подтверждается старыми источниками тоже.
Раз такое говорят — время проверять.
Я выбрал любимую дискету с DR-DOS 7.+ и взял оттуда .OVL части
программ, заодно и сами образы программ тоже.
Само значение равное нулю говорит, что структура заголовка хранится в основной запускаемой части. А здесь незапускаемая .OVL часть держит противоречие.
Получается, что это «не идеальный образ» или образ, необязательная информация
в котором — неверна, или попросту отсутствует.
Значит DOS функция загружает overlay-части основываясь на чём-то другом.
DOS и Оверлеи (не влезай, убьет)
Сразу к делу. В самом ABI системы, функция отвечающая за оверлеи
имеет номер 0x4B и работает с параметрами немного хитрее, чем можно подумать.
Я нашел на просторах интернета хороший пример части программы,
которая демонстрирует использование той самой DOS функции.
Я просто переведу на русский язык все комментарии в коде.
... ... ... ; Выделим память для оверлея mov bx,1000h ; 64 KB (4096 блоков) mov ah,48h ; Номер функции 48h = выделить блок int 21h jc __error ; Если не получилось выделить блок mov pars,ax ; адрес области перекрытия mov pars+2,ax ; сегмент для overlay-части ; создадим сегмент для входной точки mov word ptr entry+2,ax mov stkseg,ss mov stkptr,sp mov ax,ds ; ES = DS mov es,ax mov dx,offset oname ; DS:DX = название файла mov bx,offset pars ; ES:BX = parameter block ; Специальная DOS функция для оверлеев mov ax,4b03h ; <-- EXEC 0x03 int 21h mov ax,_DATA ; создадим свой собственный сегмент данных mov ds,ax mov es,ax cli mov ss,stkseg ; восстановить указатель на .STACK mov sp,stkptr sti jc __error ; В противном случае EXEC завершится без ошибок push ds ; сохраним наши данные в сегмент ; Вызов OVERLAY части ; Вызов функций из оверлея это всегда FAR (об этом позже в этой статье) call dword ptr entry pop ds ; восстановим .DATA сегмент ... ... ... oname db 'OVERLAY.OVL',0 ; название файла pars dw 0 ; адрес сегмента для загрузки dw 0 ; релокации для файла entry dd 0 ; входная точка для overlay-части stkseg dw 0 ; сохранить Stack Segment stkptr dw 0 ; сохранить Stack Pointer
MS DOS использует функцию EXEC для загрузки оверлеев. Эта
функция, номер 0x4B, используется также для загрузки и запуска одной программы из другой, если поместить код 0x00 в AL.
Если в AL поместить код 0x03, то тогда будет загружен оверлей. В этом случае не создается PSP-сегмент, поэтому оверлей не устанавливается как независимая программа.
Уже бегу к моим баранам…
Значит, судя из листинга, достаточно знать название
оверлея, чтобы по относительному пути загрузить его в область перекрытия.
Перемещения
В спецификации исполняемого файла часто фигурирует поняние релокаций.
Скажу так… Иногда, случаются вредные ситуации, когда на этапе сборки программы нельзя совершенно точно сказать адрес области, информацию из которой вы бы хотели знать.
Отсюда следует потребность в своеобразных уточнениях линкером или сборщиком, «куда конкретно надо подойти?»
Релокация (англ. «relocation») — это информация о поправках к абсолютным адресам в памяти, созданная компилятором или ассемблером и хранящаяся в файле.
Про то, «что такое указатели», пожалуй говорить не буду. Это уже должны знать просто так, но конкретику про указатели на те времена, думаю немного разжевать.
Типы указателей (не обязательно)
Это можно пропустить, это я зашёл немножечко в сторону.
И так, согласно документации Borland, физическим, (или аппаратным), возможностям того времени, и тяжелым временам, в голове должно сложиться приблизительно следующее:
-
Максимальное машинное слово (англ. «CPU WORD») — 16-бит. (времена Intel i8086+);
-
Максимальное машинное слово, которое понимает ОС — 16-бит;
-
Весьма маленький объем ОЗУ и Program Memory;
-
.PSPсегмент (потому что этоPC/MS-DOS).
Теперь всё просто:
-
Не далёкие указатели (англ. «Near Pointers») — это указатели, которые помещаются в регистр. То есть 16-битные.
-
Далёкие указатели (англ. «Far Pointers») — это указатели, которые уже НЕ помещаются в регистр;
Сделаю акцент на последнем. Это важно.
Далёкие указатели это не просто адрес «куда-то» в 32-разрядном размере, а целая форма записи 16:16, что означало 16 бит адрес, 16 бит номер сегмента.
Из-за сегментной архитектуры x86, далёкий адрес строится таким образом:
let far_addr: u32 = (segment << 4) | offset (20 бит физический адрес)*
А чтобы его разобрать на составные части, придется делать уже две
операции, а не одну.
let segment: u16 = far_addr >> 16; let offset : u16 = far_addr & 0xFFFF;
После небольшого погружения в управление памятью, наконец-то можно говорить о самом формате исполняемого файла.
Mark Zbikowski Executable Format
Теперь, как один из важных вопросов закрыт, представляю вам то, что вы и без меня знаете.
Буквы MZ (шестн. 0x4D 0x5A) — это инициалы инженера Microsoft, который предложил и представил
альтернативу односегментным .COM программам.
Теперь любая программа имела в себе структуру MZ-заголовка
и её подопечные структуры данных, чтобы хранить информацию
о коде, данных и других жизненно необходимых её частях.
struct MzHeader{ // Стандартная (повсеместная) часть заголовка pub e_sign: Lu16, // подпись ZM или MZ pub e_cblp: Lu16, // последний блок pub e_cp: Lu16, // количество блоков/страниц pub e_relc: Lu16, // количество релокаций pub e_cparhdr: Lu16, // размер заголовка в блоках pub e_minep: Lu16, // мин. выделенной памяти в блоках pub e_maxep: Lu16, // макс. выделенной памяти в блоках pub ss: Lu16, pub sp: Lu16, pub e_check: Lu16, pub ip: Lu16, pub cs: Lu16, pub e_lfarlc: Lu16, // Сырое смещение таблицы релокаций // Расширенная/Дополнительная часть MZ-заголовка pub e_ovno: Lu16, // Текущая .OVL часть. (как было показано в предыдущей главе, скорее всего оно опционально) pub e_res0x1c: [Lu16; 4],// UInt16[4] линкер/компилятор или мусор pub e_oemid: Lu16, pub e_oeminfo: Lu16, pub e_res_0x28: [Lu16; 10], // UInt16[10] линкер/компилятор/OEM/мусор pub e_lfanew: Lu32, // Пока что равен нулю. }
Пройдусь по некоторым полям, которые мало кто хочет обозначать
-
Префикс
e_это «executable»; -
e_minepиe_maxepрасшифровываются как минимальное и запрашиваемое (а не максимальное) значение ожидаемой памяти в блоках (напримерe_maxep = maximum expected paragraphs); -
e_lfarlc— содежит сырое (или абсолютное) смещение от начала образа (с нуля), ровно до таблицы релокаций. -
e_ovnoрасшифровывается какoverlay's number, а не количество оверлеев, что напрямую говорит о том, что оверлеи тоже внутри себя хранят расширенный DOS заголовок-
0x0000— Главная программа (.EXEфайл) -
1+— Оверлей-часть программы (.EXEили.OVL);
-
-
e_res0x1C— массив зарезервированных байт, который долгое время был выделен для дальнейших полей, но (судя по всему) откладывался; -
e_res0x28— массив зарезервированных байт. В «идеальных» файлах является нулевым (везде хранит нули); -
e_oemidиe_oeminfoпрактически нигде не документированны, и возможно используются «как попало». Ожидается, что в них хранится уникальный номер и ссылка на информацию от производителя ПО, но эти поля так же не влияют на запуск.
Более того, DR-DOS в OEM полях хранит свои специальные данные.
Теперь скажу свои предположения касаемо загадочных пустот в заголовке.
Много где на форумах я видел, что «якобы компилятор или сборщик мог помечать специальные флаги для себя там». Я полагаю, такое следствие вполне себе может быть, и вполне оправдано, так как эти поля загрузчиком не проверялись. Мало того, эти поля могли быть использованы различным вредоносным ПО.
Исследуя компилятор Open Watcom я не смог найти чего-то интересного для
MZ заголовка, поэтому подтверждать гипотезу о компиляторах не осмелюсь пока что. А инструменты Borland и Watcom закрыты от глаз пользователей, к моему сожалению.
Поля до e_ovno встречаются во всех DOS, поэтому в некоторых кругах
DOS-заголовок означает только стандартные поля структуры.
А вся структура заголовка, какой она есть в PC-DOS, MS-DOS и других MS-DOS совместимых ОС, называется MZ-заголовок или в некоторых кругах — Расширенный DOS заголовок
Поле e_relc говорит количество релокаций в файле, а поле e_lfarlc
переводится как «location FAR address relocations» (предположительно).
Таблица релокаций выглядит очень просто:
struct MzRelocations { // Количество записей определяет "e_relc" // Формат записи 16:16 pub rec: [Lu32; e_relc], }
Некоторые источники говорят, что небезызвестное поле e_lfanew
появилось позже с появлением первого сегментного формата — «New Executable».
Отсюда, собственно и название поля: «LONG file address New…» (а дальше продолжите сами).
Внимание, разделение
Стоит помнить, что речь идет о временах, когда на уровне двоичных данных
не было понятия «секции»
Ещё очень много лет пройдёт, когда понятие «секции» появится у IBM и Microsoft. Поэтому после заголовков и таблиц релокаций в файле будут разграниченные области, но никаких .CODE или .DATA не будет!
Только начиная с IBM OMF формата (именно LX исполняемого файла), можно думать, что внутри бинарника существует подобие секций. Там фигурируют объекты, и объекты по природе безымянны, то есть не имеют явных имен .text и .bss, а содержат флаги, указывающие на их предустановленные права.
Для формата пораньше — Microsoft/IBM OMF (LE исполняемого файла), по идее, те же самые правила: есть объекты кода, и объекты безымянны.
Запуск
Здесь будет немного математики, но я постараюсь уложить это так, чтобы было проще читать.
В некоторых источниках, говорят, что PC-DOS 1.0 создавала область
.PSP (полн. «Program Segment Prefix») для исполняемых файлов. Я это пропущу мимо, пока что.
Операционной системе для загрузки программы надо знать список важных переменных
-
Размер программы;
-
Начальные Значения регистров для программы;
-
Начальный адрес загрузки образа;
-
MZ-Заголовок;
-
Релокации (исправления к специальным адресам)
Чтобы узнать размер программы (или образа программы), надо немножечко посчитать:
let image_size = (size = 0) match { true => pages * 512 false => (pages - 1) * 512 + size }
Чтобы посчитать точку входа в программу, тоже прийдется посчитать, а не брать сырые значения из заголовка.
let image_base = dos_psp_offset + e_cparhdr + 0x10
— Почему ещё плюс 16 байт?!
Объясняю; потому что e_cparhdr (или длина заголовка в блоках) измеряется в блоках, а сегменты смещаются на 16 байт.
Теперь часть посложнее. Поскольку заголовок считан, таблица PSP уже существует, и настроены регистры стэка, прийдется операционной системе посмотреть в таблицу релокаций файла.
Применяет исправления операционная система таким образом:
let base_address = load_address; // адрес загрузки for relocation in relocations { let target_ptr = base_address + relocation.offset as usize; let value = read_u16(target_ptr); // текущее значение write_u16(target_ptr, value + base_segment); // запись исправленного адреса }
Выводы
В целом, сам MZ заголовок и его данные выглядят и расшифровываются не трудно. Основная доля записей посвящалась истории и заметкам «Охотника за указателями».
Проблемы .EXE файлов проявляются с каждым форматом их сегментации, а их за время набралось немало.
У каждого формата свои достоинства и недостатки, а определять их можно по первому слову от e_lfanew смещения. (То есть указатель в e_lfanew покажет расположение следующей ASCII-подписи).
Источники
ссылка на оригинал статьи https://habr.com/ru/articles/939506/
Добавить комментарий