Дисклеймер
Я не гонюсь за звездами или голосами. Я пишу из очень большого интереса,
и обязан донести материал "как есть", стараясь избегать серьезных неточностей.
Вступление
Практически сразу, в PC-DOS 1.0, вместе с .COM
файлами, (или программами .COM
),
появились .EXE
файлы (полн. "EXEcutable" или "исполняемые"). Сегодня речь пойдет именно об этом.
Поскольку история происходит снова в Microsoft, запутаться можно очень легко, в любом месте.
Речь пойдет о много о чем в этот раз. И о первом формате сборки, то есть о знаменитом MZ
-заголовке и его подопечных. И о технических деталях. Но впервую очередь о том "Зачем же это надо?"
Небольшой обзор

Согласно википедии:
.EXE (полн. англ. executable — исполняемый) — расширение исполняемых файлов, применяемое в операционных системах DOS, Windows, Symbian OS, OS/2 и в некоторых других, соответствующее ряду форматов. Кроме объектного кода может содержать различные метаданные (ресурсы, цифровая подпись).
А сама программа .EXE
в понимании PC-DOS
это
Исполняемый файл - это размеченный образ, содержащий в себе таблицы данных, секции кода, данных, и фиксированную точку входа.
По сравнению с командами это уже не монолит кода. Это вполне себе книга, которая имеет название, содержание и главы.
Те кто знаком с PC/MS-DOS
любым образом, сразу держат в голове факт, что команда при вызове загружалась в "Program Memory" область и занимала какое-то там место, но помещалась там абсолютно вся. Из любой точки памяти процесса можно было "ткнуть" (или взять указатель) на любое место памяти процесса, без лишних телодвижений.
В чуть позже, это будет называться "Ближние указатели" (англ. "Near Pointers").
Я привык звать их "недалёкими".
Чтобы начать разговаривать о самом формате исполняемых файлов, придется
сначала подробно объяснить другие очень важные детали.
Взгляд на PC-DOS/MS-DOS
Обязательно держу в голове, что PC-DOS (MS-DOS тоже) - это ОС, работающая в реальном режиме процессора Intel, что предполагает использование адресного пространства размером всего лишь 1 МБ. Заодно все адреса физических устройств в памяти настоящие
Так-то памяти ещё меньше. Около 640 КБ
основной памяти, потому что старшие адреса памяти заняты под BIOS другие компоненты, хотя среди них попадаются разрозненные куски оперативной памяти, называемые UMB (т.е. верхний блок памяти) или (англ. "Upper Memory Block").
Расчерчу схему заполненной памяти
+----------------------------+ <--Конец
| Верхние блоки памяти (UMB) | |
+----------------------------+ | Это называют Транзитивной
| | | областью памяти
| Program Memory | |
| Область загрузки программ | |
| | <----+
+----------------------------+ <-+
| Резидентная часть | |
| (COMMAND.COM) | |
+----------------------------+ | В некоторых материалах это всё
| MS-DOS (MSDOS.SYS) | | является Резидентной областью памяти
+----------------------------+ |
| BIOS (IO.SYS) | |
+----------------------------+ <-+-0x0400
| Векторы прерываний |
+----------------------------+ <--Начало
В скобочках отмечены файлы, содержимое которых обрабатывалось и держалось
в блоках памяти после загрузки.
Взгляд на архитектуру
Меня в этой статье крайне интересует сегментная адресация у I8086+
, так как
далее она фигурирует абсолютно везде.
В основных терминах адресации I8086+ фигурирует параграф
Параграф = 16 байт. (или 0x10)
В контексте хх-DOS
(и её менеджера памяти):
Блок памяти - это непрерывный участок оперативной памяти, который программа может запросить у операционной системы для своих нужд.
Когда программа работает под управлением DOS, она не имеет прямого неконтролируемого доступа ко всей памяти. Вместо этого она обращается к операционной системе через системные вызовы.
Все операции с блоками памяти в понимании DOS должны считаться параграфами. (шестнадками). Или перефразирую так:
Все блоки памяти, которые нужно освободить или выделить считаются в параграфах.
Теперь разбор путаницы, которая меня застала и всё испортила.
Некоторое время в документации Microsoft будет фигурировать слово "Сегмент". Считайте, начиная со времен DOS, оно уже точно есть и активно используется.
Сегмент - это область памяти, начинающаяся с адреса, кратного 16 байтам. (то есть кратное параграфу)
Вот самое важное, что нужно мне надо было понять, что сегмент — это не фиксированный кусок памяти размером 64КБ. Это, в первую очередь, это метод адресации. Или способ вычисления физического адреса в памяти.
Типы указателей
И так, согласно документации 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 бит физический адрес)
// far_addr: u32 = (segment << 4) | offset // побитовое-ИЛИ лучше избегать
// Вот почему 20-разрядный адрес
let area = 2^20 = 1 048 576 // ровно 1МБ
А чтобы его разобрать на составные части, придется делать уже две
операции, а не одну.
let segment: u16 = far_addr >> 16;
let offset : u16 = far_addr & 0xFFFF;
Арифметика?
Intel решили в архитектуре 8086 использовать два 16-битных регистра (сегментный и регистр смещения) для формирования 20-битного адреса. Это позволило адресовать 1 МБ памяти (то есть см. выше)
Пробуем добрать до границы, которую ещё может "понять" машина.
BYTE
это слово равное 1 байту, а WORD
в i8086 это два байта.
Внутри себя BYTE
сможет содержать 255 различных значений, поскольку байт это 8 бит, а устройство понимает лишь два варианта сигнала: 0 и 1. (основание системы для счёта будет 2).
let byte_variants = 2^8 // или 256.
Логика у WORD
слова такая же, только уникальных значений она хранит больше.
let word_variants = 2^8 * 2^8 // 2^16 = 65 536
// Складывают показатели, основание остается неизменным
Где же небезызыестные 64 КБ? Почему они фигурируют?
Фигурируют они рядом. Достаточно максимальное значение 16-разрядного слова
разделить на 1024. Это будет ровно 64 КБ данных.
А теперь представьте, в программе есть две логические части, которые легко представить в виде
районов города:
Первая хранит сам код программы (будет районом Кода);
Вторая хранит строки или какие-то
hardcoded
(т.е. вшитые в код) данные. (Будет районом данных).
Процессор даже представить себе не может то, что программа как город, может быть разбита на районы.
Пока вы находитесь в одном районе, вы можете свободно ходить по всем домам от 0 до 65535. Если вы дойдете до дома №65535 и сделаете еще шаг, на горизонте не будет другой район!
Вы просто перескочите в начало этого же района — к дому №0. Где-то это называется переполнение (англ. wrap-around
), и это серьезная ошибка в программе, которая почти всегда приводит к ее краху.
Чтобы попасть в другой район (другой сегмент), надо сознательно и явно сменить название района в своей карте. То есть, программа должна выполнить команду, которая меняет значение сегментного регистра.
Проблема только одна. Программы-COM не могут содержать два района кода, у них на всё про всё рассчитано очень мало места, и ОС их загружает "как есть". А разработчик как архитектор города иногда вынужден выделять не один район кода.
Зачем EXE?
Здесь и раскрывается смысл появления исполняемых файлов.
EXE-программы по своей сути уже могут содержать несколько сегментов кода или данных.
Размер программы заведомо уже может быть больше 64КБ данных, но для прыжков по районам города
уже приходится пользоваться не просто указателями, а далёкими (англ. FAR) указателями, которые содержат не только смещение (или номер дома), но и район города тоже.
Поскольку программа (с точки зрения сущности в файловой системе) всё так же просто набор данных, операционная система должна как-то его распознать, и при необходимости, исправить некоторые адреса, чтобы уместить все её сегменты.
Перемещения
Внезапно промелькнули пару слов про какие-то исправления, ведь да? Значит пришло время объясниться.
Скажу так... Иногда, случаются вредные ситуации, когда на этапе сборки программы нельзя совершенно точно сказать адрес области, информацию из которой вы бы хотели знать.
Отсюда следует потребность в своеобразных уточнениях линкером или сборщиком, "куда конкретно надо подойти?"
Перемещение (англ. "relocation") — это информация о поправках к абсолютным адресам в памяти, созданная компилятором или ассемблером и хранящаяся в файле.
В новых программах (имеются ввиду .EXE
-программы) необязательно, но
будут храниться те самые перемещения.
DOS не будет загружать исполняемый образ "как есть", поскольку его структура больше и сложнее.
Об этом позже, но сразу скажу, что перед загрузкой образа в память придется много чего сделать.
Ещё пару разделов и наконец-то спецификация формата.
Остается ответить один важный вопрос
"Как быть, если программа в памяти требует больше места, чем можно себе позволить?"
Положим, есть EXE-программа размером 300 КБ. Её надо запустить её на компьютере с 256 КБ ОЗУ. Даже если сам процессор может адресовать 1 МБ, физически в компьютере просто нет 300 КБ свободной памяти подряд, чтобы загрузить всю программу целиком. Она просто не влезет.
Эту проблему решали по-разному. Один из способов, о котором дальше речь - это оверлеи.
Второй, более интересный - это DOS расширители. (англ. "DOS extender"). Вдруг внезапно встречали DOS4GW или DOS32a? Это программные DOS-extenderы.
Оверлеи
Согласно словарям и старым источникам:
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
-сегмент, поэтому оверлей не устанавливается как независимая программа.
Значит, судя из листинга, достаточно знать название
оверлея, чтобы по относительному пути загрузить его в область перекрытия.
Наконец-то теперь спецификация формата.
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 MzRelocation {
// Количество записей определяет "e_relc"
// Формат записи 16:16
pub offset: u16,
pub segment: u16
}
///
/// Внимание, это псевдо-инструкции
/// Читает таблицу перемещений, основываясь на смещении таблицы
///
pub fn get_relocs(...) -> Vec::<MzRelocation> {
let rel_vec: Vec::<MzRelocation> = Vec::<MzRelocation>::new();
reader.seek(e_lfarlc, 0); // <-- Смещение от 0 до таблицы релокаций
for rel_index in 0..e_relc {
// Записать релокации в таблицу
let rel: MzRelocation = reader.Fill::<MzRelocation>() // <-- Чтение и сдвиг позиции reader-a
rel_vec.push(rel); // <-- Запись
}
rel_vec
}
Некоторые источники говорят, что небезызвестное поле e_lfanew
появилось позже с появлением первого сегментного формата - "New Executable".
Отсюда, собственно и название поля: "LONG
file address New..." (а дальше продолжите сами).
Главный интересующий меня вопрос - это загрузка программ.
Запуск
Здесь будет немного математики, но я постараюсь уложить это так, чтобы было проще читать.
Поскольку речь идет о PC-DOS и MS-DOS, перед тем как загрузить программу, система
выделяет специальную область, которая называется PSP
-сегмент.
Рисовать ситуацию, как это выглядит в памяти - ненадо. Все просто. Но очень необычным ходом является следующее.
Операционной системе для загрузки программы надо знать список важных переменных
Размер программы;
Начальные Значения регистров для программы;
Начальный адрес загрузки образа;
MZ-Заголовок;
Релокации (исправления к специальным адресам)
Чтобы узнать размер программы (или образа программы), надо немножечко посчитать:
let image_size = (e_cblp = 0) match {
true => e_cp * 512
false => (e_cp - 1) * 512 + e_cblp
}
Чтобы посчитать точку входа в программу, тоже прийдется посчитать, а не брать сырые значения из заголовка.
let image_base = psp_offset + e_cparhdr + 0x10
Поле e_cparhdr
хранит размер заголовка в параграфах. Чтобы получить смещение к началу кода/данных после заголовка, это значение нужно умножить на 16 (что практически эквивалентно сдвигу на 4 бита влево, или +0x10
)
Теперь часть посложнее. Поскольку заголовок считан, таблица PSP уже существует, и настроены регистры стэка, прийдется операционной системе посмотреть в таблицу релокаций файла.
Применяет исправления операционная система таким образом:
let base_address = load_address; // адрес загрузки
for rel in rel_vec {
let target_ptr = base_address + rel.offset as usize;
let value = read_u16(target_ptr); // <-- текущее значение
write_u16(target_ptr, value + base_segment);// <-- запись исправленного адреса
}
Выводы
В целом, сам MZ заголовок и его данные выглядят и расшифровываются не трудно, но чтобы понять "зачем это надо?", пришлось много искать, и вдумчиво разбирать. Основная доля записей посвящалась истории и заметкам "Охотника за указателями".
Проблемы .EXE
файлов проявляются с каждым форматом их сегментации, а их за время набралось немало.
У каждого формата свои достоинства и недостатки, а определять их можно по первому слову от e_lfanew
смещения. (То есть указатель в e_lfanew
покажет расположение следующей ASCII-подписи).
Источники
Комментарии (36)
n99
21.08.2025 15:17Нашел этот пост в интернете, как раз сегодня всякое -related гуглю.
Пару ссылок на тулзы, непосредственно связанные с темой
http://old-dos.ru//index.php?page=files&mode=files&do=list&cat=235 EXE-распаковщики
https://archive.org/details/anormal-executable-tools_202107 ANORMAL's executable tools
Надо заметить, ЕХЕ под досом эта та еще наркомания, https://fasmworld.ru/uchebnyj-kurs/031-segmentnaya-adresaciya
Данную ссылку правильно было бы назвать "как работает досовский ЕХЕ", а не "сегментная адресация", потому что тема адресации не раскрыта совсем, и вместо этого показывают то, как это реализовано в экзешниках. То есть эта статья не про механику сегментов в процессоре, а о том, как это в результате отразилось на ЕХЕ-шниках, и то без объяснения что и почему.
Особенно "приятно" было встретить эти "межсегменты" (характерные манипуляции с регистром DS) в досовской игре paratrooper.COM, хотя игра чисто комовский файл, просто решили так извратно работать с памятью.
Format.com кстати это EXE, преобразованный в com, с циклом настройки релокаций перед запуском.
Про модели памяти здесь описано https://habr.com/ru/companies/timeweb/articles/880586
NeriaLab
21.08.2025 15:17К сожалению, по ссылке http://old-dos.ru//index.php?page=files&mode=files&do=list&cat=235 - там не все распаковщики, по факту их раза в 4 больше. У меня их точно штук 150, всяких разных
infund
21.08.2025 15:17В Турбо Паскале, помнится, была опция компилятора, с помощью которой можно было оверлеи делать. Давно это было. Сам никогда этим не пользовался, но возможность была.
SIISII
21.08.2025 15:17Было такое дело. Насколько помню, в самой ДОС поддержки оверлеев не было, и они реализовывались программами самостоятельно (в отличие от более вменяемых систем, где они шли "из коробки").
pda0
21.08.2025 15:17Как-то написал такую тестовую программу посмотреть как работают оверлеи, но практической потребности в них так и не возникло. Оверлеи фактически для 80086/8, на двойках уже был защищённый режим и 7+ паскаль мог делать программы под него.
SIISII
21.08.2025 15:17По сути, дело в объёме прямо доступной памяти. Оверлеи широко использовались на машинах, где логический адрес был ограничен 16-ю битами, т.е. программа в принципе не могла адресовать больше 64 Кбайт, даже если физически памяти было больше (скажем, на PDP-11 с MMU физический адрес составлял либо 18, либо 22 бита в зависимости от модели, но виртуальный/логический всё равно оставался 16-битным).
Inskin
21.08.2025 15:17Помню занимательный факт с тех времён - если в заголовке экзешника поменять Z и M местами (то есть заголовок будет не MZ, а ZM), то программа по-прежнему будет запускаться и работать :)
art2021 Автор
21.08.2025 15:17Я помню, что в исходниках какого-то Win16 приложения был комментарий про это. Дословно не передам, но мысль была такова:
ZM is very old DOS 1.0 sign
Я не стал освещать этот момент, потому что, уж об этом нюансе точно ничего нормального не найдешь.
pda0
21.08.2025 15:17Вещь в которой я не уверен на 100% за давностью лет. Exe, меньше определённого размера (возможно размера заголовка exe-файла) был бы запущен как com, даже при наличии MZ в начале.
SIISII
21.08.2025 15:17Не исключено. Но точно известно, что большой файл с расширением COM, имеющий заголовок MZ, будет загружаться и выполняться как EXE. Вероятно, если ОС смотрит на объём, то он должен быть как раз 64 Кбайта - 256 байт -- предельный размер настоящей COM-программы.
100h
21.08.2025 15:17Позанудствую.
"Program Memory"
Что это такое? Под МС-ДОС программа могла скопировать себя (или свою часть), или просто загрузить произвольный двоичный файл в любую область адресного пространства 8086, где существовало ОЗУ, допускающее чтение и запись, и выполнить переход на адрес, находящийся в этой области. И никто ничего бы не заметил, никакой защиы памяти, равно как и деления на области чтение/запись/исполнение не было.
Теоретически, программа могда исполняться даже из неиспользуемых страниц видеопамяти, где отсутствовал риск быть затертой выводом на экран.Этим пользовались вирусы, всякие хитрые резидентные программы и вполне официально - такая опция как BIOS in shadow RAM.
Также можно было поступить особо извращенно - снять read-only с ОЗУ, в котором находилась shadow-копия БИОСа, пропатчить код, установить read-only снова - и большинство антивирусов оказывались бессильны против такой модификации.
Так работали некоторые поздние русификаторы - вместо поддержки резидентными программами, БИОС патчился правильным знакогенератором.
Про указатели.
Помимо "близких" и "дальних" указателей, адресовать переменную или функцию можно было и по "короткому", a.k.a. "short" методу адресации, в предела -127..+128 байт от текущего адреса.Существует отличная переводная книга "Язык ассемблера для IBM PC и программирования" ("IBM PC Assembly Language and Programming"), где все эти нюансы расписаны наиподробнейшим образом, безо всяких недалеких указателей.
Оверлеи - так это вообще малая часть всей истории. К тому времени, когда программы выросли до таких размеров, что перестали помещаться в ОЗУ, уже появились аппаратные (exTended) и программные (exPanded) менеджеры дополнительной памяти.NeriaLab
21.08.2025 15:17А можно ссылку на "IBM PC Assembly Language and Programming"? Я нашел такую (eng) и такую (список)...
Хотя мне по вкусу философия Рэндэлла Хайда (Randall Hyde): "The Art of Assembly Language" (pdf)
100h
21.08.2025 15:17Ооо и я накосячил.
extended было начиная с 386, проц. работал в режиме virtual86.
expanded работало и на РС/XT, использовались спец. платы расширения с управляемым окном памяти в пределах первого мегабайта.
vicsoftware
21.08.2025 15:17BIOS in Shadow RAM - это несколько другое. BIOS не копировался в какой-то другой кусок памяти (видео или иное). BIOS оставался на тех же адресах, но на эти адреса вместо ROM аппаратным способом подключался кусок из RAM и BIOS копировался туда ещё на этапе загрузки.
SIISII
21.08.2025 15:17Оверлеи - так это вообще малая часть всей истории. К тому времени, когда программы выросли до таких размеров, что перестали помещаться в ОЗУ, уже появились аппаратные (exTended) и программные (exPanded) менеджеры дополнительной памяти.
Поправочка: это на ПК они являются малой частью истории. А если брать историю вычислительной техники вообще, то они широчайшим образом использовались все 1960-70-е годы.
art2021 Автор
21.08.2025 15:17Спасибо большое! Теперь к делу:
Что это такое?
Согласно описанию разметки ОЗУ в документах, у MS-DOS есть несколько разделов в ОЗУ
Ещё раз спасибо за такую справку. Готовлю исправленную статью
100h
21.08.2025 15:17Ок, действительно есть такой термин. Прикол в том, что резидентную часть command.com , а также большую часть IO.sys и msdos.sys можно было загрузить вообще за пределы 640К, если там имеется какое либо ОЗУ. И даже вообще за пределы адресного пространства 1Мб - это пресловутые ключи DOS = HIGH и DOS = UMB.
Строго говоря, для работы проца нужно обеспечить только валидную таблицу прерываний, первые 400h байт. Все остальное можно было перезаписать своей программой. Но при этом о возврате в ДОС можно забыть.
kmatveev
21.08.2025 15:17Привет! Я снова душнить пришёл.
Из-за сегментной архитектуры x86, далёкий адрес строится таким образом:
let far_addr: u32 = (segment << 4) | offset (20 бит физический адрес)*
А чтобы его разобрать на составные части, придется делать уже две
операции, а не одну.let segment: u16 = far_addr >> 16;
let offset : u16 = far_addr & 0xFFFF;Я очень рекомендую не использовать операцию or для склеивания segment и offset: если окажется, что offset больше, чем 15, то биты наложатся и будет очень плохо. Лучше использовать сложение. В операции разложения дальнего адреса на segment и offset у вас большие ошибки: segment надо сдвигать вправо не на 16, а на 4, а для получения offset делать & 0x000F.
И так, чисто для связности изложения: то, что у вас в формуле расчёта размера программы обозначено как pages - это e_cp из MZ-заголовка, а size - это e_cblp.
art2021 Автор
21.08.2025 15:17Приветствую! Спасибо за замечания. Скоро обновлю статью. Заодно комментари и в коде поставлю. (внезапно пропали при переносе сюда)
unreal_undead2
21.08.2025 15:17Какой то странный переход от ограничения ближних указателей к оверлеям. Код или данные в EXE могли быть больше 64k, при этом всё грузилось в память, но при необходимости использовались дальние указатели (в C компиляторах в связи с этим были разные модели памяти от tiny до huge с разными дефолтными типами указателей). Оверлеи нужны если программа не влезает в 640k.
drWhy
21.08.2025 15:17Вспомнилась утилита exe2bin.exe, которая могла преобразовать некоторые программы exe в com.
Ну и, конечно же, самая дорогая и функциональная программа всех времён и народов (если судит из соотношения к размеру), позволявшая повторно запустить ранее запускавшуюся .com программу, не имея файла! Программа продавалась за 7 фунтов, и однажды один из клиентов задал вопрос - почему так дорого, если объём программы 0 байт? Впрочем, он согласился, что программа заявленную функциональность выполняет.
unreal_undead2
21.08.2025 15:17Да, если компилировать C код в tiny модели (код и данные живут в одном общем сегменте, используются только ближние указатели), exe2bin стабильно отрабатывал.
SIISII
Точка входа в COM-программу -- не "везде", а строго фиксированная -- её первый же байт. Максимальный размер COM-программы -- не 64 Кбайта, а 64 Кбайта минус 256 байт (для PSP). И, кстати, это не "команда", это именно COM-файл или COM-программа.
А вот у EXE-программы точка входа как раз не фиксированная, а может находиться в произвольном месте, поскольку её положение указывается в заголовке файла или в иной структуре, уж не помню точно.
Раньше ДОСа расширение .EXE для обозначения выполняемых файлов точно использовалось в VAX/VMS -- "мамаше" Винды НТ. Но, вполне может быть, что она тоже не была первой (хотя "бабка" Винды -- RSX-11 -- использовала расширение TSK).
Вообще-то, эти указатели называются ближними, а не "недалёкими". А far pointers -- дальние указатели.
Неверно. В ДОСе EXEшки целиком загружались в память (точней, целиком загружались секции с кодом и данными; управляющая информация использовалась в процессе загрузки, но потом выкидывалась за ненадобностью для дальнейшей работы). Оверлеи же, если были, находились в другом файле -- о чём, собственно, Вы дальше пишите. Хотя у большинства ОС, поддерживающих оверлеи, они являются составной частью выполняемого файла, а не отдельным файлом.
Это не ABI, это API. API -- набор системных сервисов, которые ОС предоставляет прикладным программам, откуда и его название. ABI же -- это, по сути, соглашения о связях между подпрограммами (через какие регистры передаются параметры и всё такое). В одной и той же системе ABI может быть несколько -- скажем, в 32-разрядной Винде, кажется, было не меньше четырёх способов передачи параметров. А вот API -- он один, и правила передачи параметров в него одни (но могут совсем не совпадать с ABI используемых языков программирования, особенно "в древности", в т.ч. в МС ДОС: тамошние системные вызовы рассчитаны на вызов из ассемблерных программ и не могут быть напрямую вызваны из кода на языке высокого уровня, ведь требуется занесение информации в определённые регистры, причём разные для разных функций, и выдача команды INT, которую трансляторы не используют).
Тоже неверно. Программы делились на секции очень давно, задолго до появления МС. Правда, ИБМ таки была :) В частности, у неё программы точно делились на секции в DOS/360 и OS/360 -- а первая версия первой из них появилась на рынке вместе с первыми машинами Системы 360, в 1965 году (OS/360 в жутко кастрированном виде -- на год позже).
Кстати говоря, формат PE COFF, что в Винде, пошёл с VAX/VMS, т.е. с середины 1970-х.
Ну и relocations -- всё-таки перемещения, а не релокации...
Блоки по 16 байт в x86 называются параграфами.
art2021 Автор
Доброго времени суток! Благодарю за отзыв. Я пересмотрю всё и исправлюсь. Про OS/360 однозначно буду читать сегодня.
Я сколько понимаю, (почему я писал именно про binary interface, а не API):
Дело приходится иметь с уже собранными компонентами системы или внешними модулями, соответственно и вызов функции (или прыжок на метку, как это раньше было?) подразумевает под собой двоичный контекст.
API это условно список функций или служб, которые предоставляет программа для публичного использования.
После компоновки и сборки, программа содержит этот самый список функций, но чтобы его обозначить, надо явно говорить компилятору, что функции будут использоваться во-вне. Итого, как я понимаю, все-таки используется ABI компонента, поскольку это те самые явно отмеченные функции.
Так же сколько я понимаю, Соглашения о вызовах говорят "Что надо делать" до/после вызова функции, поэтому. Но используют их вроде бы уже в модуле, который импортирует что-то извне? Приношу извинения, если я не прав - помогите.
Я на эту тему находил много интересных разногласий, и сам не до конца понимаю мысль.
Есть материалы, где говорится, что программа-COM может загружаться с любого места.
(например https://habr.com/ru/companies/timeweb/articles/880586/)
Вполне уверен, что я мог ошибиться
Я исправлю моменты в статье, чтобы не отлынивать от терминологии, спасибо.
bolk
Статью не читал, но думаю имеется ввиду любой сегмент.
vicsoftware
Именно. В статье так и сказано.
"COM-образ загружается в любой сегмент памяти, причём, всегда со сдвигом 100h в рамках этого сегмента. "
Что в общем-то неудивительно, учитывая, что COM-образ всегда укладывается в один сегмент.
SIISII
Загружаться -- да, в любое место памяти (выровненное по границе параграфа), но это не значит, что у самого файла точка входа может находиться в произвольном месте. EXEшники, кстати говоря, тоже могут загружаться в любое место памяти (собственно, и загружаются: заранее ведь нельзя сказать, с какого физического адреса будет производиться загрузка).
Ну, когда говорят о вызовах системных сервисов из прикладных программ, говорят именно про API. Про ABI вспоминают при обсуждении правил передачи параметров между подпрограммами и тому подобных вещей, что к вызовам системных сервисов отношения не имеет (если подпрограмма A вызывает подпрограмму B, то они используют определённый ABI, при этом обе подпрограммы обычно являются частью одной программы). В общем, ABI -- про внутреннее устройство программы и взаимодействие её частей между собой, API -- про внешние связи между программами и ОС (или, шире, между разными программами или сервисами, или между, скажем, драйверами и функциями ядра ОС; во всех этих случаях общая суть в том, что один компонент обращается за услугами к другому компоненту, и оба этих компонента не являются частью одного модуля, т.е. что связи именно внешние).
Но вообще, эти термины, как и 95% других терминов, никак не регламентированы, из-за чего регулярно возникает путаница. Например, микроядерных ОС в природе не существует, если понимать этот термин так, как он понимался изначально; почти не существует сейчас и RISC-процессоров в изначальном смысле (в частности, ни ARM, ни RISC-V не являются RISC-процессорами, если исходить из того, что сделали разработчики первого RISC-процессора), ну и т.п. Лично я это называю "инфляцией терминологии" (да и сам термин "инфляция" тоже подвергся инфляции: зачастую под ним понимают рост цен, хотя это разные вещи).
pda0
> Блоки по 16 байт в x86 называются параграфами.
"а сегменты смещаются на 16 байт"
Чувствую кто не знаком с сегментной адресацией - ничего не понял. :) В 16-бит x86 адресация была фактически 32-битной. 16 бит номер сегмента и 16 бит смещение. Но адресовал процессор всего 1 мегабайт. Дело в том, что сегменты (размером в 64 Кб, что следует из 16 бит смещения) шли не друг за другом, а накладывались друг на друга с шагом в 16 байт. Т.е. нулевой байт памяти имел адрес 0:0, шестнадцатый 0:16, но одновременно 1:0; семнадцатый 0:17 и 1:1, 32-й 0:32, 1:16 и 2:0 соответственно.
Делалось это по простой причине. В процессоре фактически был аппаратный доступ к элементам массива произвольного размера, через чтение по смещению: индекс умноженный на константу. И если у вас были большие массивы, вы не хотели бы, чтобы они пересекли границу сегмента и вы лишились простого и удобного доступа к их элементам. Но шаг между сегментами в 16 байт позволял легко организовать выделение памяти, где смещение было бы равно нулю.
qw1
Размер файла определяется в MZ-заголовке 2-м и 3-м словом.
Всё, что за пределами этой черты, ОС не загружает в память, и этот остаток на жаргоне назывался "оверлеем" (там могли быть любые ресурсы, которые программа загружает по своему усмотрению).
У меня сейчас перед глазами файл TURBO.EXE (Turbo Pascal 7.0), его размер 0x628C7, размер в заголовке - 0x16060. На границе видно, что начинается "оверлей".
SIISII
Хм... Подзабыл о такой возможности, спасибо, что напомнили.