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.


Author | Alexey Tolstopyatov (21 y.0.)

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