Вступление

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

Во-первых речь пойдет только о первом формате сборки, то есть о знаменитом MZ-заголовке и его подопечных.

Небольшой обзор

MS-DOS 2.0 снимок с WebArchive
MS-DOS 2.0 снимок с WebArchive

Согласно википедии:

.EXE (полн. англ. executable — исполняемый) — расширение исполняемых файлов, применяемое в операционных системах DOS, Windows, Symbian OS, OS/2 и в некоторых других, соответствующее ряду форматов. Кроме объектного кода может содержать различные метаданные (ресурсы, цифровая подпись).

А сама программа .EXE в понимании PC-DOS это

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

По сравнению с командами это уже не монолит кода. Это вполне себе книга, которая имеет название, содержание и главы.

Я приведу таблицу, и там детальнее покажу все особенности.

Характеристики

.COM

.EXE

Точка входа

Везде (любое место)

Фиксированная

Максимальный размер

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") для исполняемых файлов. Я это пропущу мимо, пока что.

Операционной системе для загрузки программы надо знать список важных переменных

  1. Размер программы;

  2. Начальные Значения регистров для программы;

  3. Начальный адрес загрузки образа;

  4. MZ-Заголовок;

  5. Релокации (исправления к специальным адресам)

Чтобы узнать размер программы (или образа программы), надо немножечко посчитать:

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-подписи).

Источники

Комментарии (26)


  1. SIISII
    21.08.2025 15:17

    Я приведу таблицу, и там детальнее покажу все особенности.

    Точка входа в COM-программу -- не "везде", а строго фиксированная -- её первый же байт. Максимальный размер COM-программы -- не 64 Кбайта, а 64 Кбайта минус 256 байт (для PSP). И, кстати, это не "команда", это именно COM-файл или COM-программа.

    А вот у EXE-программы точка входа как раз не фиксированная, а может находиться в произвольном месте, поскольку её положение указывается в заголовке файла или в иной структуре, уж не помню точно.

    Раньше ДОСа расширение .EXE для обозначения выполняемых файлов точно использовалось в VAX/VMS -- "мамаше" Винды НТ. Но, вполне может быть, что она тоже не была первой (хотя "бабка" Винды -- RSX-11 -- использовала расширение TSK).

    В чуть позже, это будет называться "НеДалёкие указатели" (англ. "Near Pointers").

    Вообще-то, эти указатели называются ближними, а не "недалёкими". А far pointers -- дальние указатели.

    Поскольку, размер образ .EXE файла может быть значительно больше, (уже за пределами ОЗУ), -- в ОЗУ полностью он просто не поместится, а в "Program Memory" уж и подавно нет.

    Неверно. В ДОСе EXEшки целиком загружались в память (точней, целиком загружались секции с кодом и данными; управляющая информация использовалась в процессе загрузки, но потом выкидывалась за ненадобностью для дальнейшей работы). Оверлеи же, если были, находились в другом файле -- о чём, собственно, Вы дальше пишите. Хотя у большинства ОС, поддерживающих оверлеи, они являются составной частью выполняемого файла, а не отдельным файлом.

    В самом ABI системы

    Это не ABI, это API. API -- набор системных сервисов, которые ОС предоставляет прикладным программам, откуда и его название. ABI же -- это, по сути, соглашения о связях между подпрограммами (через какие регистры передаются параметры и всё такое). В одной и той же системе ABI может быть несколько -- скажем, в 32-разрядной Винде, кажется, было не меньше четырёх способов передачи параметров. А вот API -- он один, и правила передачи параметров в него одни (но могут совсем не совпадать с ABI используемых языков программирования, особенно "в древности", в т.ч. в МС ДОС: тамошние системные вызовы рассчитаны на вызов из ассемблерных программ и не могут быть напрямую вызваны из кода на языке высокого уровня, ведь требуется занесение информации в определённые регистры, причём разные для разных функций, и выдача команды INT, которую трансляторы не используют).

    Стоит помнить, что речь идет о временах, когда на уровне двоичных данных
    не было понятия "секции"

    Ещё очень много лет пройдёт, когда понятие "секции" появится у IBM и Microsoft. Поэтому после заголовков и таблиц релокаций в файле будут разграниченные области, но никаких .CODE или .DATA не будет!

    Тоже неверно. Программы делились на секции очень давно, задолго до появления МС. Правда, ИБМ таки была :) В частности, у неё программы точно делились на секции в DOS/360 и OS/360 -- а первая версия первой из них появилась на рынке вместе с первыми машинами Системы 360, в 1965 году (OS/360 в жутко кастрированном виде -- на год позже).

    Кстати говоря, формат PE COFF, что в Винде, пошёл с VAX/VMS, т.е. с середины 1970-х.

    Ну и relocations -- всё-таки перемещения, а не релокации...

    -- Почему ещё плюс 16 байт?!

    Объясняю; потому что e_cparhdr (или длина заголовка в блоках) измеряется в блоках, а сегменты смещаются на 16 байт.

    Блоки по 16 байт в x86 называются параграфами.


    1. art2021 Автор
      21.08.2025 15:17

      Доброго времени суток! Благодарю за отзыв. Я пересмотрю всё и исправлюсь. Про OS/360 однозначно буду читать сегодня.

      Это не ABI, это API. API -- набор системных сервисов, которые ОС предоставляет прикладным программам, откуда и его название. ABI же -- это, по сути, соглашения о связях между подпрограммами (через какие регистры передаются параметры и всё такое).

      Я сколько понимаю, (почему я писал именно про binary interface, а не API):
      Дело приходится иметь с уже собранными компонентами системы или внешними модулями, соответственно и вызов функции (или прыжок на метку, как это раньше было?) подразумевает под собой двоичный контекст.

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

      После компоновки и сборки, программа содержит этот самый список функций, но чтобы его обозначить, надо явно говорить компилятору, что функции будут использоваться во-вне. Итого, как я понимаю, все-таки используется ABI компонента, поскольку это те самые явно отмеченные функции.

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

      Точка входа в COM-программу -- не "везде", а строго фиксированная -- её первый же байт. Максимальный размер COM-программы -- не 64 Кбайта, а 64 Кбайта минус 256 байт (для PSP). И, кстати, это не "команда", это именно COM-файл или COM-программа.
      А вот у EXE-программы точка входа как раз не фиксированная, а может находиться в произвольном месте, поскольку её положение указывается в заголовке файла или в иной структуре, уж не помню точно.

      Я на эту тему находил много интересных разногласий, и сам не до конца понимаю мысль.
      Есть материалы, где говорится, что программа-COM может загружаться с любого места.
      (например https://habr.com/ru/companies/timeweb/articles/880586/)

      Вполне уверен, что я мог ошибиться

      Вообще-то, эти указатели называются ближними, а не "недалёкими". А far pointers -- дальние указатели.

      Я исправлю моменты в статье, чтобы не отлынивать от терминологии, спасибо.


      1. bolk
        21.08.2025 15:17

        Есть материалы, где говорится, что программа-COM может загружаться с любого места

        Статью не читал, но думаю имеется ввиду любой сегмент.


        1. vicsoftware
          21.08.2025 15:17

          Именно. В статье так и сказано.

          "COM-образ загружается в любой сегмент памяти, причём, всегда со сдвигом 100h в рамках этого сегмента. "

          Что в общем-то неудивительно, учитывая, что COM-образ всегда укладывается в один сегмент.


    1. pda0
      21.08.2025 15:17

      > Блоки по 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 байт позволял легко организовать выделение памяти, где смещение было бы равно нулю.


  1. 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


    1. NeriaLab
      21.08.2025 15:17

      К сожалению, по ссылке http://old-dos.ru//index.php?page=files&mode=files&do=list&cat=235 - там не все распаковщики, по факту их раза в 4 больше. У меня их точно штук 150, всяких разных


      1. n99
        21.08.2025 15:17

        Ознакомьтесь с Anormal Pack, там 2гб архив с подобным добром )


        1. NeriaLab
          21.08.2025 15:17

          Благодарю за совет. Посмотрю. Может что-то и пригодится для следующей части моей статьи


  1. infund
    21.08.2025 15:17

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


    1. SIISII
      21.08.2025 15:17

      Было такое дело. Насколько помню, в самой ДОС поддержки оверлеев не было, и они реализовывались программами самостоятельно (в отличие от более вменяемых систем, где они шли "из коробки").


    1. pda0
      21.08.2025 15:17

      Как-то написал такую тестовую программу посмотреть как работают оверлеи, но практической потребности в них так и не возникло. Оверлеи фактически для 80086/8, на двойках уже был защищённый режим и 7+ паскаль мог делать программы под него.


  1. Inskin
    21.08.2025 15:17

    Помню занимательный факт с тех времён - если в заголовке экзешника поменять Z и M местами (то есть заголовок будет не MZ, а ZM), то программа по-прежнему будет запускаться и работать :)


    1. bolk
      21.08.2025 15:17

      В статье это есть — в описании MzHeader


    1. art2021 Автор
      21.08.2025 15:17

      Я помню, что в исходниках какого-то Win16 приложения был комментарий про это. Дословно не передам, но мысль была такова:

      ZM is very old DOS 1.0 sign

      Я не стал освещать этот момент, потому что, уж об этом нюансе точно ничего нормального не найдешь.


      1. bolk
        21.08.2025 15:17

        Это просто инвертированные инициалы. Они буквально есть в статье в описании MzHeader.


        1. art2021 Автор
          21.08.2025 15:17

          Да, они намеренно там есть. Просто хотел поделиться…


  1. pda0
    21.08.2025 15:17

    Вещь в которой я не уверен на 100% за давностью лет. Exe, меньше определённого размера (возможно размера заголовка exe-файла) был бы запущен как com, даже при наличии MZ в начале.


  1. 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) менеджеры дополнительной памяти.


    1. NeriaLab
      21.08.2025 15:17

      А можно ссылку на "IBM PC Assembly Language and Programming"? Я нашел такую (eng) и такую (список)...

      Хотя мне по вкусу философия Рэндэлла Хайда (Randall Hyde): "The Art of Assembly Language" (pdf)



    1. 100h
      21.08.2025 15:17

      Ооо и я накосячил.
      extended было начиная с 386, проц. работал в режиме virtual86.
      expanded работало и на РС/XT, использовались спец. платы расширения с управляемым окном памяти в пределах первого мегабайта.


    1. vicsoftware
      21.08.2025 15:17

      BIOS in Shadow RAM - это несколько другое. BIOS не копировался в какой-то другой кусок памяти (видео или иное). BIOS оставался на тех же адресах, но на эти адреса вместо ROM аппаратным способом подключался кусок из RAM и BIOS копировался туда ещё на этапе загрузки.


  1. art2021 Автор
    21.08.2025 15:17

    Спасибо большое! Теперь к делу:

    Что это такое?

    Согласно описанию разметки ОЗУ в документах, у MS-DOS есть несколько разделов в ОЗУ

    взял из своей первой статьи

    Ещё раз спасибо за такую справку. Готовлю исправленную статью


    1. 100h
      21.08.2025 15:17

      Ок, действительно есть такой термин. Прикол в том, что резидентную часть command.com , а также большую часть IO.sys и msdos.sys можно было загрузить вообще за пределы 640К, если там имеется какое либо ОЗУ. И даже вообще за пределы адресного пространства 1Мб - это пресловутые ключи DOS = HIGH и DOS = UMB.

      Строго говоря, для работы проца нужно обеспечить только валидную таблицу прерываний, первые 400h байт. Все остальное можно было перезаписать своей программой. Но при этом о возврате в ДОС можно забыть.


  1. 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.