Examining OS/2 2.1 Executable File Formats
An inside look at 32-bit LX-style executables
John Rodley
John is president of AJR Co., located in Cambridge, MA and can be contacted on CompuServe at 72607,3142 or at [email protected].
Executable files are the end result of a massive collaboration of makefiles, source files, include files, compiler flags, linker flags, environment variables, definition files, resource files, and even source-control flags. Get something wrong in one of them, and even the prettiest algorithm morphs into a UAE.
Windows programmers who've forgotten to export a dialog window procedure, for instance, know all about this--the dialog comes up, runs (sort of), then crashes. You know you have to link with the DLL version of the run-time library (LIBCDLL.DLL), but you have this nagging suspicion you might still be catching the static run-time library. What you need is a tool that looks into the executable, telling you exactly what's going on inside by first dumping the Resident and Non-Resident Name Tables where the offending window procedure should appear, then the Imported Name Table where LLIBCDLL.DLL should appear.
SHOWEXE.C does just this. The original version of SHOWEXE, which exploded NE-style executables, was written by David Schmitt (PC Tech Journal, November, 1988). The updated version I present here does the same for 32-bit, flat-memory-model, LX-style executables. Although LX is documented in the IBM OS/2 32-bit Object Module Format and Linear Executable Module Format (available on CompuServe, type GO OS2SUP, library 17, OMF.ZIP), I've yet to run across any NE documentation. Consequently, I relied on Schmitt's article, my debugger, and lots of experimentation to write this update of SHOWEXE.
Headers
An NE or LX .EXE file always contains the old DOS 2.1 MZ .EXE and the new NE, or LX .EXE. The MZ .EXE (so called for the two ASCII bytes at offset 0 in the file) contains the DOS stub program that prints the message "This program requires Microsoft Windows." The MZ header contains a pointer to the NE or LX header (see lfanew in Listing One) that is the file offset of the new style .EXE header. Listing One shows a simplified MZ header structure that gets you the new .EXE file offset. Between the MZ header and file offset lfanew, there may reside an actual DOS program of variable size.
The ASCII chars at lfanew contain the executable format specifier: NE, LE, LX, or PE (for Windows NT). NE indicates a 16-bit, segmented Windows or OS/2 .EXE; LX a 32-bit, flat-model OS/2 2.1 .EXE. Windows 3.1 is made up almost entirely of NE executables, while OS/2 2.1 contains a mix of NE and LX executables. Table 1 shows the file types of some of the files delivered with Windows 3.1 and OS/2 2.1. SMARTDRV and EMM386 are the only LE executables I have found in either of these systems.
A look at the header flags shows that there are several moves in the direction of cross-processor portability in LX, the most significant being the Byte Order and Word Order specifiers. Occurring directly after the Executable Type specifier (to minimize the amount of wrong-endian processing any loader might have to do), these allow either Big- or Little-endian byte and word orders over the entire rest of the executable file. Processor Type and OS Type specifiers also now each get 16 bits of their own, where NE relegated them to a couple of bits each in the Flags word. Interestingly, the Memory Page Size, fixed at 4K on Intel x86 CPUs, is also parameterized in the header, presumably to vary over different hardware platforms.
NE and LX take different approaches to preserving executable integrity. NE allowed space for a 32-bit file checksum in the header. This was intended as a layer of protection against viruses, to be checked off-line via a separate virus checker. LX takes a finer-grained, load-time approach to executable integrity. There are individual checksums for each 4K page as well as the Fixup Section, the Non-Resident Name Table, and the Loader Section, which includes all tables except the Non-Resident Name Table. In sum, these cover the entire file; individually, they allow the loader to do much less expensive checksums against small pieces of the file as they're loaded, rather than doing the whole file at once as the NE checksum requires.
Initial values in both models are much what you'd expect: code segment/offset, data segment/offset, stack size, heap size, and so on. They change from 16-bit values in NE to 32-bit values in LX, but their intent remains the same.
Segments, Pages, and Objects
The real difference between OS/2 1.x and OS/2 2.x is the shift from the 16-bit segmented-memory model to the 32-bit, flat-memory model. This shift is best reflected in the NE Segment Table and its LX analogs, the Object and Object Page Tables.
Listing Two has two global arrays of 32K integers, with a single reference to each array. When compiled and linked as an NE .EXE using Microsoft C 6.x, the linker produces one code segment from the example program, and three data segments: a distinct segment for each array (segments 3 and 4) and one for the Auto-DS Segment (segment 5). The same program linked 32-bit under Borland C++ for OS/2 produces the list of objects and pages shown in Table 5. As with NE, you get only one code object that has one page, but the two large arrays produce only one data object made up of 64 zero-filled, 4K pages (32,767 ints*4 bytes per int262,136 bytes and 262,136 bytes/4096 bytes/page = 64 pages). Thus, LX replaces the NE Segment Table with the Object and Object Page Tables.
An NE Segment Table entry contains a set of flags (READ/WRITE/EXECUTE_), the file location of the Segment image, the file size of the segment, and its size in memory. An LX Object Table entry looks much the same, containing the object's size and attributes (read/write/execute_), a count of pages that make up the object, and a pointer into the Object Page Table where this object's first page is described (and the rest are stored consecutively). Listing One shows the structures of NE's Segment Table and LX's Object and Object Page Table entries.
For these objects/segments to make a coherent program, the code object/segment has to be able to access addresses in the other objects/segments. Relocation--the process of connecting references within a lump of executable code to things outside that lump--consists of the target, source, and Fixup Record. The target is the place in the code where a symbolic reference must be replaced by a real address (the target of the relocation process), the source is where the real address can be found, and the Fixup Record links the target to the source. The Fixup Record Table is a list of Fixup Records for a particular segment/object.
External fixups are references that resolve to something outside this executable, typically DLL calls. Internal fixups are references to objects within this executable, typically references to the data segment. To discuss 32- versus 16-bit executables, I'll first examine internal fixups.
NE and LX structure the relocation data differently. NE attaches a separate Fixup Record Table to each segment that needs one. If a segment contains targets, NE physically appends a Fixup Record Table to the end of that segment and sets the Relocations Available bit in the segment description in the Segment Table. The fixup records themselves contain the offset of the target and segment number and offset of the source.
LX linkers place a single Fixup Record Table in the header. Each page in the Object Page Table contains an index into the Fixup Record Table pointing to the first fixup record for that page. The linker stores all the Fixup Records for that page consecutively, right up to the first fixup record for the next page. The fixup record contains the offset of the target and the object number and offset of the source. The target itself contains only the offset of the source. When loading a particular page, the loader runs through the Fixup Record Table, reading the attributes of the target and source, the source-object number, and the location of the target from the fixup record. Then it gets the actual offset of the source within the source object from the contents of the target.
While their fixup records are roughly equivalent, NE requires one record for each source, while LX uses the more obvious one-per-target strategy. Listing Three has three large arrays and two references to each array. NE produces three fixup records, while LX produces six; see Table 3. Obviously, NE is more load-time efficient, while LX is more run-time efficient. NE also allows for chaining of targets.
External fixups refer to objects located physically outside the executable, most commonly DLL calls. Both NE and LX executables support import-by-ordinal and import-by-name external fixups. The linker generates import-by-name external fixups for called functions defined explicitly in the IMPORTS section of the .DEF file. It generates import-by-ordinal external fixups for called functions defined in import libraries, a much more common technique. The benefit of import-by-name is that you don't need the import library to link the executable, but this technique is very rare.
With import-by-name DLL calls, the linker stores the names of both the external DLL and the function within the DLL in the .EXE itself. In LX, the target field in the fixup record contains indexes into the Import Module Name Table and the Import Procedure Name Table. This gives you the name of both the DLL and the function within the DLL. In NE, the target field contains two indexes into the Imported Name Table that do the same job.
With an import-by-ordinal reference, the linker specifies the DLL name and the function's ordinal within that DLL. The fixup-record target field contains an index into the Import Module Name Table (Imported Name Table in NE). However, instead of an index into the Import Procedure Name Table to get the function name, it contains the function's ordinal number within the external DLL. Thus, to get the function name to go with the ordinal, you have to run something like SHOWEXE on the external DLL and find the ordinal in either the Resident or Non-Resident Name Tables.
While the fixup records and sources for external fixups are much the same in NE and LX, the targets can be very different. In NE, targets within the same segment that resolve to the same source are chained together so that the loader has only to read one fixup record. Listing Four makes three calls to DosSleep(). You might expect this to generate three fixup records, but you only get one. Listing Five is a disassembled Listing Four in which the contents of the three target calls to DosSleep are 0000:001B, 0000:0024, and 0000:FFFF. There's the chain: The fixup record points to the target at offset 0012, which points to the target at offset 001B to the target at offset 0024, which signals the end of the chain with FFFF. At load-time, the loader gets a real address for the source, then marches up the target chain, replacing the chain links with the real address. (You have to look at the disk file to see the target chain. CodeView will replace the target-chain links with a real address.) In LX, there is no support for target chaining. The Fixup Record Table for the LX version of Listing Four is shown in Table 4. As you can see, the linker produces a separate fixup record for each call to DosSleep().
Reading the Tables
In either format, the linker places the "file offsets" of all the tables (except the Fixup Record Table in NE) in the header, but you have to be careful with these offsets. Though most of them are relative to the beginning of the new header, some are relative to other offsets or to the beginning of the file. Some of the tables have corresponding element counts within the header, but some are terminated by a special character, and others are only terminated by reaching the file offset of the next file section. And just to make it interesting, some of the tables, such as the Entry and Fixup Record Tables, contain entries which are structures made up of members of variable size (8, 16, and 32 bit). Table 2 lists the tables for LX, where they're located, what type of elements they contain, and how they're terminated.
The structures of the fixed-size entries are shown in Listing One. To read them, keep reading the fixed-size struct until you hit the terminator. The only table you have to dig for is the NE Fixup Record Table. The NE linker appends Fixup Record Tables to the segments to which they belong. Loading them requires finding the segment through the offset in the Segment Table record, adding the segment size, reading the two-byte relocation count at that offset, and then reading that many fixup records.
Under NE, the "Name" table entries (Imported, Resident, and Non-Resident) all contain a Pascal-style string (one-byte length followed by length bytes, non-null-terminated string). Resident and Non-Resident Name Table entries also add a 2-byte ordinal to the end. Under LX, the Name table-entry structures (Import Module, Import Procedure, Resident and Non-Resident) are identical except that they expand the string length to two bytes.
The Entry Table in NE and the Entry and Fixup Record Tables in LX contain more-complex, variable-sized entries. All these tables have to be read as byte streams. Entry Table entries are bundled, with the first two bytes being the count of entries in the bundle and their type, followed by count entries of a single format. So, you hit the bundle, figure out the count and the encapsulated format, and read count encapsulated entries. Figure 1 shows the format for LX Entry Table entries.
Unlike NE, the LX Fixup Record Table is part of the file header. The fixup records themselves can take one of four formats, and within each of these formats, two or three of the fields have a variable size. The fixup-record types you'll run into most often are 16-bit Internal Fixups and Import-by-Ordinal External Fixups. Figure 2 is the format of Fixup Records for LX.
Physical Objects
The only other types of data in the EXE are Debugging Info and Segments/Pages. Both NE and LX punt on Debugging Info, allowing the linker to reserve a section of the executable for proprietary-format debugging data. LX does make an attempt to assist the debugging process, instituting a pointer to a linker/debugger-specific Debugging Info Section and Debugging Info Length.
In NE, the linker places the file offsets of all the code and data segments in the Segment Table. The actual segments are located on sector boundaries and are found by shifting the sector index in the Segment Table entry (ns_sector in the SEG struct) left by the alignment (align from NE struct). Get the offset and read ns_cbseg bytes, and you have the actual segment.
In LX, the linker puts the offset of the page within the Data or Iterated Page Section in the page's Object Page Table entry, while placing the sizes and file offsets of the actual Data and Iterated Page Sections in the header. You locate the physical pages within the file by reading the page-data offset from the Object Page Table, shifting it left by the Page Offset Shift and adding it to either the Data or Iterated Data Pages Offset (depending on flags in the OBJPG struct). All pages are physically 4K long with any sub-4K pages zero-filled to reach the required size. Remember that both formats support zero-filled pages/segments that exist as entries in the Object Page or Segment Tables but don't have physical images in the executable.
Conclusion
The days when the loader simply copied all the bytes into memory and jumped to the entry point are long gone. With OOP, GUIs, internationalization, and the drive toward portability, EXE file formats have become more interesting. The loader and operating system now encompass functionality that would previously have been written into the source--if written at all. To master this functionality, you need to understand the reaction of the loader and the OS to particular facets of the executable, and be able to see clearly all the parts of that executable.
Acknowledgments
Special thanks to Michael Roth at IBM Austin for his help with this article.
Table 1: (a) Windows 3.1 programs and their .EXE formats; (b) OS/2 2.1 programs and their .EXE formats.
Format Name Description (a) NE SOL.EXE Solitaire game LE EMM386.EXE Extended-memory manager NE PRINTMAN.EXE Print manager NE PROGMAN.EXE Program-manager shell LE SMARTDRV.EXE SmartDrive disk cache NE GDI.EXE Graphical-device interface API NE RECORDER.DLL Windows recorder NE VBRUN300.DLL Visual Basic run-time DLL (b) LX PMSHELL.EXE Presentation Manager shell LX CMD.EXE Command-line shell NE LINK386.EXE Linker NE RC.EXE Resource compiler LX DOSCALL1.DLL System-call DLL LX OS2KRNL OS/2 2.1 kernel
Table 2: LX tables, their file locations and terminations. All symbolic references are to the LX .EXE header structure of Listing One except lfanew, which is a member of the MZ header.
Table Name File Location Termination Entry Size Object lfanew+ObjTblOfs Item count NumObjs Fixed Object Page lfanew+ObjPgTblOfs Item count* Fixed Resource lfanew+RscTblOfs Item count NumRscEntries Fixed Resident Name lfanew+ResTblOfs Null terminator Variable/ string Non-Resident NResTblOfs Byte count NResNmTblLen Variable/ Name string Entry lfanew+EntryTblOfs Null terminator Variable/ bundled Module-format lfanew+ModFmtTblOfs Item count NumModEntries Fixed Directive Fixup Page lfanew+FixupPgTblOfs See Object Page Table Fixed Fixup Record lfanew+FixupRecTblOfs File offset < Variable lfanew+ImpModTblOfs Import lfanew+ImpModTblOfs Item count ImpModEntries Variable/ Module Name string Import lfanew+ImpProcTblOfs File offset < DataPgOfs Variable/ Procedure string Name
Table 3: LX relocation list for Listing Three.
Type Target Source Internal 1.0054 2.000401ec Internal 1.004a 2.000401e8 Internal 1.0040 2.000201f0 Internal 1.0036 2.000201ec Internal 1.002c 2.01f4 Internal 1.0022 2.01f0
Table 4: LX relocation list for Listing Four.
Type Target Source Import by ordinal 1.37 DOSCALLS.229 Import by ordinal 1.2d DOSCALLS.229 Import by ordinal 1.23 DOSCALLS.229
Table 5: LX Object and Object Page Tables for Listing Two.
Object and Object Page Tables Object 1, Size 1598, Addr 0, Flags 2005, PgTableInd 1, NumPgs 1, Rsv0 READ, EXECUTE, 32-BIT, PAGES: Number Offset Size Flags 1 0 (0x0) 2048 Legal Physical Page - 0000 Object 2, Size 262808, Addr 0, Flags 2003, PgTableInd 2, NumPgs 65, Rsv0 READ, WRITE, 32-BIT, PAGES: Number Offset Size Flags 2 4 (0x4) 512 Legal Physical Page - 0000 3 0 (0x0) 0 Zero Filled Page - 0003 4 0 (0x0) 0 Zero Filled Page - 0003 5 0 (0x0) 0 Zero Filled Page - 0003 6 0 (0x0) 0 Zero Filled Page - 0003 7 0 (0x0) 0 Zero Filled Page - 0003 8 0 (0x0) 0 Zero Filled Page - 0003 9 0 (0x0) 0 Zero Filled Page - 0003 10 0 (0x0) 0 Zero Filled Page - 0003 11 0 (0x0) 0 Zero Filled Page - 0003 12 0 (0x0) 0 Zero Filled Page - 0003 13 0 (0x0) 0 Zero Filled Page - 0003 14 0 (0x0) 0 Zero Filled Page - 0003 15 0 (0x0) 0 Zero Filled Page - 0003 16 0 (0x0) 0 Zero Filled Page - 0003 17 0 (0x0) 0 Zero Filled Page - 0003 18 0 (0x0) 0 Zero Filled Page - 0003 19 0 (0x0) 0 Zero Filled Page - 0003 20 0 (0x0) 0 Zero Filled Page - 0003 21 0 (0x0) 0 Zero Filled Page - 0003 22 0 (0x0) 0 Zero Filled Page - 0003 23 0 (0x0) 0 Zero Filled Page - 0003 24 0 (0x0) 0 Zero Filled Page - 0003 25 0 (0x0) 0 Zero Filled Page - 0003 26 0 (0x0) 0 Zero Filled Page - 0003 27 0 (0x0) 0 Zero Filled Page - 0003 28 0 (0x0) 0 Zero Filled Page - 0003 29 0 (0x0) 0 Zero Filled Page - 0003 30 0 (0x0) 0 Zero Filled Page - 0003 31 0 (0x0) 0 Zero Filled Page - 0003 32 0 (0x0) 0 Zero Filled Page - 0003 33 0 (0x0) 0 Zero Filled Page - 0003 34 0 (0x0) 0 Zero Filled Page - 0003 35 0 (0x0) 0 Zero Filled Page - 0003 36 0 (0x0) 0 Zero Filled Page - 0003 37 0 (0x0) 0 Zero Filled Page - 0003 38 0 (0x0) 0 Zero Filled Page - 0003 39 0 (0x0) 0 Zero Filled Page - 0003 40 0 (0x0) 0 Zero Filled Page - 0003 41 0 (0x0) 0 Zero Filled Page - 0003 42 0 (0x0) 0 Zero Filled Page - 0003 43 0 (0x0) 0 Zero Filled Page - 0003 44 0 (0x0) 0 Zero Filled Page - 0003 45 0 (0x0) 0 Zero Filled Page - 0003 46 0 (0x0) 0 Zero Filled Page - 0003 47 0 (0x0) 0 Zero Filled Page - 0003 48 0 (0x0) 0 Zero Filled Page - 0003 49 0 (0x0) 0 Zero Filled Page - 0003 50 0 (0x0) 0 Zero Filled Page - 0003 51 0 (0x0) 0 Zero Filled Page - 0003 52 0 (0x0) 0 Zero Filled Page - 0003 53 0 (0x0) 0 Zero Filled Page - 0003 54 0 (0x0) 0 Zero Filled Page - 0003 55 0 (0x0) 0 Zero Filled Page - 0003 56 0 (0x0) 0 Zero Filled Page - 0003 57 0 (0x0) 0 Zero Filled Page - 0003 58 0 (0x0) 0 Zero Filled Page - 0003 59 0 (0x0) 0 Zero Filled Page - 0003 60 0 (0x0) 0 Zero Filled Page - 0003 61 0 (0x0) 0 Zero Filled Page - 0003 62 0 (0x0) 0 Zero Filled Page - 0003 63 0 (0x0) 0 Zero Filled Page - 0003 64 0 (0x0) 0 Zero Filled Page - 0003 65 0 (0x0) 0 Zero Filled Page - 0003 66 0 (0x0) 0 Zero Filled Page - 0003 Object 3, Size 49152, Addr 0, Flags 2003, PgTableInd 67, NumPgs 1, Rsv0 READ, WRITE, 32-BIT, PAGES: Number Offset Size Flags 67 0 (0x0) 0 Zero Filled Page - 0003
Figure 1 LX Entry Table entry format.
Figure 2 LX fixup-record format.
Listing One
// NE and LX Header structures and structures of all fixed size table entry // types. Dummy struct that gets you file offset of "new" exe header. Ignores // MZ header items other than ID word and lfanew. Read at offset 0 of file. typedef struct { unsigned short magic; // MUST BE ASCII "MZ" char useless_bytes[34]; // Ignore these bytes unsigned long lfanew; // Here's the file offset of new exe header. } SIMPLE_MZ_EXE; // structure of NE header. Follows magic bytes "NE" in file. typedef struct { unsigned char ver; // Version. unsigned char rev; // Revision unsigned short enttab; // File offset of Entry Table from lfanew. unsigned short cbenttab; // Entry Table byte count. long crc; // CRC checksum of entire file. unsigned short flags; // Exe flags (such as ERROR ...) unsigned short autodata; // Segment num of Auto-DS seg, 1-based. unsigned short heap; // Segment number of heap, 1-based. unsigned short stack; // Segment number of stack, 1-based. unsigned short ip; // Initial value of IP register. unsigned short cs; // Initial value of CS register. unsigned short sp; // Initial value of SP register. unsigned short ss; // Initial value of SS register. unsigned short cseg; // # of segments in Segment Table. unsigned short cmod; // # of modules in Module Reference Table. unsigned short cbnrestab; // Byte count of Non-Res Name Table. unsigned short segtab; // Offset of Segment Table from lfanew. unsigned short rsrctab; // Offset of Resource Table from lfanew. unsigned short restab; // Offset of Res Name Table from lfanew. unsigned short modtab; // Offset of Module Ref Table from lfanew. unsigned short imptab; // Offset of Imp Name Table from lfanew. unsigned long nrestab; // Offset of Non-Resident Name Table from // beginning of file. unsigned short cmovent; // Number of movable entries. unsigned short align; // File sector size, Segments are aligned // on boundaries of this value. unsigned short cres; // Item count of Resource Table. char resv[10]; // reserved. } NE_EXE; // an entry in the NE Segment Table struct SEG { unsigned short ns_sector; // The file sector segment starts at. unsigned short ns_cbseg; // # of bytes in segment image. unsigned short ns_flags; // Type of segment (code,data ...) unsigned short ns_minalloc; // Minimum size in memory. }; // an entry in NE Module Reference Table. struct MOD_REF { unsigned index; // An index into the Imported Name Table. unsigned uModNum; // Module number used by Fixup Records // trying to use this Module Reference. }; // an entry in a NE Fixup Record Table struct NEW_REL { unsigned char target; // Type of target (see targets below) unsigned char source; // Type of source (see sources below) unsigned offset; // Offset in this segment of target. unsigned module_num; // Module number (see Mod Reference entry) if // source = 1 or 2, segment number if source = 0 unsigned ordinal; // target offset if source=0, function ordinal if // source=1 and function name, offset in Imported // Name Table if source=2 }; // Possible values for target #define NE_TARG_16SEG 2 // 16-bit segment #define NE_TARG_16SEGOFS 3 // 16-bit segment, 16-bit offset. #define NE_TARG_16OFS 5 // 16-bit offset. #define NE_TARG_16SEG32OFS 11 // 16-bit segment, 32-bit offset. #define NE_TARG_32OFS 13 // 32-bit offset. // Possible values for source #define NE_DEST_THISEXE 0 // Source is in this exe. #define NE_DEST_DLLBYORDINAL 1 // Source is imported by ordinal. #define NE_DEST_DLLBYNAME 2 // Source is imported by name. // Structure that defines the LX exe header. Follows the two magic bytes "LX". typedef struct { UCHAR ByteOrder; // LITTLE_ENDIAN or BIG_ENDIAN UCHAR WordOrder; // LITTLE_ENDIAN or BIG_ENDIAN ULONG FormatLevel; // Loader format level, currently 0 USHORT CpuType; // 286 through Pentium+ USHORT OSType; // DOS, Win, OS/2 ... ULONG ModVersion; // Version of this exe ULONG ModFlags; // Program/Library ... ULONG ModNumPgs; // Number of non-zero-fill or invalid pages ULONG EIPObjNum; // Initial code object ULONG EIP; // Start address within EIPObjNum ULONG ESPObjNum; // Initial stack object ULONG Esp; // Top of stack within ESPObjNum ULONG PgSize; // Page size, fixed at 4k ULONG PgOfsShift; // Page alignment shift ULONG FixupSectionSize; // Size of fixup information in file ULONG FixupCksum; // Checksum of FixupSection ULONG LdrSecSize; // Size of Loader Section ULONG LdrSecCksum; // Loader Section checksum ULONG ObjTblOfs; // File offset of Object Table ULONG NumObjects; // Number of Objects ULONG ObjPgTblOfs; // File offset of Object Page Table ULONG ObjIterPgsOfs; // File offset of Iterated Data Pages ULONG RscTblOfs; // File offset of Resource Table ULONG NumRscTblEnt; // # of entries in Resource Table ULONG ResNameTblOfs; // File offset of Resident Name Table ULONG EntryTblOfs; // File offset of Entry Table ULONG ModDirOfs; // File offset of Module Directives ULONG NumModDirs; // Number of Module Directives ULONG FixupPgTblOfs; // File offset of Fixup Page Table ULONG FixupRecTblOfs; // File offset of Fixup Record Table ULONG ImpModTblOfs; // File offset of Imp Module Table ULONG NumImpModEnt; // Number of Imported Modules ULONG ImpProcTblOfs; // File offset of Imported Proc Table ULONG PerPgCksumOfs; // File offset of Per-Page // Checksum Table ULONG DataPgOfs; // File offset of Data Pages ULONG NumPreloadPg; // Number of Preload Pages ULONG NResNameTblOfs; // File offset of Non Resident Name Table // from beginning of file! ULONG NResNameTblLen; // Length in bytes of Non Resident Name Table; // table is also NULL terminated. ULONG NResNameTblCksum; // Non Resident Name Table checksum ULONG AutoDSObj; // Object number of auto data ULONG DebugInfoOfs; // File offset of debugging info ULONG DebugInfoLen; // Length of Debugging Info ULONG NumInstPreload; // Number of instance-preload pages ULONG NumInstDemand; // Number of instance-demand pages ULONG HeapSize; // Heap size ULONG StackSize; // Stack size } LX_EXE; // An entry in the LX object table typedef struct { ULONG size; // Load-time size of object ULONG reloc_base_addr; // Address the object wants to be loaded at. ULONG obj_flags; // Read/Write/Execute, Resource, Zero-fill ... ULONG pg_tbl_index; // Index in Object Page Table at which this // object's first page is located. ULONG num_pg_tbl_entries; // Number of consecutive Object Page Table // entries that belong to this object. ULONG reserved; // reserved. } LX_OBJ; // An entry in the LX Object Page Table. typedef struct { ULONG offset; // File offset of this pages data. Relative to // beginning of Iterated or Preload Pages USHORT size; // Size of this page. <= 4096 USHORT flags; // Iterated, Zero-filled, Invalid ... } LX_PG; // An entry in the LX Fixup Page Table is a single 32-bit value. // possible values for LX Object Page Table flags member typedef enum pg_types { LX_DATA_PHYSICAL = 0, // Legal Physical Page, file offset relative to // Preload Pages LX_DATA_ITERATED, // Iterated Data Page, file offset relative to // Iterated Pages LX_DATA_INVALID, // Invalid page. LX_DATA_ZEROFILL, // Zero-filled page. LX_DATA_RANGE // Range of pages. }; // An entry in the LX Resource Table typedef struct { USHORT type_id; // one of rsc_types USHORT name_id; // ID application uses to load this resource ULONG size; // size of the resource USHORT object; // which object is this resource located in? ULONG offset; // resource offset within the object } LX_RSC;
Listing Two
// TwoArray.C - Two almost-64k arrays, with one reference to each. // NE builds 3 segments for this, LX one object with 64 4k-pages. #include <stdio.h> int array1[32767]; int array2[32767]; int main(){ array1[0] = 1; array2[0] = 2; return( 0 ); }
Listing Three
// SixRef.C - Three almost-64k arrays, with two references to each. // NE uses 3 fixups for the references, LX 6. #include <stdio.h> int array1[32767], array2[32767], array3[32767]; int main(){ array1[0] = 1; array1[1] = 2; array2[0] = 1; array2[1] = 2; array3[0] = 1; array3[1] = 2; return( 0 ); }
Listing Four
// ThreeExt.C - Three DLL calls. NE uses 1 fixup record with the three targets // chained together. LX uses three fixups, no chain. #define INCL_DOSPROCESS #include <os2.h> int main(){ DosSleep( 2 ); DosSleep( 2 ); DosSleep( 2 ); return( 0 ); }
Listing Five
000F:0001 8BEC MOV BP,SP 000F:0003 B80000 MOV AX,0000 000F:0006 9A72020F00 CALL 000F:0272 000F:000B 57 PUSH DI 000F:000C 56 PUSH SI 7: DosSleep( 2 ); DosSleep( 2 ); DosSleep( 2 ); 000F:000D 6A00 PUSH 00 000F:000F 6A02 PUSH 02 000F:0011 9A1B000000 CALL 0000:001B 000F:0016 6A00 PUSH 00 000F:0018 6A02 PUSH 02 000F:001A 9A24000000 CALL 0000:0024 000F:001F 6A00 PUSH 00 000F:0021 6A02 PUSH 02 000F:0023 9AFFFF0000 CALL 0000:FFFF 8: return( 0 ); } 000F:0028 B80000 MOV AX,0000 000F:002B E90000 JMP 002E 000F:002E 5E POP SI 000F:002F 5F POP DI 000F:0030 C9 LEAVE
Copyright © 1994, Dr. Dobb's Journal