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 BYTE
s 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 PDF
s 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.
- Static Imports resolving due
0x80
high byte; - 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 -
- Position of Module Name;
- 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:
- Calculate the offset
e_lfanew + e_modtab + 2 * (relocation.ModuleIndex - 1)
and seek to it. - Read
u16
module Name Offset.dll_name_offset
- Calculate the offset
e_lfanew + e_imptab + dll_name_offset
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.
To find the procedure name (import by name): To find the procedure name for an IMPORT_NAME relocation, do the following:
- Calculate the offset
e_lfanew + e_imptab + relocation.NameOffset
and seek to it. Read the Pascal string (oneu8
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 BYTE
s 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 MZ
header 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:
- Target OS is unknown;
- 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
- Number of Segments -
6
- Number of Entry Bundles -
2
- Number of Moveable Entries -
0
- Number of Automatic Data segments -
6
- Number of Resources -
0
- Number of
BYTE
s in NonResident names table -11
- 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] |