NE-Executable | Segments Table and per-segment Relocations

This note is a next part which expands terms and meaning of internal structures in Win16-OS/2 1.0+ NE segmented program. This part contains more information about Segments table in NE Windows executable format.

Overview

The “Segments Table” is a table which presents information about next the following code or data segments.

NE header holds 2 important values for “Segments Table”. Those fields interpret like “Segment table offset” and “Segments count”. Be careful. Not “Bytes count in segments table”. Just count of records.

Segments table offset is a relative. You need to know the position (or an offset) of NE header.

let real_segtab: u16 = e_lfanew + e_segtab;
// first value holds in MZ header.
// second value holds in NE header.

If you want to see official release of Microsoft docs about - I’m going to give those terms here how.

[!NOTE] The segment table contains an entry for each segment in the executable file. The number of segment table entries are defined in the segmented EXE header. The first entry in the segment table is segment number 1.

Segments have 2 variant of names depends on special bit in bit-mask.

  • 0x0000 is .CODE named segment;
  • 0x0001 is .DATA named segment.

That’s all. No any .BSS or read-only data and .your-naming named segments here.

Format of Segment record

Firstly it would be better to show an information like text:

TYPE  | Microsoft DESCRIPTION
------+--------------------------------------------------------------
WORD  |  Logical-sector offset (n byte) to the contents of the segment 
      |  data, relative to the beginning of the file. Zero means no 
      |  file data.
------+--------------------------------------------------------------
WORD  |  Length of the segment in the file, in bytes. Zero means 64K.
------+--------------------------------------------------------------
WORD  |  Flag word
      |  0x0000 = .CODE-segment type. 
      |  0x0001 = .DATA-segment type. 
      |  0x0010 = MOVEABLE Segment is not fixed. 
      |  0x0040 = PRELOAD  Segment will be preloaded; read-only if 
      |                    this is a data segment.  
      |  0x0100 = RELOC_INFO Set if segment has relocation records. 
      |  0xF000 = DISCARD Discard priority. 
------+--------------------------------------------------------------
WORD  |  Minimum allocation size of the segment, in bytes. Total size 
      |  of the segment. Zero means 64K.

I suppose zero values uses 64K instead just because 0x10000 value more than 16-bit machine word (limit at 0xFFFF).

Format of Segment record as a structure

All fields in segment’s record are described in previous region of document. Let’s try to replicate safe structure of a record.

struct Segment {
    pub e_seg_offset: Lu16,
    pub e_seg_length: Lu16,
    pub e_flags: Lu16,
    pub e_min_alloc: Lu16,
}

And try to memorize, that zero values of e_seg_length and e_min_alloc are not zero for real. They are meaning 0x10000 value what equals 64K exactly.

I use Sunflower to demonstrate the segments table reading and bytes reinterpretaion for this note. Target for the demo is an OS/2 1.1 CMD.EXE file from installation media.

Type:s #Segment:4 Offset:2 Length:2 Flags:2 Minimum Allocation:2 Characteristics:s
.CODE 0x1 0x1 0x5BCA 0xD00 0x5BCA WITHIN_RELOCS
.CODE 0x2 0x30 0x6388 0xD00 0x6388 WITHIN_RELOCS
.CODE 0x3 0x63 0x41A4 0xD00 0x41A4 WITHIN_RELOCS
.CODE 0x4 0x85 0x1FB9 0xD00 0x1FB9 WITHIN_RELOCS
.CODE 0x5 0x96 0x1CBF 0xD00 0x1CBF WITHIN_RELOCS
.DATA 0x6 0xA5 0x1191 0xD41 0x3430 WITHIN_RELOCS HAS_MASK PRELOAD

This table is a result of Sunflower plugin work and belongs to CMD.EXE. Other files has different segmentation and records in this table will be different too.

Every record in this table has special flag named WITHIN_RELOCS. If something in segment requires a “note” about relocation - the Microsoft LINK.EXE not set this flag in byte-mask and makes the next following structure.

See more carefull at the table and try to remember that “.DATA segments marked as PRELOAD are READ_ONLY”. That works like this.

Per-Segment Relocation records

“Per-segment relocations” or “Segment relocations” or “Fixup records” or “Per segment data” are having one meaning for NE linked program. Those terms may be in different articles and this fact is breaking down the brain.

[!NOTE] The location and size of the per-segment data is defined in the segment table entry for the segment. If the segment has relocation fixups, as defined in the segment table entry flags, they directly follow the segment data in the file.

Here’s part of Microsoft document about. Once thing what I’ve changed is a data types for this note. I’m afraid someone like me can interpret dw like DWORD the define WORD instead. That’s main reason to changes.

WORD    Number of relocation records that follow. 
        A table of relocation records follows. The following is the format 
        of each relocation record. 

BYTE    Source type. 
        0Fh = SOURCE_MASK 
        00h = LOBYTE 
        02h = SEGMENT 
        03h = FAR_ADDR (32-bit pointer) 
        05h = oFFSET (16-bit offset) 

BYTE    Flags byte. 
        03h = TARGET_MASK 
        00h = INTERNALREF 
        01h = IMPORToRDINAL 
        02h = IMPORTNAME 
        03h = OSFIXUP 
        04h = _ADDITIVE_ 

WORD    Offset within this segment of the source chain. 
        If the _ADDITIVE_ flag is set, then target value is added to 
        the source contents, instead of replacing the source and 
        following the chain. 
        
        The source chain is an 0xFFFF 
        terminated linked list within this segment of all 
        references to the target. 
        The target value has four types that are defined in the flag 
        byte field.  

Next data reinterpretation strongly depends on Flags Byte.

Per-Segment Relocation records | Internal Reference

Type of Internal Reference in relocations table means that next following information tells you which number of segment heeded to make a FAR jump or a FAR call (or callf).

INTERNALREF 
BYTE    Segment number for a fixed segment, or 0FFh for a 
        movable segment. 

BYTE    0

WORD    Offset into segment if fixed segment, or ordinal 
        number index into Entry Table if movable segment.

Hold in your head what the target value was an offset, and this next value is segment. That’s why CALLF and FAR pointers (16:16 aliases) are figured out.

struct InternalReferenceReloc {
    // head for each record
    pub r_srcs: Lu8,
    pub r_flag: Lu8,
    pub r_offset: Lu16,

    // internal reference bytes
    // You can combine this like once Lu16 value.
    pub r_segment: Lu8,
    pub r_always_zero: Lu8,
    pub r_offset_index: Lu16,
}

In summary bytes that belongs to internal reference have a size about 32-bit or 4 bytes.

Per-Segment Relocation | Import by name/ordinal

The import by ordinal and import by name are differs between only with meaning of first word.

For an importing entry by name words reinterpret like this:

WORD    Index into module reference table for the imported 
        module.

WORD    Offset within Imported Names Table to procedure name 
        string. 

But for importing entries by ordinal (anonymous imports) words reinterpretation looks little different:

WORD    Index into module reference table for the imported 
        module. 
WORD    Procedure ordinal.

And I have the greatest news! Those words in summary also 32-bits or 4-bytes for all. This means that the structure of relocation records doesn’t have paddings or sectors shiftings. Different flags types strongly means different constant-sized reinterpretated bytes! This helps you to see relocations table correct without bad code or bad solutions.

Let’s try to collect importing name fixup record in one safe structure:

struct ImportingNameReloc {
    pub r_srcs: Lu8,
    pub r_flag: Lu8,
    pub r_offset: Lu16,

    // internal reference bytes
    // You can combine this like once Lu16 value.
    pub r_modtab_offset: Lu16, // <-- offset for DLL name
    pub r_imptab_offset: Lu16, // <-- offset for procedure ASCII name
}

And let’s compile annonynous import fixup record into safe structure too.

struct ImportingOrdinal {
    pub r_srcs: Lu8,
    pub r_flag: Lu8,
    pub r_offset: Lu16,

    // internal reference bytes
    // You can combine this like once Lu16 value.
    pub r_modtab_offset: Lu16, // <-- index in modtab of DLL name
    pub r_procedure_ord: Lu16, // <-- value of ordinal
}

Little about Imports (might be skipped)

If you know about anonymous imports - see next region. So, if you read something about PE (full. “Portable Executable”) format of segmentation. It uses in Microsoft Windows NT for 32-bit and 64-bit linked code. You might saw importing entries in IAT (full “Importing Addresses Table”) or in ILT (full “Import Lookup Table”) entries named like @100.

This is an index of procedure in external module (.EXE or .DLL) which uses by linker to declare it for calls by other modules.

NE format is elder than PE and I advice you thinking about it like “Idea of ordinals came from here (meant NE)”. ~For a first time, of course~.

Usually, for each exporting procedure in module linker makes own unique (or incremental) index. This idea you will see in next article about resident and not resident names.

And as far as I know, there are always 2 ways to call importing function or procedure. This is a call by name (standard way) and call by ordinal (or call by procedure’s special index).

YOUR_MOD.DLL have 2 parts of sources
+------------------------------+       +---------------------------+
| YOUR_MOD.def                 |------>| type of module: EXE       |
+------------------------------+       | type of mem_model: compact|
| header_1.h -> header_1.c     |       | your_func_1 @1            |
| header_2.h -> header_2.c     |       | your_func_100 @100        |
| header_3.h -> header_3.c     |       | your_secret_func @14      |
| ...                          |       | ...                       |
+------------------------------+

Definitions file (*.def) was in Borland IDEs like special project file which tells to compiler and linker more information about what do you want to build.

Per-Segment Relocation | Operating System Fixup

And the last region of this document is an Operating System Fixups. In Microsoft docs declared only fixups for Intel FPU devides.

[!NOTE] OSFixup is a floating point instruction that Windows or OS/2 will “fix up” when the coprocessor is emulated

WORD    Operating System fixup type. 
        Floating-point fixups. 
        0x0001 = FIARQQ-FJARQQ 
        0x0002 = FISRQQ-FJSRQQ 
        0x0003 = FICRQQ-FJCRQQ 
        0x0004 = FIERQQ 
        0x0005 = FIDRQQ 
        0x0006 = FIWRQQ 

WORD    0x0000

Apparently the relocations containing the J are supposed to refer to the second byte of the command sequence (including relocations and interrupts supported by other platforms for completeness sake)

And this bundle has a same size with other types. Strongly 32-bits or 4 bytes.

In The End for segments and relocations

In this part, I’ve deconstructed and tried to describe the segments table and relocation records of the NE format. These structures are fundamental to understanding how 16-bit Windows and OS/2 managed memory and code execution in a segmented Intel x86 architecture.


Author | Alexey Tolstopyatov (21 y.0.)

Desktop developer and System developer enthusiast. Currently a student in Tomsk State University in the area of software for microprocessor systems.