NE-Executable | Importing modules and Procedures

This note is a next part of notes-cycle about Microsoft NE format for Windows 1.x/3.x and OS/2 1.x programs. This part contains more information about importing procedures, and modules in NE Windows executable format.

I’ve specially decided to write this article strongly after all experiments. I could have done it six months ago if I didn’t respect you. Because all my words must have proofs. And after all experiments with Microsoft LINK.EXE and Open Watcom 1.8, I’m ready to declare and determine all interesting things what I’ve found.

Remember the base

The segmented New Executable format is a first Microsoft solution which allows you to run programs in Intel 80286+ Protected Mode. Starts from Protected Mode the memory organization was the different.

All DOS executables (MZ Executables) works in Real Mode and have special memory characteristics.

For now (since 1985 year, as you know) the Microsoft Windows 1.01 was released. And the Microsoft Windows 1.01 was the second well-known source which active uses the New Executable segmented modules. (First source was a multitasking MS-DOS 4.0).

Modern terminology like an “Import” or “Export” in binary structures didn’t exist. Exporting procedures was read from .DEF file and rewrote as binary records inside the module by the linker. Exporting procedures has own name in “Resident” or “Not resident names” table, and the raw address has been in EntryTable.

This idea was fully described in previous article. In this article I want describe little more the process of 16-bit import.

Import Modules Table | Overview

The Importing Modules table have a e_imptab value in the NE header. This is a relative offset starting from NE Header.

let real_imptab: u16 = e_lfanew + e_imptab;

The Importing Modules are the strings in Resident Names table by the @0 ordinal. Name of dynamically linked library in file system may be different.

dosca11s.dll may contain DOSCALLS string by @0 and exactly this name is a module name.

The “Importing Modules Table” is a sequence of ASCII (not terminated) starting with special BYTE which tells count of bytes in the following C string.

Let’s look inside the Microsoft documentation:

The imported-name table follows the module-reference table. This table 
contains the names of modules and procedures that are imported by the 
executable file. Each entry is composed of a 1-byte field that 
contains the length of the string, followed by any number of 
characters. The strings are not null-terminated and are case 
sensitive.

The importing names table has a following format

BYTE     Length of the name string that follows. 
BYTE     ASCII text of the name string.

And let’s retranslate this structure to Rust pseudo code.

struct ImportRecord {
    pub e_cbname: u8,
    pub e_name: Vec::<u8>, // size = e_cbname
}

Great! You may think about “you can read imports now!”. But get it slow… really. Unfortunately, it’s not that simple, which is why I’m going to explain the unsaid words from the documentation.

Importing Modules Table | LINK.EXE and the Patience

I’ve specially decided to not start with Module References table just because I really want to demonstrate all ambigous Microsoft LINK.EXE behavior. If I were based on Microsoft documentation, I would never be able to read the imports correctly in my life.

First canonical thing, which I’ve found is a naming corruption. The previous article about exports tells more about this phemonemon. I’ll just tell it again. All resolved imports in the next will be UPPERCASE. And this is sad (but true). I suppose, this trouble concrete bound with exporting process. All procedures, what the application (in example CLOCK.EXE) requires, are the parts of Windows API, which store in KERNEL, GDI, USER executable modules.

As far as I know, toolkit for them was the Microsoft Macro Assembler (shorten MASM) and the Microsoft Linker. This will come back to us when we’re analyzing the binary content.

Story with the IBM OS/2 1.0+ unfortunately the same. Toolkit for Win-OS/2 middleware and the Win16 applications was made by Microsoft. (evil LINK.EXE is knocking again)

Second thing which I want to declare is an ambiugous filling of table. If you carefully read the Microsoft docs about it, you will define that names in the table follows one by one.
This is not true again!

Microsoft linker bases on another logic and this idea on the practice (if you will try to read imports) become a reason of strings tuncation or an incorrect unsafe strings (data may contains unreadable ASCII codes).

Importing Modules Table | How to see it?

Let’s look inside the binary for an answer. I represent you a specially “corrupted” variant of “Import Modules Table”.

00xx:0000 00 00 04 4D 41 53 4D 03   4E 45 53[00 00 00 00 00    | ...MASM.NLS.....
00xx:0000 00  |  | ..           |     ]06 4D  | 52 4E 45 4C    | ..........KERNEL
              |  |              |       |     |
              +-------> Head of import table. |
                 |      The sequence of names starts strongly from zero.
                 |      This is a good flag to determine start. 
                 |              |       |     |
                 |              |       |     |
                 +----> Count of chars in the Pascal string.
                                              |
                                              +--> Padding?!  

This is a working example of importing modules table for real. But unknown padding which nobody wants to see is reason of data truncation.

My assumptions about this padding are probably wrong, but they are there, and they will help you read the table correctly.

My ideas about this padding are spinning around the per-segment relocations
just because names offsets in the case of "Import by Name" fixups don't actually
known at the "one" linkage step. Linker makes the reserved space with unknown
for me data (in example count of ordinals per the segment)

Module References Table | Overview

The main tool in your hands is a module references table. It locates at the e_modtab value and relative too. Following this annoying logic here is a NEAR pointer

let real_modtab: u16 = e_lfanew + e_modtab;

Count of records in the module references table describes by the e_cbmod in NE header. Be carefully: This is exactly count of WORD records. I don’t know WHY? But for this you better know the b means not BYTEs and not bundles and not an actually size of table at all. This is a count of the offsets in the table

That’s all. Module References Table is a list of an offsets which enumerates from one.

struct ModuleReferenceRecord {
    pub e_modoff: u16;
}

Here is an example taken from Sunflower

| Index:2     | Offset:2    |
|-------------|-------------|
| 1           | 0x0001      |
| 2           | 0x0006      |
| 3           | 0x0013      |

Offsets in the module reference table are relative too. Starts from beginning of imptab.

let real_mod_by_index: u16 = e_lfanew + e_imptab + modtab[index - 1];

Then, you firstly must know the module references instead of importing names. It helps you avoid unexpected paddings.

Where is Importing Procedures hidden? | Microsoft LINK.EXE knows more than me…?

This part of an article the most interesting, I suppose.

You can send the Microsoft PDFs to the DeepSeek or another LLM with deep analysis by the source. Unfortunately for you, all what it suggest will be wrong. And all those suggestions will be wrong only by the one Microsofts logic issue.

For real there are two ways to resolve importing procedures exactly.

  1. Static Imports resolving due 0x80 high byte;
  2. Per-segment Relocations filtering.

First way I prefer to completely avoid just because results of this experiment always different and unsafe. (Any OS/2 binary could crash the editor after call). But Win16 applications resolves successfully.

I’ve found this way very hard and advise you to follow by the second way.

Second way strongly requires by you the understanding of per-segment relocations. Please open the previous articles about it, because the next information strictly depends on it.

Per-Segment relocations and Imports | My helping hand

As you remember, there are 2 huge sources of information:

  • EntryTable (with Non/Resident Names);
  • Segments and Per-segment relocations.

Let’s look deeper inside the per-segment relocations. I’m going to call Sunflower for this

Segments Table

The segment table contains an entry for each segment in the executable file.

### Segments

The number of segment table entries are defined in the segmented EXE header.
The first entry in the segment table is segment number 1. The following is the structure of a segment table entry. 

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

### Relocations Table for Segment #1 (.CODE)

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.
| ATP:1   | RTP:1   | RTP:s        | IsAdditive:f   | OffsetInSeg:2   | SegType:2   | Target:2   | TargetType:s   | Mod#:2   | Name:2   | Ordinal:2   | Fixup:s   |
|---------|---------|--------------|----------------|-----------------|-------------|------------|----------------|----------|----------|-----------|-----------|
| 0x2     | 0x0     | [Internal]   | [False]        | 0x598A          | 1           | 0x0        | [MOVABLE]      | 0x0      | @0       | 0x0         |           |
| 0x2     | 0x0     | [Internal]   | [False]        | 0x5B13          | 2           | 0x0        | [MOVABLE]      | 0x0      | @0       | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x57AB          | 0           | 0x0        | []             | 0x2      | @120     | 0x0         |           |
| 0x2     | 0x0     | [Internal]   | [False]        | 0x24C5          | 3           | 0x0        | [MOVABLE]      | 0x0      | @0       | 0x0         |           |
| 0x2     | 0x0     | [Internal]   | [False]        | 0x5AB5          | 4           | 0x0        | [MOVABLE]      | 0x0      | @0       | 0x0         |           |
| 0x3     | 0x2     | [Import]     | [False]        | 0x2F62          | 0           | 0x0        | []             | 0x1      | @0       | 0x8         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x4967          | 0           | 0x0        | []             | 0x1      | @8       | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x4DB1          | 0           | 0x0        | []             | 0x2      | @130     | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x49A9          | 0           | 0x0        | []             | 0x2      | @2       | 0x0         |           |
| 0x2     | 0x0     | [Internal]   | [False]        | 0x4E11          | 5           | 0x0        | [MOVABLE]      | 0x0      | @0       | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x3164          | 0           | 0x0        | []             | 0x1      | @14      | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x56FA          | 0           | 0x0        | []             | 0x2      | @137     | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x309A          | 0           | 0x0        | []             | 0x1      | @17      | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x493C          | 0           | 0x0        | []             | 0x2      | @10      | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x57EE          | 0           | 0x0        | []             | 0x2      | @138     | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x2E63          | 0           | 0x0        | []             | 0x2      | @144     | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x2098          | 0           | 0x0        | []             | 0x3      | @9       | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x450B          | 0           | 0x0        | []             | 0x4      | @1       | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x48EC          | 0           | 0x0        | []             | 0x3      | @10      | 0x0         |           |
| 0x5     | 0x0     | [Internal]   | [False]        | 0x126E          | 1           | 0x8300     | [MOVABLE]      | 0x0      | @0       | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x4917          | 0           | 0x0        | []             | 0x3      | @11      | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x534           | 0           | 0x0        | []             | 0x2      | @151     | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x4AF3          | 0           | 0x0        | []             | 0x6      | @1       | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x523D          | 0           | 0x0        | []             | 0x2      | @34      | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x392A          | 0           | 0x0        | []             | 0x2      | @163     | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x4E2A          | 0           | 0x0        | []             | 0x2      | @164     | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x52C6          | 0           | 0x0        | []             | 0x2      | @39      | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x2F8C          | 0           | 0x0        | []             | 0x6      | @8       | 0x0         |           |
| 0x5     | 0x0     | [Internal]   | [False]        | 0x12A6          | 1           | 0x9C00     | [MOVABLE]      | 0x0      | @0       | 0x0         |           |
| 0x5     | 0x0     | [Internal]   | [False]        | 0x200           | 2           | 0xFD00     | [MOVABLE]      | 0x0      | @0       | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x25E9          | 0           | 0x0        | []             | 0x7      | @7       | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x2557          | 0           | 0x0        | []             | 0x7      | @9       | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x5787          | 0           | 0x0        | []             | 0x2      | @53      | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x265D          | 0           | 0x0        | []             | 0x7      | @15      | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x5575          | 0           | 0x0        | []             | 0x2      | @57      | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x5791          | 0           | 0x0        | []             | 0x2      | @59      | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x58D7          | 0           | 0x0        | []             | 0x7      | @21      | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x5909          | 0           | 0x0        | []             | 0x7      | @22      | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x4FE3          | 0           | 0x0        | []             | 0x2      | @63      | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x23D0          | 0           | 0x0        | []             | 0x7      | @27      | 0x0         |           |
| 0x5     | 0x0     | [Internal]   | [False]        | 0x12BF          | 1           | 0xB500     | [MOVABLE]      | 0x0      | @0       | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x576B          | 0           | 0x0        | []             | 0x2      | @70      | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x5538          | 0           | 0x0        | []             | 0x2      | @71      | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x2408          | 0           | 0x0        | []             | 0x7      | @32      | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x54EA          | 0           | 0x0        | []             | 0x2      | @72      | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x43D0          | 0           | 0x0        | []             | 0x2      | @73      | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x52AC          | 0           | 0x0        | []             | 0x2      | @74      | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x42ED          | 0           | 0x0        | []             | 0x2      | @75      | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x5495          | 0           | 0x0        | []             | 0x2      | @77      | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x4DCD          | 0           | 0x0        | []             | 0x7      | @40      | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x43A4          | 0           | 0x0        | []             | 0x2      | @81      | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x43ED          | 0           | 0x0        | []             | 0x2      | @82      | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x256E          | 0           | 0x0        | []             | 0x7      | @46      | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x2620          | 0           | 0x0        | []             | 0x7      | @48      | 0x0         |           |
| 0x5     | 0x0     | [Internal]   | [False]        | 0x12B9          | 1           | 0xCE00     | [MOVABLE]      | 0x0      | @0       | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x5081          | 0           | 0x0        | []             | 0x2      | @95      | 0x0         |           |
| 0x3     | 0x1     | [Import]     | [False]        | 0x546D          | 0           | 0x0        | []             | 0x2      | @98      | 0x0         |           |
| 0x5     | 0x0     | [Internal]   | [False]        | 0x1274          | 1           | 0x5800     | [MOVABLE]      | 0x0      | @0       | 0x0         |           |

You see many records registered and interpret by Sunflower as [Import] Look at the table and find [Import] records with zero-ordinal (@0). Those records will be imports by name. Other imports having non-zero ordinal are imports by ordinal.

Main idea of this algorithm bases on the fact that “Ordinals in Relocations table already set”. Troubles what you may have to implement it -

  1. Position of Module Name;
  2. Position of Procedure Name.

Don’t think that table of relocations looks clearly like Microsoft wants. The RTP:s field is custom (my solution) and it retranslate the RTP:1 field from BYTE to expected name of relocation type.

So, select all imports which marked as IMPORT_BY_NAME or IMPORT_ORDINAL (check out RTP:1).

To find module name you must:

  1. Calculate the offset e_lfanew + e_modtab + 2 * (relocation.ModuleIndex - 1) and seek to it.
  2. Read u16 module Name Offset. dll_name_offset
  3. Calculate the offset e_lfanew + e_imptab + dll_name_offset and seek to it.
  4. Read the Pascal string (one u8 of length, that many bytes of character data) at that location. (Make sure to add the NUL terminator at the end of the string if you are using Pascal strings with C string functions)
  5. Seek to the location remembered in the first step.

To find the procedure name (import by name): To find the procedure name for an IMPORT_NAME relocation, do the following:

  1. Calculate the offset e_lfanew + e_imptab + relocation.NameOffset and seek to it. Read the Pascal string (one u8 of length, that many bytes of character data) at that location. (Make sure to add the NUL terminator at the end of the string if you are using Pascal strings with C string functions!) Seek to the location remembered in the first step.

You can imagine it like this:

Looking for an import                Calculate offset to the mod_name
+----------------+    +----------------+  modtab[i] +----------------+
| Relocation     | -> | ModuleIndex (i)| ---------->| Import Table   |
| ModuleIndex=_  |    | Offset=0x0006  |            | "DOSCALLS"     |
| Record         |    |                |            | Name Entry     |
| NameOffset=0x__|    +----------------+            +----------------+
+----------------+             |
                               v Calculate offset to the imp_proc_name
                      +----------------+
                      | Procedure Name |
                      | "DosWrite"     |
                      +----------------+

If relocation type equal the IMPORT_ORDINAL, it looks little simplier:

                                Calculate the offset mod_name
+----------------+    +----------------+    +----------------+
| Relocation     | -> | Module Table   | -> | Import Table   |
| ModuleIndex=2  |    | Offset=0x0006  |    | "DOSCALLS"     |
| Record         |    |                |    | Name Entry     |
| ImpOdrinal=0x10|    +----------------+    +----------------+
+----------------+    

Importing module: read uppercase PascalString by mod_name offset
Importing procedure: ImpOrdinal or @16 (this means 0x10)  

As you see, this idea requires filled correctly per-segment relocations already.

I suppose the Ghidra bases on the same algorithm but also holds special libraries or data files with symbols. That may be a reason of more informative output (in example you will see the USER!FatalExit / @1 instead of (USER!@1))

In the 2.0.0.0 build of Sunflower the “Sunflower.Ne” plugin has an error of bytes reinterpretation for relocations. In 2.1.0.0 I’ve fixed it and completely see the imports.

For example I’ve took the OS/2 1.1 installation program (SYSINST2.EXE). Let’s see the part of results of this procedure

### Resolved Imports of `SESMGR`

16-bit Imports processor bases at per-segment relocations. If segment has special bit in the byte-mask, next services will iterate preprocessed relocations records
| Name               | Ordinal   |
|--------------------|-----------|
| `@14`              | @14       |
| `@17`              | @17       |
| `@8`               | @8        |
| `DOSSMPMPRESENT`   | @0        | <-- DosSmPmPresent 
| `DOSSMSETTITLE`    | @0        | <-- DosSmSetTitle (I suppose SM means Session Manager)

### Resolved Imports of `KBDCALLS`

16-bit Imports processor bases at per-segment relocations. If segment has special bit in the byte-mask, next services will iterate preprocessed relocations records
| Name    | Ordinal   |
|---------|-----------|
| `@10`   | @10       |
| `@11`   | @11       |
| `@13`   | @13       |
| `@4`    | @4        |
| `@5`    | @5        |
| `@9`    | @9        |

### Resolved Imports of `MSG`

16-bit Imports processor bases at per-segment relocations. If segment has special bit in the byte-mask, next services will iterate preprocessed relocations records
| Name   | Ordinal   |
|--------|-----------|
| `@1`   | @1        |
| `@2`   | @2        |

### Resolved Imports of `QUECALLS`

16-bit Imports processor bases at per-segment relocations. If segment has special bit in the byte-mask, next services will iterate preprocessed relocations records
| Name   | Ordinal   |
|--------|-----------|
| `@1`   | @1        |
| `@8`   | @8        |

I advise you to memorize those 2 strings from SESMRG imports table. They are coming back later in the next region.

You can’t differ module names and procedure names | evil trick by LINK.EXE

If you seek for symbols in hex-view of NE segmented program You can find interesting phemonenon: I also demonstrate hex table of OS/2 1.x SYSINST2.EXE just because this file demonstrates it very colourful.

00 06 53 45 53 4D 47 52 0D 44 4F 53 53 4D 53 45 | _.SESMGR.DOSSMSE
54 54 49 54 4C 45 08 44 4F 53 43 41 4C 4C 53 08 | TTITLE.DOSCALLS.
4B 42 44 43 41 4C 4C 53 08 56 49 4F 43 41 4C 4C | KBDCALLS.VIOCALL
53 03 4E 4C 53 03 4D 53 47 00                   | S.NLS.MSG_


'_' is not ASCII code, this is a start and the end of Import module names

But if you look at this better, you will see very strange things. For a first: DOSSMSETTITLE is a procedure name instead of module name. But this is not differs explicit like in Portable Executable format programs. The DosSMSetTitle is a DOS Session Manager SetTitle function which will be defined incorrect (like DLL).

Remember previous tables with processed by relocation records are imports. Do you see the DosSMPMPresent in the dump? Yes, You don’t see it! But relocations caught this symbols correctly (without non-ASCII garbage). So this tells once for me:

Without expected training or knowledge of OS/2 and Win16
environment architecture you can't define symbols corrrectly!

And the Microsoft LINK.EXE not strongly differs symbols by table. It fills tables following the prepared relocation records too.

Therefore, if you don’t know about PC/MS-DOS Session Manager, you can make a mistake when define SESMGR like an function instead of DLL.

I’ll say it again: “This observations you can do without decompiling and disassembling procedures”. And I’ve done it avoiding disassembler. Sunflower plugin just reads and reinterprets binary data of segmented executable and translate it to markdown.

Beyond the Dynamic Linking | Runtime insertions

Remember that project linked by any format may have an imports and the extra objects data insertions. It strongly depends on

  • Dynamic Linking (make a FAR call to something “into the runtime”);
  • Static Linking (move functions from another classes or headers “into the compile-time”).

Symbols of statically linked procedures also exists, and if you link the binary which uses format output (e.g printf from (c)stdio.h) the signs about it also will be. But those static calls already stores inside the segments and never checks Sunflower plugin because it requires disassembling.

How the Operating System knows for who you make FAR call?

And IBM OS/2 and Microsoft Windows uses special abstractions for a userland for applications. If you want to call another library at the runtime stage, operating system represents you special functions which tries to find position of your function in memory. Pointer what you give by this call is not just FAR pointer.

Those objects in the meaning of Windows API calls the “Handles”. Hungarian notation for C/++ programmers unofficially holds special prefix for Windows handles.

auto hDevides = QueryDosDevices(...); // naming for example

And the hDevices marked as h because type of it will become a Win32 or Win16 HANDLE.

If you see the winevdm documentation by otya128, you might catch the pointers to procedures in relocation tables stores as HANDLE16. This is a special pointer types which WineVDM uses to make a real retranslated 16 -> 32 -> 64-bit call to existing dynamic (shared) object or a program.

In the End

Many sources which not copies the Microsoft documentation may help you to resolve unexpected problems. You can lookup the Sunflower sources and make sure that all what I described here fully repeats the Sunflower codebase.

Those materials what I’ve done for it bases strongly on the Sunflower reports and Hexadecimal view of programs. Detail analysis of raw data bases on the different materials (not Microsoft docs at all) but for a many people I specially try to open them eyes, what “official sources may be written badly.” I never used decompilers and disassemblers for Microsoft Windows and IBM OS/2 modules because they are secured under the law.

»


NE-Executable | EntryTable and Non/Resident Names

This note is a next part of notes-cycle about Microsoft NE format for Windows 1.x/3.x and OS/2 1.x programs. This part contains more information about EntryTable, resident and not-resident names in NE Windows executable format.

I really wanted to name this note like “Microsoft tries to make shared code!” because (I advice you thinking, that) NE format is a first format, which provides possibility to make shared objects (Microsoft and IBM names it like DLL).

P/S: I advice you to think about it for a time which spent on this note.

Some people tells me and some articles was showed that

  • Programs .EXE optionally uses procedures from library;
  • Libraries .DLL represents procedures for programs.

I want to believe and beeing agreed with it but this is not strong rule and results are variant, unfortunately. That’s why for next sections in this document I never tell about this bisection enough!

Overview | Exporting Names

This note will be very large because main task of it is describe shared (or exported) procedures declaration and usage.

Firstly I want to determine 2 types of exporting procedures in NE segmentation format. There are:

  • Resident procedures;
  • Not resident procedures.

Make an alias with PC-DOS or MS-DOS loading in memory. MS-DOS has “Resident Memory” in RAM which always holds and executes a functions of Operating System. So, MS-DOS makes a memory scope where starts and lives and “dies” user programs. This scope named “Transistend Memory” or “Program Memory”.

I suppose Microsoft followed same logic when format was designed.

Resident Names | Overview

The resident-name table follows the resource table, and contains this module’s name string and resident exported procedure name strings. The first string in this table is this module’s name. These name strings are case-sensitive and are not null-terminated.

The Resident names table has a following format:

BYTE        Count of bytes in ASCII string

BYTE array  ASCII not terminated string of procedure/resource name

WORD        Procedure or Resource Ordinal

Let’s replicate safe structure for this

struct ResidentNameRecord {
    pub r_cbname: Lu8,
    pub r_sname: [L8; 1], // for real length of slice will be r_cbname
    pub r_ordinal: Lu16,  // this is what I want to describe next in this note
}

Resident Names | LINK.EXE breaks down the brain

[!NOTE] For real, Microsoft LINK.EXE always makes resident names with only upper case (but why?).

If you will use LINK.EXE for making Win16 environment module - all resident procedures become upper case. For a next linear executable formats - LINK.EXE and LNK386.EXE not corrupts members of resident names table.

Sunflower plugin makes once table for resident and not resident names but marks every record in table for whom the procedure belongs.

I’ve taken example from Windows 1.01 - CLOCK.EXE file to demonstrate resident names in exactly program (not in .DLL).

Count Name Ordinal Name Table
5 CLOCK @0 [Resident]
5 ABOUT @1 [Resident]
12 CLOCKWNDPROC @2 [Resident]

If you look at the table better - you can see PascalCase naming corrupted by the Microsoft LINK.EXE. For real this is a famous ClockWndProc(...) -> HWND procedure.

Ordinals for procedures are 1-based (starts from one). Special ordinal like @0 is a LINK.EXE record for a project-name; This record has come from .def file and compiler and linker what name of executable will be before linking process.

 Pseudo-DEF file container
+-------------------------+
| type=EXE;               | Requires by
| name=clock;             | the LINK.EXE                 
| About @1                |-------------->[file]CLOCK.EXE
| ClockWndProc @2;        | to make...      |
| ...                     |                 |
+-------------------------+                 |
            +-------------------------------+
            |
            |
     CLOCK.EXE bytes       
    +-------+--------+
    | MZ Header      |
    | DOS stub       |
    | NE Header      |--[relative offset e_restab]---+
    | ...            |                               |
    | Resident names |-------> [09_CLOCK.EXE_0,05_ABOUT_1,12_CLOCKWNDPROC_2]
    | ...            |

Interesting thing, I’m thinking ABOUT record is a resource record, just because this is a next dialog .rc (meant “resource”) file markup (compiled to .RES of course), I suppose.

NonResident Names | Overview

Not resident names table contains exporting functions or procedures, which target module not uses. Those functions or procedures fully represents for next external programs or DLLs.

In the other words, (resource taken from Microsoft docs):

The nonresident-name table follows the entry table, and contains a module description and nonresident exported procedure name strings. The first string in this table is a module description. These name strings are case-sensitive and are not null-terminated.

The Nonresident names table has same format with Resident names table but I explicitly replicate it for you.

struct NonResidentRecord {
    pub n_cbname: Lu8,
    pub n_sname: [Lu8, 1], // for real sizeof(n_sname) = n_cbname
    pub n_ordinal: Lu8,
}

Next region what I really want to share with others is little repeating previous ResidentNames table pain.

NonResident Names | LINK.EXE makes strange things again

Did you see it? Read carefully previous definition by Microsoft. NonResident names also case-sensitive. But what if I tell you that fact for real is false. The Microsoft LINK.EXE also retranslate non-resident names to upper case.

I’m never seeing linked Win16 application which has really case-sensitive ASCII string record. But also, I tell you little more about it. Mirsosoft LINK.EXE and LNK386.EXE for LE/LX linked executables works strongly like the Microsoft NE format documentation tells.

The traditional example of big shared code base for Windows 1.01, 2.03, 3.x, is KERNEL.EXE or GDI.EXE program modules. Those titans are main Windows middleware between decice drivers and userland. They are maximum isolated and functions/procedures, what they have as helping hands for themselves isolated to. Those private (internal) procedures not uses public API what that titans presents for us. I suppose this is a main reason to hold app public procedures of KERNEL.EXE as non-resident records.

Let’s open Sunflower with KERNEL.EXE by Microsoft Windows 3.10 and lookup a NonResident names table:

Count Name Ordinal Name Table
50 Microsoft Windows Kernel Interface for 2.x and 3.x @0 [Not resident]
12 GLOBALUNLOCK @19 [Not resident]
12 ISTASKLOCKED @122 [Not resident]
12 GETLPERRMODE @99 [Not resident]
7 LSTRCPY @88 [Not resident]
7 _LCLOSE @81 [Not resident]
10 GLOBALLOCK @18 [Not resident]
14 LOCALCOUNTFREE @161 [Not resident]
9 ANSILOWER @80 [Not resident]
10 DISABLEDOS @42 [Not resident]
12 UNDEFDYNLINK @120 [Not resident]
17 GLOBALHANDLENORIP @159 [Not resident]
6 _LOPEN @85 [Not resident]
15 GLOBALLRUNEWEST @164 [Not resident]
5 CATCH @55 [Not resident]
13 GLOBALFREEALL @26 [Not resident]
8 ANSINEXT @77 [Not resident]
13 NOHOOKDOSCALL @101 [Not resident]
7 _LCREAT @83 [Not resident]
15 PATCHCODEHANDLE @110 [Not resident]
3 STO @108 [Not resident]
16 CALLPROCINSTANCE @53 [Not resident]
11 MEMORYFREED @126 [Not resident]
16 MAKEPROCINSTANCE @51 [Not resident]
12 SETERRORMODE @107 [Not resident]
14 ISWINOLDAPTASK @158 [Not resident]
7 _LLSEEK @84 [Not resident]
15 LOCKCURRENTTASK @33 [Not resident]
13 GETCODEHANDLE @93 [Not resident]
16 FREEPROCINSTANCE @52 [Not resident]

This is a little part of all not resident names. Count of records in this table about 160. This fact tells about detailed API that declared for the userland minimum.

And all this names have terrible case for reading. This upper case breaks down eyes and mind with the idea same with “but WHY?!”.

If you want to imagine “Where this table locates?”

 Pseudo-DEF file container
+-------------------------+
| type=EXE;               | Requires by
| name=clock;             | the LINK.EXE                 
| ClockGetTitle;          |-------------->[file]CLOCK.EXE
| ClockGetSeed;           | to make...      |
| ...                     |                 |
+-------------------------+                 |
            +-------------------------------+
            |
            |
     CLOCK.EXE bytes       
    +--------+----------+ absolute offset from top of file
    | MZ Header         | -------------+
    | DOS stub          |              |
    | NE Header         |              | WORD e_nrestab
    | ...               |              |
    | NotResident names |-------> [...13_CLOCKGETTITLE_1,14_CLOCKGETSEED_2...]
    | ...               |

In the next region of the note I want touch a little a question about how exports falls to the Resident or NonResident names tables.

But how to export procedures for NE executables? | insights of forgotten Borland project

For a proofs of my words, I specially found sources of program for a Win16. I suppose those sources was the demo of Windows 3.1 application. Also, I specially make a warning. Next file following by this paragraph is modified by me project .def file.

; SYSVALS.DEF module definition file
; Made: Charles Petzold
; Modified: CoffeeLake 2025
;------------------------------------

NAME           SYSVALS   WINDOWAPI ; LINK.EXE makes a SYSVALS @0 record

; LINK.EXE makes a non-resident record by @0 ordinal.
DESCRIPTION    'System Values Display (C) Charles Petzold, 1988' 

PROTMODE                           ; Flag will be written in NE header's e_aflags BYTE mask. 0x08 means PROTECTED_MODE_ONLY.
HEAPSIZE       1024                ; NE header will hold e_heap with 1 << 10 value.
STACKSIZE      8192                ; NE header will hold e_stack with 1 << 13 value

EXPORTS        ClientWndProc       ; Resident names table will hold the "CLOCKWNDPROC" record by the LINK.EXE incremental ordinal value.

Let’s proof my words with Sunflower report.

Count Name Ordinal Name Table
47 System Values Display (C) Charles Petzold, 1988 @0 [Not resident]
7 SYSVALS @0 [Resident]
13 CLIENTWNDPROC @1 [Resident]

As you will see, LINK.EXE really made incremental value for ClientWndProc Window registration procedure. (This is a based procedure for a raw Win16/Win32 windowed application). And the name of ClientWndProc became Pascal uppercase and now is breaking down the eyes… unfortunately.

EntryTable (or EntryPoints table) | Overview

In the beginning, I’ve warned that criminal document very large. Only after all headache what I’ve described in previous regions, I’m ready to tell about EntryTable. This structure differs with previous by self complexity.

Main characteristics of EntryTable in NE header for us are e_enttab and cb_enttab which tells offset and count of something what needed to be described here.

Firstly, read it careful. cb_enttab field tells not BYTEs count This field tells Count of EntryTable bundles.

Secondary, e_enttab contains relative offset to the start of EntryTable.

// u16 just because value of EntryTable offset
// guarantees no data turncation. Using of u32 is redundant.
let real_enttab: u16 = e_lfanew + e_enttab;

EntryTable contains records about exporting procedures in code (or data :D) segments. Don’t let you doubt, Forwarder entries (importing procedures fixups) will in linear executable format. For a Win16 NE linked applications EntryTable contains specially Exporting procedures or data structures.

EntryTable separated by the linker by “Entry Bundles”. Every entries bundle has own header which tells “How many entries here?” and “What kind of all entry contains here?”

Let’s see Microsoft docs about it.

The entry table follows the imported-name table. This table contains bundles of entry-point definitions. Bundling is done to save space in the entry table. The entry table is accessed by an ordinal value. ordinal number one is defined to index the first entry in the entry table. To find an entry point, the bundles are scanned searching for a specific entry point using an ordinal number. The ordinal number is adjusted as each bundle is checked. When the bundle that contains the entry point is found, the ordinal number is multiplied by the size of the bundle’s entries to index the proper entry. The linker forms bundles in the most dense manner it can, under the restriction that it cannot reorder entry points to improve bundling. The reason for this restriction is that other .EXE files may refer to entry points within this bundle by their ordinal number.

The EntryTable has following format

 
BYTE    Number of entries in this bundle. All records in one bundle 
        are either moveable or refer to the same fixed segment. A zero 
        value in this field indicates the end of the entry table. 

BYTE    Segment indicator for this bundle. This defines the type of 
        entry table entry data within the bundle. There are three 
        types of entries that are defined. 
            0x00 = Unused entries. There is no entry data in an unused 
            bundle. The next bundle follows this field. This is 
            used by the linker to skip ordinal numbers. 
            
            0x01-0xFE = Segment number for fixed segment entries. A fixed 
            segment entry is **3 bytes long** and has the following 
            format. (Fix up that size in your head. This information very important later)

            0xFF = Moveable segment entries. The entry data contains the 
            segment number for the entry points. A moveable segment 
            entry is **6 bytes long** and has the following format. 
            (Fix up this size too. This extremely needs little later.)

This is a header of every EntryBundle Next structure for each entry point depends on segment’s indicator. If entry is .FIXED (more than 0x00 strongly less than 0xFF) The next is a format of each point in bundle:

BYTE    Flag word. 
        0x01 = Set if the entry is exported. 
        0x02 = Set if the entry uses a global (shared) data 
               segments. 

The first assembly-language instruction in the 
entry point prologue must be "MOV AX,data 
segment number". This may be set only for 
SINGLEDATA library modules. 

WORD    Offset within segment to entry point.

If segment indicator equals strongly upper BYTE’s (meant 0xFF) entry points record has following format

BYTE    Flag word. 
        01h = Set if the entry is exported. 
        02h = Set if the entry uses a global (shared) data 
              segments. 
INT 0x3F. 

BYTE    Segment number. 

WORD    Offset within segment to entry point.

If you have troubles with reinterpretation of moveable entry, I give you little hint: “The INT 0x3F instruction opcode is contstant for all entries what are moveable”. Those data better to store as raw bytes. (opcode: Lu8 and interrupt_code: Lu16). Interrupt opcode for I8086+ always equal 0xCD and raw WORD of an 0x3F interrupt code are not changing and reads by the executable’s loader as raw bytes to make something :D.

Let’s replicate bundle’s structure too.

struct EntryBundle {
    pub e_entries_count: u8,
    pub e_indicator: u8,
}

Next following entries in bundle depends on set e_entries_count and segment’s indicator. Imagine, this structure looks like an array


Entry Bundle #1
+-----------------+
| entries count <------count=2
| seg indicator   |<---type=FIXED
|+---------------+|       ||
|| flag=export   ||<-------+ This entry is FIXED
|| data=shared   ||       |  Entry @1 in this bundle is @1 in whole entry table.
|| seg=0x02      ||       |  This @1 actually named "ordinal"
|| offset=0xDD0  ||       |
|+---------------+|       |
|+---------------+|       |
|| flag=export   ||<------+ And this entry is FIXED
|| data=shared   ||         Entry @2 is a @2 in whole entry table too, following
|| seg=0x02      ||         this logic next. That's why it calls ordinals.
|| offset=0xDE2  ||
|+---------------+|
+-----------------+<== After this block (bundle)
                       follows Entry Bundle #2
And next Entry Bundle #2 will has unknown (used/unused)
records about entries and each entry in entry bundle #2
has global incremented ordinal (or index if you think it simplier).
Means, entries in bundle #2 starts from @3. Not from @1.

Also, I call the exorcist Sunflower for demonstrate KERNEL.EXE EntryBundles for this document.

### EntryTable Bundle #1

The linker forms bundles in the most dense manner it can, 
under the restriction that it cannot reorder entry points to improve bundling. 
The reason for this restriction is that other .EXE files may refer to entry points within this bundle by their ordinal number.

| Ordinal   | Offset   | Segment   | Entry    | Data type   | Entry type   |
|-----------|----------|-----------|----------|-------------|--------------|
| @1        | 65F8     | 1         | Export   | [Single]    | [FIXED]      |
| @2        | 2DBA     | 1         | Export   | [Single]    | [FIXED]      |
| @3        | 29AD     | 1         | Export   | [Single]    | [FIXED]      |



### EntryTable Bundle #2

The linker forms bundles in the most dense manner it can, 
under the restriction that it cannot reorder entry points to improve bundling. 
The reason for this restriction is that other .EXE files may refer to entry points within this bundle by their ordinal number.

| Ordinal   | Offset   | Segment   | Entry    | Data type   | Entry type   |
|-----------|----------|-----------|----------|-------------|--------------|
| @4        | 213B     | 2         | Export   | [Single]    | [MOVEABLE]   |



### EntryTable Bundle #3

The linker forms bundles in the most dense manner it can, 
under the restriction that it cannot reorder entry points to improve bundling. 
The reason for this restriction is that other .EXE files may 
refer to entry points within this bundle by their ordinal number.

| Ordinal   | Offset   | Segment   | Entry    | Data type   | Entry type   |
|-----------|----------|-----------|----------|-------------|--------------|
| @5        | 465A     | 1         | Export   | [Single]    | [FIXED]      |
| @6        | 46DC     | 1         | Export   | [Single]    | [FIXED]      |
| @7        | 483B     | 1         | Export   | [Single]    | [FIXED]      |
| @8        | 4891     | 1         | Export   | [Single]    | [FIXED]      |
| @9        | 48B5     | 1         | Export   | [Single]    | [FIXED]      |
| @10       | 4861     | 1         | Export   | [Single]    | [FIXED]      |
...
<--- Turncated here. Entry points in KERNEL.EXE about 160+

This is a markdown output of Sunflower plugin. I’ve marked this as text because it may be hard to see as independednt document.

EntryTable | LINK.EXE tries to organize exports

I want to ask you a little question:

  • What actially means “moveable” in the procedure context?

I’ve asked this question for myself and this question became a “critical stop” at my knowlegde bound.

I’m so sorry, that document became very long and annoying may be. But my main task is fully describe all problems in this segmentation and linkage processes.

Moveable entries calls like that because loader specific litrally tells about it. When image of NE segmented program loads in RAM all data about every takes from SegmentsTable and relocations if they are exists for each segment, are applies to make a correct pointers to expected entries.

Operating system can rebase some entry points in memory for free a segment to necessary data. Those entries which system can rebase are called “Moveable”.

System loader reads raw bytes of INT 0x3F instruction and Windows or OS/2 handles this interrupt and replaces INT 0x3F with far jump (or a far call) to required procedure.

Next following BYTEs in “Moveable” entry structure are special detains for operating system which helps to resolve problem with pointers.

Program which has moveable entry points usually loads slower, because executables loader calls OS for resolve moveable entry points in e_cbmovent count.

But when application has been loaded in RAM already CPU and IO bound by the moveable entry points became zero, just because OS has been resolved fragmentation problems.

This is a genius trick I suggest

EntryTable | I love and hate Microsoft LINK.EXE…

I have 2 ideas at this section. First idea:

  • If you had read this document carefully, you would have noticed that the entry bundles are not necessary for the layout of the “entry points”. Second idea is an antipode:
  • If you had read this document carefully, you would have noticed that’s why entry bundles are necessary for it.

My idea when I’ve read Microsoft documents was that first idea from previous paragraph. And I’ve really decide that entry bundles is a strange solution. But they are exists. And their existance are fully declared and described. But not for users of course. Users must doesn’t know about linkage process, this is a little dirty secret.

For real, I’ve found once application to this “bundles idea”. When you declare exporting procedures in definition (*.def) file of project, you can set ordinal for procedure manually. And this action is breaking down all Microsoft LINK.EXE logic after starting compiler.

What if i write you next idea: Microsoft Linker makes special spaces between entry points, marking this scope as .UNUSED entries bundle. That information needs only for loader and assembler, “how many records should be skipped?” and this is simplier way than infinite jumps between records in entry table.

Second interesting think, I’ve found, is an application of “entry table”. All this document along I’ve told “EntryPoints are procedures what shoud be exported”.

But for real this is not strong rule! EntryPoints offset is a pointer to something that shold be exported. And that’s all. No limits. Exporting entry in EntryTable can be an unsafe structure, or procedure or just label in program with x86 opcode.

This fact became a reason for interesting linkage of VxD drivers for example (but they are LE linked executables). Also this fact became an milestone of Microsoft Visual Basic 3.0/4.0 runtime data structures and definitions. And many interesting utilities used this too.

EntryTable | Why sizes so important?

Remember, I’ve written about important sizes. It’s a time to apply it for reading EntryTable.

///
/// Attempts to rewrite my logic. 
/// Algorithm mostly bases on Microsoft NE segmentation format.pdf
/// 
/// \param r -- binary reader instance
/// \param cb_ent_tab -- bundles count in EntryTable /see NE Header/
/// 
pub fn read_sf<R: Read>(r: &mut R, cb_ent_tab: u16) -> io::Result<Self> {
    let mut entries: Vec<SegmentEntry> = Vec::new();
    let mut bytes_remaining = cb_ent_tab;
    let mut _ordinal: u16 = 1; // entry index means ordinal in non/resident names tables

    while bytes_remaining > 0 {
        // Read bundle header
        let mut buffer = [0; 2];
        r.read_exact(&mut buffer)?;
        bytes_remaining -= 2;

        let entries_count = buffer[0];
        let seg_id = buffer[1];

        if entries_count == 0 {
            // End of table marker
            break;
        }

        if seg_id == 0 {
            // Unused entries (padding between actual entries)
            for _ in 0..entries_count {
                entries.push(SegmentEntry::Unused);
                _ordinal += 1;
            }
            continue;
        }

        // Calculate bundle size based on segment type
        let entry_size = if seg_id == 0xFF { 6 } else { 3 };
        let bundle_size = (entries_count as u16) * entry_size;
        
        if bundle_size > bytes_remaining {
            return Err(io::Error::new(
                io::ErrorKind::InvalidData,
                format!("Bundle size exceeds remaining bytes: bundle_size={}, remaining={}", 
                        bundle_size, bytes_remaining),
            ));
        }
        bytes_remaining -= bundle_size;

        // Process each entry in the bundle
        for _ in 0..entries_count {
            let entry = if seg_id == 0xFF {
                // Movable segment entry (6 bytes)
                SegmentEntry::Moveable(MoveableSegmentEntry::read(r)?)
            } else {
                // Fixed segment entry (3 bytes)
                SegmentEntry::Fixed(FixedSegmentEntry::read(r, seg_id)?)
            };
            entries.push(entry);
            _ordinal += 1;
        }
    }

    Ok(Self { entries })
}

Full source file available at win16ne repository. And this is a part of this file.

In the End

This document became extremely long. But this scope fully described. I hope it helps you to resolve problems. Really the most research work made not me and otya128. This project more technical than mine cycle of atricles. And for reverse engineers I really advise to read it.

»


NE-Executable | Segmented EXE Format New executable

The NE (fully “New executable”) is a 16-bit executable file format was designed by Microsoft for 16-bit Windows 1.x - Windows 3.x, Windows 9x, OS/2 1.x, multitasking MS-DOS 4.0 and the OS/2 sunset of Windows NT up to version 5.0 (Windows 2000).

This Segmented EXE format provides support for:

  • Segmented memory model;
  • Protected Mode (I286+) operation;
  • Dynamic linking;
  • Resource management;
  • Shared code between programs.

Overview

NE segmented program or library always starts like MZ-Executable. Means image has MZheader structure. Last field in structure - e_lfanew is a pointer to next NE header structure.

The NE header is an important detail which describes the entire subsequent structure of the image. An NE segmented executable typically contains about 7 tables (or unsafe structures) and segments which contain program’s code.

                    | DOS stub            |
                    +---------------------+
  relative offset   | ...                 |
  [1,24,6...]<------+ Module references   |
              +-----+ Importing names     | relative offset
relative offset     | Entry Table         +------------> [#1{...}][#2{...}...]
[...]<--------+     | Resident names      +-------+
                    | Not resident names  +----+  | relative offset
        +-----------+ Segments Table      |    |  +-----> [...]
        |           | Per-segment fixups  |    | absolute offset
        |           | Resources table     |    +--------> [...]
        |           ...                   |
 +------+--------------------+            |
 | #1 .CODE 0xBABE no_relocs |            |
 | #2 .CODE 0xFEED           +------>[#2 fixups table]
 | ...                       |            |
 +-------------+-------------+            |
              ||    |                     |
              ||    |                     |
              |+--->+---------------------+
              |     | #1 .CODE bytes      |
              +---->+---------------------+
                    | #2 .CODE bytes      |
                    | ...                 |
                    +---------------------+
                    | Resource #1         |
                    | Resource #2         |
                    | Resource #3         |
                    | ...
                    EOF
                    

NE Header

The NE header is a packed structure. Starts with ASCII fixed string [E, N] (little endian reinterpretation) or [N, E] (big endian reinterpretation). This is a main sign which indicates that the following values describe 16-bit Windows or OS/2 program.

#[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 Header has fields-masks which tells more about exploring binary image.

NE Header | LINK.EXE

Depending on the version of Microsoft Link, the processed file may have minor changes in NE segmentation format. That’s why images linked by LINK.EXE 4.x are have wrong reinterpretation of EntryTable LINK.EXE 5.10 instead

NE Header | e_flags

The e_flags field has 2 categories. Program and Application flags.

Program flags are a common module description which stores CPU, module type, flags.

//
// In 16-bit DOS/Windows terminology, DGROUP is a segment class that referring
// to segments that are used for data.
//
// Win16 used segmentation to permit a DLL or program to have multiple
// instances along with an instance handle and manage multiple data
// segments. This allowed one NOTEPAD.EXE code segment to execute
// multiple instances of the notepad application.
//
enum FlagWord {
    // how is data handled?
    NOAUTODATA = 0x0000,
    SINGLEDATA = 0x0001, // shared among instances of the same program
    MULTIPLEDATA = 0x0002, // separate for each instance of the same program

    // additional flags:
    LINKERROR = 0x2000, // Linker error, module can't be loaded
    LIBMODULE = 0x8000, // if this flag is set, this is a DLL;
                        // see the "Dynamic Libraries" section below
};

#define GLOBINIT  1 << 2    // global initialization
#define PMODEONLY 1 << 3    // Protected mode only
#define I8086     1 << 4    // 8086 instructions
#define I286      1 << 5    // 80286 instructions
#define I386      1 << 6    // 80386 instructions
#define I8087     1 << 7    // 80x87 (FPU) instructions

Application flags tells a program’s window behavoiur. Windows 3.x and Win-OS/2 uses this flags for running module

// Application flags
// 
// I suggest, Win-OS/2 or Windows 3.x uses this 
// information, so application flags tells how to work Windows
// OR OS/2 Presentation Manager. (shorten P.M.) 
// 
enum apptype {
    none,
    fullscreeen,    // fullscreen (not aware of Windows/P.M. API)
    winpmcompat,    // compatible with Windows/P.M. API
    winpmuses       // uses Windows/P.M. API
};

More information has Oracle VirtualBox driver for various operating systems. Oracle holds a C/++ sources of NE Header declaration

NE Header | e_os

Value that e_os holds is optional, because not all binaries was compiled for the IBM OS/2 environment. In old Windows 1.x executables e_os flag is zeroed what means 2 things:

  1. Target OS is unknown;
  2. Any OS supported (don’t think about unix).

Fonts compiled as NE executables have zeroed target os flag too.

enum targetos {
    Unknown = 0x00, // Any OS or unknown
    OS2     = 0x01, // IBM OS/2
    Win16   = 0x02, // Windows/286
    Dos4    = 0x03, // European DOS 4.x
    Win32s  = 0x04, // Windows/386
    Boss    = 0x05  // Borland OS service
}

Other structures?

Other structures will be in next pages, for each structure will be placed per one page. In future this region may contain links to other pages

»


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.

»


NE Executable | OS/2 CMD.EXE Sunflower analysis

All information that follows next is a generated by SunFlower and little changed (for right md rendering) markdown report. Target file was an OS/2 1.1 command-line client CMD.EXE taken from installation 1.44M diskette. This file represents a example of Sunflower work. Some articles or notes will be linked to this document. Just because this is a example “from our life” which demonstrate (not at all) structure of NE linked programs and libraries.

Sunflower Report

Generated at: 2025-09-06 19:55:40

Sunflower Win16-OS/2 NE IA-32

Image

Project Name: CMD Description: cmd.EXE

Hardware/Software

  • Operating system: OS/2
  • CPU architecture: I386
  • LINK.EXE version: 5.10

Loader requirements

  • Heap=1FA0
  • Stack=1FA0
  • Swap area=0000
  • DOS/2 CS:IP=0:0000
  • DOS/2 SS:SP=0:00B8
  • Win16-OS/2 CS:IP=0004:005C (hex)
  • Win16-OS/2 SS:SP=0006:0000 (hex)

[!TIP] Segmented EXE Header holds on relative EntryPoint address. EntryPoint stores in #4 segment with 0x5C offset

Entities summary

  1. Number of Segments - 6
  2. Number of Entry Bundles - 2
  3. Number of Moveable Entries - 0
  4. Number of Automatic Data segments - 6
  5. Number of Resources - 0
  6. Number of BYTEs in NonResident names table - 11
  7. Number of Module References - 7

Program Flags

How data is handled?

In 16-bit DOS/Windows terminology, DGROUP is a segment class that referring to segments that are used for data.

Win16 used segmentation to permit a DLL or program to have multiple instances along with an instance handle and manage multiple data segments. In example: allowed one NOTEPAD.EXE code segment to execute multiple instances of the notepad application.

  • SINGLE_DATA (shared among instances of the same program)

How application runs?

  • PROTECTED_MODE_ONLY

    Extra details?

Application Flags

This block (field) tells how windowing or not windowing wants to run

OS/2 Flags

Sunflower plugin shows this section if e_flagothers not zero. But I also suppose if appflags has OS2_FAMILY or e_os equals 0x1, what means OS/2 - you can read this section.

  • LONG_NAMES (avoid FAT rule 8.3 convertion)

Importing modules

All .DLL/.EXE module names which resolved successfully

  • \{0}\1B
  • \04j\{n}\02\06\{0}?\1F?\1F\\\{0}\04\{0}\{0}\{0}\06\{0}\06\{0}\07\{0}\0B\{0}@\{0}p\{0}p\{0}w\{0}?\{0}
  • \01\{0}\{0}\{0}\{0}\{t}\{0}\{0}\{0}\01\01\{0}\{0}\{0}\{0}\{0}\{0}\{0}\{0}\01\{0}?[\{0}\{r}?[0\{0}?c\{0}\{r}?cc\{0}?A\{0}\{r}?A?\{0}?\1F\{0}\{r}?\1F?\{0}?\1C\{0}\{r}?\1C?\{0}?\11A\{r}04\03CMD\{0}\{0}\{0}\01\{0}%\{0}.\{0}7\{0};\{0}?\{0}H\{0}
  • \{0}X\01\{0}\{0}\{0}\{0}\{t}\{0}\{0}\{0}\01\01\{0}\{0}\{0}\{0}\{0}\{0}\{0}\{0}\01\{0}?[\{0}\{r}?[0\{0}?c\{0}\{r}?cc\{0}?A\{0}\{r}?A?\{0}?\1F\{0}\{r}?\1F?\{0}?\1C\{0}\{r}?\1C?\{0}?\11A\{r}04\03CMD\{0}\{0}\{0}\01\{0}%\{0}.\{0}7\{0};\{0}?\{0}H\{0}\{0}\06SESMGR\{r}DOSSMSETTITLE\0EDOSSMPMPRESENT\08DOSCA
  • LS\08KBDCALLS\03MSG\03NLS\08QUECALLS\08VIOCALLS\{0}\{0}\07cmd.EXE\{0}\{0}\{0}\{0}\{0}\{0}\{0}\{0}\{0}\{0}\{0}\{0}\{0}\{0}\{0}\{0}\{0}\{0}\{0}\{0}\{0}\{0}\{0}\{0}\{0}\{0}\{0}\{0}\{0}
  • ``
  • ``
  • \{0}
  • @91
  • ?[0\{0}?c\{0}\{r}?cc\{0}?
  • \{0}\{r}?A?\{0}?\1F\{0}\{r}?\1F?\{0}?\1C\{0}\{r}?\1C?\{0}?\11A\{r}04\03CMD\{0}\{0}\{0}\01\{0}%\{0}.\{0}7\{0};\{0}?\{0}H\{0}\{0}\06SESMGR\{r}DOSSMSE
  • TITLE\0EDOSSMPMPRESENT\08DOSCALLS\08KBDCALLS\03MSG\03NLS\08QUECALLS\08VIOCALLS\{0}\{0}\07cmd.EXE\{0}\{0}\{0}\{0}\{0}\{0}\{0}\{0}\{0}\{0}
  • ``
  • ?[0\{0}?c\{0}\{r}?cc\{0}?
  • \{0}\{r}?A?\{0}?\1F\{0}\{r}?\1F?\{0}?\1C\{0}\{r}?\1C?\{0}?\11A\{r}04\03CMD\{0}\{0}\{0}\01\{0}%\{0}.\{0}7\{0};\{0}?\{0}H\{0}\{0}\06SESMGR\{r}DOSSMSE
  • TITLE\0EDOSSMPMPRESENT\08DOSCALLS\08KBDCALLS\03MSG\03NLS\08QUECALLS\08VIOCALLS\{0}\{0}\07cmd.EXE\{0}\{0}\{0}\{0}\{0}\{0}\{0}\{0}\{0}\{0}
  • ?cc\{0}?A\{0}\{r}?A?\{0}?
  • \{0}\{r}?\1F?\{0}?\1C\{0}\{r}?\1C?\{0}?\11A\{r}04\03CMD\{0}\{0}\{0}\01\{0}%\{0}
  • \{0}7\{0};\{0}?\{0}H\{0}\{0}\06SESMGR\{r}DOSSMSETTITLE\0EDOSSMPMPRESENT
  • DOSCALLS
  • KBDCALLS
  • MSG
  • NLS
  • QUECALLS
  • VIOCALLS

Module References

The module-reference table follows the resident-name table. Each entry contains an offset for the module-name string within the imported names table; each entry is 2 bytes long.

Reference#:4 Offset:2
1 0x0001
2 0x0025
3 0x002E
4 0x0037
5 0x003B
6 0x003F
7 0x0048

Segments Table

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. The following is the structure of a segment table entry.

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

Resident And NonResident Names

The resident-name table follows the resource table, and contains this module’s name string and resident exported procedure name strings. The first string in this table is this module’s name.

The nonresident-name table follows the entry table, and contains a module description and nonresident exported procedure name strings. The first string in this table is a module description. These name strings are case-sensitive and are not null-terminated.

Count Name Ordinal Name Table
7 cmd.EXE @0 [Not resident]
3 CMD @0 [Resident]
»