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.