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.