Формат "Новых исполняемых" файлов называют сегментным. Даже в официальной документации Microsoft её заголовок это "Сегментный Новый Исполняемый формат" (ориг. "Segmented NEW Executable Format").

Этот сегментный формат программ поддерживал

  • Сегментную модель памяти;

  • Защищенный режим Intel 286+

  • Динамичкую компановку (Dynamic Linking);

  • Управление ресурсами;

  • Возможность экспорта и импорта определенных процедур.

Технический Обзор

Скомпилированный и собранный проект для Windows 3x или OS/2 1.0+
Всегда начинается с MZ-заголовка. То есть первой меткой в файле всегда будет ASCII подпись MZ или ZM.

Поле, которое определенное время было специально занулено - e_lfanew стало указывать на следующий программый заголовок.
Нетрудно догадаться, что подпись в следующем заголовке NE или EN.
Я специально указываю два варианта подписи, так как это не повлияет на дальнейший анализ, но сильно расширит область определения.

Можно сказать, что NE-заголовок в программе одна из важнейших частей, если не самая важная. Он описывает детали, которые считывает загрузчик программ для загрузки этого "образа программы" в память.

Схематично, сам новый сегментный формат описывает всего 7 специфических частей, которые жизненно необходимы

  • Детали Заголовка;

  • Таблицу входных точек;

  • Таблицу сегментов;

  • Таблицу резидентных имен;

  • Таблицу не-резидентных имен;

  • Таблицу импортируемых модулей (проектов);

  • Таблицу ссылок на модули;

  • Таблицу ресурсов.

                    | MZ-Заголовок        |
                    | ...                 |
                    | e_lfarlc=0x40       | Всегда 0x40.
                    | e_ovno=0            | Не важно, чаще всего 0
                    | ...                 |
                    | e_lfanew -----------------+
                    +---------------------+     | 
                    |                     |     |
                    | DOS Заглушка        |     |
                    +---------------------+     |
                    | NE-Заголовок        |<----+
                    |                     |
                    | ...                 |
             +------+ e_modtab            |
             |+-----+ e_imptab            +-----+
             ||     | e_enttab            +-----|------> [#1{...}][#2{...}...]
[...]<--------+     | e_resntab           +------+
             |      | e_nrestab           +----+|| 
        +-----------+ e_segtab            |    ||| 
        |           |                     |    |||
        |           | e_rsrctab           |    |||
        |           ...                   |    |||
 +------+--------------------+            |    |||
 | #1 .CODE 0xBABE no_relocs |            |    |||
 | #2 .CODE 0xFEED           +------>[#2 релокации]
 | ...                       |            |    |||
 +-------------+-------------+            |    |||
              ||    |                     |    |||
              ||    |                     |    |||
              ||    +---------------------+    |||
              ||    | Резидентные имена  <-------+
              ||    +---------------------+     |
              ||    | Импорты            <------+
              |+--->+---------------------+    |
              |     | #1 .CODE            |    |
              +---->+---------------------+    |
                    | #2 .CODE            |    |
                    +---------------------+    |
                    | Файл ресурса #1     |    |
                    | Файл ресурса #2     |    |
                    | Файл ресурса #3     |    |
                    +---------------------+    |
                    | НеРезидентные имена <----+
                    EOF
                    

Так же, можно посмотреть на указатель таблицы релокаций.
Для NE, LE/LX, PE собранных файлов без какого-либо вмешательства
он всегда держит абсолютное смещение на 0x40 байт.

NE Заголовок | Структура данных

Заголовок NE это упакованная не выровненная структура. Всегда начинается с
ASCII подписи [N, E] или [E, N]. Это важный признак, который говорит
о том, что исследуемый файл скорее всего Windows или OS/2 16-разрядная программа.

#[derive(Debug, Clone, Copy, PartialEq, Eq, Pod, Zeroable)]
#[repr(C)]
pub struct NeHeader {
    pub e_magic: [u8; 2],
    pub e_ver: u8,        // LINK.EXE major version
    pub e_rev: u8,        // LINK.EXE minor version
    pub e_enttab: u16,
    pub e_cbent: u16,     // Count of entry bundles
    pub e_crc: u32,
    pub e_flags: u16,     // [program_flags][app_flags]
    pub e_autodata: u16,  // automatic DS (data segment) index
    pub e_heap: u16,      // Initial heap size
    pub e_stack: u16,     // Initial stack size 
    pub e_cs_ip: u32,     // CS:IP
    pub e_ss_sp: u32,     // SS:SP
    pub e_cbseg: u16,     // Count of segments
    pub e_cbmod: u16,     // Count of module references
    pub e_cbnres: u16,    // Size of Nonresident names table
    pub e_segtab: u16,
    pub e_cbres: u16, 
    pub e_resntab: u16,   // Resident names table
    pub e_modtab: u16,    // Module references table
    pub e_imptab: u16,    // Importing module names
    pub e_nrestab: u32,   // Non-Resident names table (raw offset)
    pub e_cbentmov: u16,  // Count of moveable entries 
    pub e_align: u16,     // Sector shift. (0 means 512)
    pub e_restab: u16,    // Resources table
    pub e_os: u8,         // Target OS
    // **OS/2 part** of header. (checks by IBM OS/2 Win-OS/2 module)
    pub e_flagothers: u8, // OS/2 flags for loader (e.g. HPFS/FAT naming) 
    pub e_pretthunk: u16, // Return Thunk offset
    pub e_thunk: u16,     // Segment reference thunk offset
    pub e_swap: u16,      // minimum code swap
    pub e_expver: [u8; 2],// Expected Windows version!
                          // (little endian reinterpretation!)
}

В этот раз речь пойдет именно о полях этого заголовка.

NE-Заголовок | LINK.EXE

Как всем известно, формат был сделан "на скорую руку", поэтому
читаемость файлов зависит от версии линковщика. По этой причине интерпретация и запись
таблицы входных точек (англ. EntryTable) у LINK.EXE 4.0 и LINK.EXE 5.0+ отличается.
Sunflower об этом предупреждает,
хотя настоящих доказательств такого феномена при себе не имею. Я лишь видел упоминания
в источниках.

NE-Заголовок | e_flags

Поле e_flags (в моей интерпретации это два байтовых поля e_pflags и e_aflags) на самом деле разделяется в документации на две категории.

  • Программные флаги (англ. "Program Flags");

  • Флаги "Приложения" (англ. "Application Flags").

Программные флаги это общие характеристики модуля которые
говорят простую но нужную линкеру и разработчику информацию:

// Перевод:
// В понимании 16-разрядной DOS/Windows, DGROUP это класс сегментов
// которые ссылаются на сегменты используемые для данных.
// 
// Win16 использовала сегментную модель памяти, 
// чтобы позволить DLL или программе иметь несколько экземпляров, 
// каждый со своим дескриптором экземпляра (instance handle), и 
// управлять несколькими сегментами данных. 
// 
// Это позволяло одному кодовому сегменту NOTEPAD.EXE 
// выполнять несколько экземпляров приложения "Блокнот".
enum FlagWord {
    // Как данные обрабатываются/должны обрабатываться?
    NOAUTODATA   = 0x0000, // Нет .DATA сегментов
    SINGLEDATA   = 0x0001, // Общий между всеми запущенными экземплярами программы
    MULTIPLEDATA = 0x0002, // Сегменты разделяются и являются различными для каждого экземляра

    // LINK.EXE флаги/дополнительные флаги
    LINKERROR = 0x2000, // Во время связывания произошла ошибка. 
    LIBMODULE = 0x8000, // Опциональный флаг; Красный флаг для определения регистров
                        // Если AX = 0, в случае DLL модуля -- модуль явно содержит ошибку и не загрузится.
                        // Для драйверов .DRV это не работает. 
};

#define GLOBINIT  1 << 2    // Глобальная инициализация
#define PMODEONLY 1 << 3    // Запускается только в защищенном режиме
#define I8086     1 << 4    // 8086 инструкции
#define I286      1 << 5    // 80286 инструкции
#define I386      1 << 6    // 80386 инструкции
#define I8087     1 << 7    // 80x87 (FPU) инструкции.

DGROUP можно представить как механизм,
который позволял запускать несколько копий программы,
используя один общий код, но изолированные данные для каждой копии.
Подробнее об этом будет в другой части.

Флаг MULTIPLEDATA, если обнаруживается, то являлся намеком на использование
DGROUP и в таком случае следует проверить поле e_autodata заодно.

e_autodata это сокращенное сочитание "Automatic Data Segment Index", что означает
номер сегмента в таблице сегментов. (нумерация начинается с 1). Условно, загрузчик использует
это значение, чтобы автоматически загрузить правильный сегмент данных в регистр DS
при создании нового экземпляра приложения

Флаг LIBMODULE говорит не просто, что файл является динамическим объектом.
Если модуль является динамической библиотекой .DLL,
то CS:IP в заголовке вместо этого является процедурой загрузки библиотеки DLL,
а SS:SP не используется, так как библиотека не получает собственный стек.
Он использует дескриптор модуля в AX, который представляет собой непрозрачный тип данных, представляющий EXE-файл или DLL-библиотеку при загрузке в память. При возврате он выдает AX!=0 для успешного выполнения и AX==0 для сбоя, что необычно, но очень важно.

Флаги "Приложения" описывают поведение окна, как не странно. Win-OS/2 и Windows 1.x-3x
использует их.

enum ApplicationFlag {
    None,           // Нет окна?/опционально
    FullScreen,     // Полный экран (Не избегать Windows/P.M. API)
    WinPMCompat,    // Соместимость с Windows/P.M. API
    WinPMUsage      // Активное использование Windows/P.M. API
};

Больше информации можно найти в Oracle VirtualBox или другой
документации (не от Microsoft).

NE-Заголовок | e_os

Значение e_os к сожалению опционально, и не все программы
или библиотеки его держат при себе.
Вы можете десериализовать структуру заголовка любого модуля
из Windows 1x и убедиться в том, что там этот флаг держит ноль.

Шрифты .FON скомпилированные для Windows 1.x-3x тоже держат это поле нулём.

enum targetos {
    Unknown = 0x00, // Любая ОС (не UNIX)
    OS2     = 0x01, // IBM OS/2
    Win286 = 0x02, // Windows/286
    Dos4    = 0x03, // Многозадачная DOS 4.x
    Win386= 0x04, // Windows/386
    Boss    = 0x05  // Borland OSS
}

NE-Заголовок | OS/2 часть

e_flagothers

Во многих источниках упоминалось, что новый заголовок
особенно на первых этапах разработки был меньше, и не имел поля
e_pretthunks, e_flagothers, и последующие. Говорят, что это "OS/2 часть заголовка". В структуре я обозначил их комментарием специально.

Предположительно, флаги e_flagothers проверяются или в случае e_os=0x01 (имеется ввиду флаг ОС держит значение OS/2), или только загрузчиком из OS/2 1.0+, что мне кажется более правдоподобным.

// e_flagothers содержит флаги
#define NOFAT 0x1   // OS/2 Игнорировать правило 8.3 (Стоит у модулей для HPFS)
#define PMODE 0x2   // OS/2 2.x Protected Mode исполняемый
#define PFONT 0x4   // OS/2 2.x Пропорциональные шрифты
#define GANGL 0x8   // OS/2 Область GANDLOAD (групповой загрузки)

Может для кого-то будет неожиданно здесь увидеть упоминание OS/2 2.0,
но IBM планировала продолжать начатое и вероятно взялась бы дорабатывать дальше
"новый" сегментный формат тоже. Именно из-за устройства OS/2 здесь идет речь о
каком-то "OS/2-защищенном режиме", но в большинстве проверенных файлов я не встречал
использование этого флага.

e_expver

Поле e_expver указывает ожидаемую версию Windows. Это позволяло системе определить, совместима ли программа с текущей версией системы. Согласно некоторым источникам, приложение, скомпилированное для Windows 3.1, могло отказаться загружаться в Windows 3.0. Но я лично полагаю, что не все так просто и проблема в немного другом.
Так же по значению e_expver можно угадать приблизительно в каком режиме ЦП
запускается программа.

e_expver[1]

(e_pflags & PMODE_ONLY) != 0

(e_flagothers & PFONT) != 0

Real Intel mode

Intel Protected mode

1, 2

FALSE

[x]

[ ]

1, 2

TRUE

[x]

[x]

3

FALSE

[x]

[x]

3

TRUE

[ ]

[x]

В пустых ячейках может быть любое значение.

e_pretthunks и e_thunk

Если флаг e_flagothers держит флаг GANGLOAD_AREA, тогда следующие части заголовка
интерпретируются иначе:

Поле e_pretthunks тоже проверяется только OS/2, по заверению документов и
имеет собой ввиду смещение для возврата из шлюзов (англ. "thunks") или начала GANGLOAD_AREA.

Если e_pretthunks является информацией о GANGLOAD области, то e_thunk хранит
размер этой самой области. Иначе это ссылка на сегмент.

Итого

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

Моя задача была начать об этом разговор и перевести на русский это. Сделать это как небольшой обзор.
Ведь на Хабре уже много кто делал обзор PE слинкованых программ?

Так же приложу свои же не переведенные части, где я дальше копаю
в этот формат сегментации.

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

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


  1. unreal_undead2
    26.09.2025 05:57

    Самое интересное - это как реально работает динамическая загрузка и вытеснение/подгрузка сегментов для NE (с учётом того, что оно поддерживалось и в реальном режиме Win 3.0 на 8086). Помню, это частично разбиралось в статьях в первых номерах русскоязычного PC Magazine в начале 90х.


    1. art2021 Автор
      26.09.2025 05:57

      Я пытаюсь к этому подойти. Через время выпущу пару статей о импортах и экспортах. Мне ужасно интересно было узнать как происходит весь процесс во время выполнения.