RMX-OS2: An In-Depth View - Part 3

From EDM2
Jump to: navigation, search
RMX-OS2: An In-Depth View
Part: 1 2 3 4

Written by Johan Wikman

Introduction

This is the third article in my series of articles about RMX. RMX is a system that allows OS/2 PM applications to run remotely, i.e., to run on one computer and be used on another. A somewhat more thorough overview of RMX is available in EDM/2 Volume 3 Issue 1.

One central feature of RMX is the replacement of the local procedure calls (LPCs) an application makes to PM with equivalent remote procedure calls (RPCs) over a network to a remote computer. In this article I will describe how that replacement is done.

Dynamic Link Libraries

Anybody who has programmed for OS/2 must have - at some point - come across dynamic link libraries or DLLs. But let us, nevertheless, take a closer look at the differences between static and dynamic linking.

Static Linking

When an application is statically linked, all code the application needs is collected into the application itself. Source files are first compiled into object modules that are combined - if of more general use - into a library. The executable is built by linking object modules and libraries as shown in the following figure.

Part3-rmx1.gif

There are obvious drawbacks with this approach. If several applications use the same routine from the library, it effectively means that there are N copies of that routine on disk and potentially in memory. Also, if a bug is found in the routine, then all applications using that routine must be rebuilt. However, as the application contains all code it needs there is never the risk that it stops working because some DLL is missing. This may be a highly desirable with certain programs.

Dynamic Linking

When DLLs are used the build process is slightly different. Object modules of general use are linked into a DLL instead of being combined into a library. From the DLL (or alternatively from a definitions file) an import library is generated that is used when the application is linked. When the application is run the DLL is dynamically linked and the application uses the code in the DLL as if it would be a part of the application itself.

Part3-rmx2.gif

Back in 1989 Ray Duncan listed in his book Advanced OS/2 Programming the following benefits of dynamic linking:

  • Code and read-only data in DLLs is shared invisibly between all processes or clients that use the libraries.
  • Code and data in DLLs can be loaded on demand, so it does not occupy physical memory until needed.
  • DLL routines can be referenced dynamically, i.e., by name, so a library and the applications that use it can be independently maintained and improved.
  • Centralization of frequently used routines in DLLs reduces the size of each application program file and conserves disk space.

So what has this got to do with RMX?

RMX and DLLs

Although Ray Duncan's list of DLL benefits is valid, it is not complete. One important point that must be added is:

  • Provided the prototypes of the exported functions of a DLL are known, a third party can provide a replacement DLL - with added value - to be used instead of the original DLL.

This is in essence precisely what RMX is all about. RMX provides replacement DLLs that offer remote execution capability. That is, for each PM DLL to be replaced there is a corresponding RMX DLL that contains the same functions with the same prototypes and where the functions are exported using the same ordinals. So, an application can not detect any difference between the PM DLLs and the RMX DLLs. The user suddenly just can use the application remotely.

Replacing DLLs

Ok, so we want to replace a DLL an application is using (or actually would be using if it was started). How should we do that? There are at least three ways:

  1. Simply replace the original DLL with the replacement DLL.
  2. Place the replacement DLL - with the same file and module name - earlier in the LIBPATH than the original DLL.
  3. Patch the executable so that it uses the replacement DLL instead of the original DLL.

With ordinary DLLs - i.e., DLLs that are only used by applications and not by the system - all alternatives are usable, although the first one obviously is the easiest.

With system DLLs - e.g. PMWIN.DLL and PMGPI.DLL - the situation is more complicated. First of all they can't simply be replaced as they are always in use, if not by an application then by WPS and the system itself. Also, the idea of replacing the DLLs of the system is not particularly appealing.

It is also not possible to allow the system to use the original PM DLLs and force applications to use the replacement DLLs. The reason is that once a DLL is loaded OS/2 does no longer look for it from the LIBPATH. So, even if a DLL with the name PMWIN.DLL is in the current directory and the value of LIBPATH is .;C:\OS2\DLL;... the original PMWIN.DLL which already is in memory will be used when an application is started.

That leaves the third alternative. Patching an application is not all that nice, but it is actually quite trivial to do and, above all, it works. But before I continue with the actual process of patching, I'll mention a fourth way which actually would be the nicest.

I'm convinced it would be possible to write a loader application that when started would explicitly read the code and data pages of another executable, patch the code in memory and execute it. Hence, no patching of the actual executable files would be needed.

RMX DLLs

RMX does not provide replacement DLLs for all DLLs of OS/2. Only some of the ones that deal with PM have to be replaced. Currently rmxpatch.exe - which is the application that does the actual patching - replaces the following DLLs:

Original Replaced with
PMWIN.DLL RXWIN.DLL
PMGPI.DLL RXGPI.DLL
PMSHAPI.DLL RXSHAPI.DLL
PMCTLS.DLL RXCTLS.DLL
HELPMGR.DLL RXLPMGR.DLL

Replacing these DLLs is enough to get most PM programs up and running. It turned out to be necessary to replace HELPMGR.DLL because amazingly many programs refuse to start if they fail to initialise the help system. Obviously some functionality - e.g. drag and drop as PMDRAG.DLL is not on the list - is missing but more often than not it doesn't significantly influence the usability of the application.

Reading Executable Info

Ok, let's get on with the real stuff.

Executable Header

Before anything can be done, we need to find out exactly which DLLs the executable (application or DLL) uses. In order to do that we must read the header of the executable. Although RMX is targeted only towards 32-bit applications we have to, in order to find the 32-bit header, read the old DOS executable header first. The layout of a 32-bit application looks like

 	 00h +------------------+  <--+
 	     | DOS 2 Compatible |	|
 	     |    EXE Header	|	|
 	 1Ch +------------------+	|
 	     |	unused	|	|
 	     +------------------+	|
 	 24h |  OEM Identifier	|	|
 	 26h |  OEM Info		|	|
 	     |			|	|-- DOS 2.0 Section
 	 3Ch |  Offset to       |	|   (Discarded)
 	     |  Linear EXE	|	|
 	     |  Header		|	|
 	 40h +------------------+	|
 	     |   DOS 2.0 Stub	|	|
 	     |   Program &	|	|
 	     |   Reloc. Table	|	|
 	     +------------------+  <--+
 	     |			|
 	 xxh +------------------+  <--+
 	     |    Executable	|	|
 	     |	 Info       |	|
 	     +------------------+	|
 	     |	Module	|	|
 	     |	 Info       |	|
 	     +------------------+	|-- Linear Executable
 	     |  Loader Section	|	|   Module Header
 	     |	 Info       |	|   (Resident)
 	     +------------------+	|
 	     |   Table Offset	|	|
 	     |	 Info       |	|
 	     +------------------+  <--+
 	     |   Object Table	|	|
 	     +------------------+	|
 	     | Object Page Table|	|
 	     +------------------+	|
 	     |  Resource Table	|	|
 	     +------------------+	|
 	     |  Resident Name	|	|
 	     |	Table       |	|
 	     +------------------+	|-- Loader Section
 	     |   Entry Table	|	|   (Resident)
 	     +------------------+	|
 	     |   Module Format	|	|
 	     | Directives Table |	|
 	     |    (Optional)	|	|
 	     +------------------+	|
 	     |     Resident	|	|
 	     | Directives Data	|	|
 	     |    (Optional)	|	|
 	     |			|	|
 	     |  (Verify Record) |	|
 	     +------------------+	|
 	     |     Per-Page	|	|
 	     |     Checksum	|	|
 	     +------------------+  <--+
 	     | Fixup Page Table |	|
 	     +------------------+	|
 	     |   Fixup Record	|	|
 	     |	 Table	|	|
 	     +------------------+	|-- Fixup Section
 	     |   Import Module	|	|   (Optionally Resident)
 	     |    Name Table	|	|
 	     +------------------+	|
 	     | Import Procedure |	|
 	     |    Name Table	|	|
 	     +------------------+  <--+
 	     |   Preload Pages	|	|
 	     +------------------+	|
 	     |    Demand Load	|	|
 	     |	 Pages	|	|
 	     +------------------+	|
 	     |  Iterated Pages	|	|
 	     +------------------+	|
 	     |   Non-Resident	|	|-- (Non-Resident)
 	     |    Name Table	|	|
 	     +------------------+	|
 	     |   Non-Resident	|	|
 	     | Directives Data	|	|
 	     |    (Optional)	|	|
 	     |			|	|
 	     |  (To be Defined) |	|
 	     +------------------+  <--+
 	     |    Debug Info	|	|-- (Not used by Loader)
 	     +------------------+  <--+

where the actual DOS 2 compatible header contains the following fields:

 struct exe {
 	  WORD eid;
 	  ...
 	  WORD ereloff;
 	  ...
 };

If we read a proper old header then at offset 0x3C we find the offset of the 32-bit header. But only if the value of the eid field is EXEID (defined in exe.h) and the value of the erelof field is 0x40.

Once we have got that offset (which is from the beginning of the file) we can read the 32-bit executable header.

Modules

Our target is to find the names of all DLLs the executable implicitly uses but unfortunately that is not quite enough. There are in practice three kinds of PM applications:

  1. 16-bit applications that uses the 16-bit PM functions.
  2. 32-bit applications that uses the 16-bit PM functions.
  3. 32-bit applications that uses the 32-bit PM functions.

E.g., the Warp applications mahjongg.exe belongs to the third category while klondike.exe belongs to the second. As RMX only supports pure 32-bit applications we have to find out which interface of PM the application uses before we can patch it. The required information can be find out by looking at the fixups that have to be applied when the application is loaded by OS/2. All needed information is in the fixup section of the executable (copied here for ease of reference).

 	     +------------------+  <--+
 	     | Fixup Page Table |	|
 	     +------------------+	|
 	     |   Fixup Record	|	|
 	     |	 Table	|	|
 	     +------------------+	|-- Fixup Section
 	     |   Import Module	|	|   (Optionally Resident)
 	     |    Name Table	|	|
 	     +------------------+	|
 	     | Import Procedure |	|
 	     |    Name Table	|	|
 	     +------------------+  <--+

The fixup page table provides a mapping from logical page number to an offset into the fixup record table for that page.

The fixup record table contains entries for all fixups in the executable. To make things more complicated there are four types of fixups: an internal reference, imported by ordinal, imported by name or an internal reference via an entry table. So, the fixup record table has to be scanned and for each reference that refers to a PM DLL we must establish whether it is a pure 32-bit call.

The import module name table is a list of all modules the executable imports through dynamic link references.

The import procedure name table contains the procedures the executable imports by name.

Everything we need for extracting this information is in the 32-bit header:

 struct e32_exe
 {
     ...
     unsigned long       e32_fixupsize; // Fixup section size
     ...
     unsigned long       e32_fpagetab;  // Offset of Fixup Page Table
     unsigned long       e32_frectab;   // Offset of Fixup Record Table
 
     unsigned long       e32_impmod;    // Offset of Import Module Name Table
 
     unsigned long       e32_impmodcnt; // Number of entries in Import Module
 
 						   // Name Table
     unsigned long       e32_impproc;   // Offset of Import Proc. Name Table
     unsigned long       e32_pagesum;
     ...
   };

Before we begin we define a structure where the data of interest is collected:

 #define RMX_MAXDLLNAME 128
 
 typedef struct RMXMODULE_
 {
   CHAR  achName[RMX_MAXDLLNAME];
   ULONG ulOffset;
   BOOL  fixup8;
   BOOL  fixup16Selector;
   BOOL  fixup1616Pointer;
   BOOL  fixup16Offset;
   BOOL  fixup1632Pointer;
   BOOL  fixup32Offset;
   BOOL  fixup32SROffset;
 } RMXMODULE;

achName is a buffer where the module name is stored.

ulOffset is the position - from the beginning of the executable - where the name of this module is. This information will be used when the exe is patched.

fixup8, fixup16Selector, fixup1616Pointer, fixup16Offset, fixup1632Pointer, fixup32Offset and fixup32SROffset are booleans that specify what kind of fixups must be applied when the DLL ,this module represents, is loaded by the system.

Before we begin here are two variables that are used throughout the presentation.

    ULONG		offExe32 = ...;
    struct e32_exe exe32    = ...;

OffExe32 is the offset of the 32-bit header from the beginning of the file. This is needed because all (but one) offsets in the header are from the beginning of the header and not from the beginning of the file. Exe32 is the 32-bit header.

The offset of the fixup section is obtained by adding the offset of the first table in the fixup section - the fixup page table - to the offset of the 32-bit header. The size of the fixup section is obtained directly from the header.

    ULONG offFixups = offExe32 + exe32.e32_fpagetab;
    ULONG cbFixups  = exe32.e32_fixupsize;

Now that we know the offset and the size of the fixup section we can read it.

    BYTE* pbFixups = (BYTE*) malloc(cbFixups);
 
    DosRead(hExe, pbFixups, cbFixups, &cBytesRead);

As we really want to have the information in the form of RMXMODULEs we must allocate some memory.

    ULONG ulModuleSize = exe32.e32_impmodcount * sizeof(RMXMODULE);
 
    RMXMODULE* pModules = (RMXMODULE*)malloc(ulModuleSize);

The next thing to do is to fill in the module name and file offset. The offset of the module name table (from the beginning of the fixup section) is the difference between the offset (from the beginning of the header) of the module name table and the offset (also from the beginning of the header) of the fixup page table.

    ULONG offModule = exe32.e32_impmod - exe32.e32_fpagetab;
    BYTE* pbRawModules = pbFixups + offModules;

Now we have enough data in order to fill in the name and offset fields of the RMXMODULEs.

 for (int i = 0; i < exe32.e32_impmodcnt; i++)
 {
   // The length of the module name is in the first byte.
 
   ULONG
     ulSize = *pbRawModules;
   RMXMODULE
     *pModule = &pModules[i];
 
   // And the actual name is then from the second byte forward.
 
   pbRawModules++;
 
   memcpy(pModule->achName, pbRawModules, ulSize);
 
   // The strings are not NULL terminated which is why the NULL
   // must be set explicitly.
 
   pModule->achName[ulSize] = 0;
   pModule->ulOffset = offFixups + (pbRawModules - pbFixups);
 
   pbRawModules += ulSize;
 }

The next task is to check out the fixups. The fixup table provides a mapping of a logical page number to an offset into the fixup record table for that page. The table contains one additional entry that is an offset to the end of the fixup record table. As the offsets are 4 bytes, the number of pages is:

   ULONG cPages = (exe32.e32_frectab - exe32.e32_fpagetab) / 4 - 1;

As the page table is first in the fixup chunk we can simply cast the fixups pointer into an unsigned long pointer and, voila, we have the page table. The actual records are obtained by adding the proper offset to the beginning of the fixup section.

    ULONG* poffPages = (ULONG*) pbFixups;
    BYTE*  pbRecords = pbFixups + (exe32.e32_frectab - exe32.e32_fpagetab);

In the fixup record table there are four kind of entries. The length and structure is different for each kind of entry but the following figure shows the general layout:

 	     +-----+-----+-----+-----+
 	 00h | SRC |FLAGS|SRCOFF/CNT*|
 	     +-----+-----+-----+-----+-----+-----+
    03h/04h |	     TARGET DATA *	     |
 	     +-----+-----+-----+-----+-----+-----+
 	     | SRCOFF1 @ |   . . .   | SRCOFFn @ |
 	     +-----+-----+----	 ----+-----+-----+
 
 	   * These fields are variable size.
 	   @ These fields are optional.

The SRC byte specifies the type of the fixup to be performed on the fixup source and the type is specified as follows:

 0Fh = Source mask.
 00h = Byte fixup (8-bits).
 01h = (undefined).
 02h = 16-bit Selector fixup (16-bits).
 03h = 16:16 Pointer fixup (32-bits).
 04h = (undefined).
 05h = 16-bit Offset fixup (16-bits).
 06h = 16:32 Pointer fixup (48-bits).
 07h = 32-bit Offset fixup (32-bits).
 08h = 32-bit Self-relative offset fixup (32-bits).
 10h = Fixup to Alias Flag.
 20h = Source List Flag.

The FLAGS field specifies how the target data should be interpreted and the flags are defined as follows:

 03h = Fixup target type mask.
 00h = Internal reference.
 01h = Imported reference by ordinal.
 02h = Imported reference by name.
 03h = Internal reference via entry table.
 04h = Additive Fixup Flag.
 08h = Reserved.  Must be zero.
 10h = 32-bit Target Offset Flag.
 20h = 32-bit Additive Fixup Flag.
 40h = 16-bit Object Number/Module Ordinal Flag.
 80h = 8-bit Ordinal Flag.

So, in order to find out which fixups are applied for each DLL we have to loop through all relocation entries and see what they tell us.

 for (i = 0; i < cPages; i++)
 {
   BYTE
     *pbCurrentRecord = pbRecords + poffPages[i],
     *pbNextRecord    = pbRecords + poffPages[i + 1];
 
   while (pbCurrentRecord < pbNextRecord)
     {
 	BYTE
 	  flTarget = pbCurrentRecord[1]; // NOTE: 1 not 0
 	ULONG
 	  ulSize = 0;
 
 	switch (flTarget & NRRTYP)
 	  {
 	  case NRRINT:
 	    ulSize = ParseInternalReference(pbCurrentRecord);
 	    break;
 
 	  case NRRORD:
 	    ulSize = ParseOrdinalImportReference(pbCurrentRecord, pModules);
 	    break;
 
 	  case NRRNAM:
 	    ulSize = ParseNameImportReference(pbCurrentRecord, pModules);
 	    break;
 
 	  case NRRENT:
 	    ulSize = ParseInternalEntryReference(pbCurrentRecord);
 	  }
 
 	pbCurrentRecord += ulSize;
     }
 }

The NNR constants are defined in exe386.h. Ok, let's then have a look at the different Parse-functions.

ULONG PreParseReference(BYTE* pbRecord);

The following fields are common to all reference entries regardless of what their actual type is:

     +-----+-----+-----+-----+
 00h | SRC |FLAGS|SRCOFF/CNT*|
     +-----+-----+-----+-----+
     ...
 
   * These fields are variable size.

What this function actually does is to figure out how much space the common parts actually occupies. As the SRC and FLAGS entries are always present the size is at least 2 bytes.

   ULONG ulSize = 2;

The item following the SRC and FLAGS field is either a byte specifying how many offsets there are - in which case the actual relocation entry is followed by a list of offsets (the SRCOFF1 ... SRCOFFn in the figure presented earlier) - or a word specifying the single source offset. Which one it is, is specified by the presens or absence of the source list flag in the SRC byte.

 BYTE sourceType = pbRecord[0];
 
 if (sourceType & NRCHAIN) // "Source List" flag.
   {
     ulSize += 1;	// The byte itself.
 
     BYTE
 	  sourceCount = pbRecord[2];
 
     ulSize += sourceCount * 2; // The actual offsets..
   }
 else
   ulSize += 2;

So, depending on the way the offsets are presented this function returns either 4, or 5, 7, 9, ... and so on.

ULONG ParseInternalReference(BYTE* pRecord);

If it is an internal reference the relocation record looks like:

 	  +-----+-----+-----+-----+
     00h | SRC |FLAGS|SRCOFF/CNT*|
 	  +-----+-----+-----+-----+-----+-----+
 03h/04h |  OBJECT * |	     TRGOFF * @	  |
 	  +-----+-----+-----+-----+-----+-----+
 	  | SRCOFF1 @ |	. . .   | SRCOFFn @ |
 	  +-----+-----+----   ----+-----+-----+
 
 	* These fields are variable size.
 	@ These fields are optional.

The call to PreParseReference figures out the size of the common parts.

    ULONG ulSize = PreParseReference(pbRecord);
    BYTE
      sourceType = pbRecord[0],
      flTarget   = pbRecord[1];

OBJECT is an index into the current module's object table that specifies the target object. Depending on a bit in flTarget it is either a byte or a word.

    if (flTarget & NR16OBJMOD)
      ulSize += 2;
    else
      ulSize += 1;

The target offset field - TRGOFF - is present only if this record does not represent a 16-bit selector fixup.

    if ((sourceType & NRSTYPE) == NRRSEG)
      ;
    else
    {

If it is present it is either a 2 or 4 byte entry depending on whether the 32-bit target offset bit has been set.

      if (flTarget & NR32BITOFF)
 	 ulSize += 4;
      else
 	 ulSize += 2;
    }

 ULONG ParseOrdinalImportReference(BYTE* pbRecord, RMXMODULE* pModule);

If it is an ordinal import reference the relocation record looks like:

 	  +-----+-----+-----+-----+
     00h | SRC |FLAGS|SRCOFF/CNT*|
 	  +-----+-----+-----+-----+-----+-----+-----+-----+
 03h/04h | MOD ORD# *|IMPORT ORD*|	  ADDITIVE * @	  |
 	  +-----+-----+-----+-----+-----+-----+-----+-----+
 	  | SRCOFF1 @ |	. . .   | SRCOFFn @ |
 	  +-----+-----+----   ----+-----+-----+
 
 	* These fields are variable size.
 	@ These fields are optional.

The common stuff is handled by PreParseReference.

    ULONG ulSize = PreParseReference(pbRecord);
    BYTE
      sourceType = pbRecord[0],
      flTarget   = pbRecord[1];

Depending on which way the source offsets are represented, the index of the module ordinal - MODORD - is either 3 or 4.

    ULONG index = 3;
 
    if (!(sourceType & NRCHAIN))
      index++;

The module index is either a byte or a word, depending on whether the 16-bit module ordinal flag has been set.

 if (flTarget & NR16OBJMOD)
 {
   usModule = *((USHORT*) &pbRecord[index]);
   index += 2;
   ulSize  += 2;
 }
 else
 {
   usModule = pbRecord[index];
   index += 1;
   ulSize  += 1;
 }

The actual ordinal is then either 1, 2 or 4 bytes depending on whether certain bits have been set or not.

 ULONG ordinal = 0;
 
 if (flTarget & NR8BITORD) // "8-bit Ordinal" flag
 {
   ordinal = (UCHAR) pbRecord[index];
   ulSize += 1;
 }
 else if (flTarget & NR32BITOFF) // "32-bit Target Offset" flag
 {
   ordinal = *((ULONG*) &pbRecord[index]);
   ulSize += 4;
 }
 else
 {
   ordinal = *((USHORT*) &pbRecord[index]);
   ulSize += 2;
 }

I don't use the ordinal for anything, but for a tdump or exehdr kind of utility you could simply print it out. The additive fixup value - ADDITIVE - is optionally present and its size is either 2 or 4 bytes.

    if (flTarget & NRADD)
    {
      if (flTarget & NR32BITADD)
 	 ulSize += 4;
      else
 	 ulSize += 2;
    }

Finally we call UpdateFixupInfo in order to update the proper RMXMODULE entry.

   UpdateFixupInfo(sourceType, &pModules[usModule - 1]);
ULONG ParseNameImportReference(BYTE* pbRecord, RMXMODULE* pModules);

If it is an name import reference the relocation record looks like:

         +-----+-----+-----+-----+
     00h | SRC |FLAGS|SRCOFF/CNT*|
 	 +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
 03h/04h | MOD ORD# *| PROCEDURE NAME OFFSET*|	  ADDITIVE * @	     |
 	 +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
 	 | SRCOFF1 @ |  ...      | SRCOFFn @ |
 	 +-----+-----+----   ----+-----+-----+
 
 	* These fields are variable size.
 	@ These fields are optional.

The beginning of this function is identical to ParseOrdinalImportReference, so I won't duplicate it here, but continue after the module index has been figured out.

The procedure name offset is a direct offset into the import procedure name table and could be used for printing out the names of the functions the executable imports. Keep in mind that the strings are pascal strings (initial byte specifying the length, no NULL at end). The procedure offset is either a word or a long depending on the 32-bit target offset flag.

 ULONG procedure = 0;
 
 if (flTarget & NR32BITOFF) // "32-bit Target Offset" flag
 {
   procedure = *((ULONG*) &pbRecord[index]);
   index += 4;
   ulSize  += 4;
 }
 else
 {
   procedure = *((USHORT*) &pbRecord[index]);
   index += 2;
   ulSize  += 2;
 }

The entry is then, just like in the ordinal import case, followed by an optional additive fixup value.

    if (flTarget & NRADD)
    {
      if (flTarget & NR32BITADD)
 	 ulSize += 4;
      else
 	 ulSize += 2;
    }

Again, the corresponding RMXMODULE entry is updated with the information.

   UpdateFixupInfo(sourceType, &pModules[usModule - 1]);
ULONG ParseInternalEntryReference(BYTE* pbRecord);

If it is an internal entry reference, the relocation entry looks like:

     00h | SRC |FLAGS|SRCOFF/CNT*|
 	 +-----+-----+-----+-----+-----+-----+
 03h/04h |  ORD # *  |	  ADDITIVE * @	  |
 	 +-----+-----+-----+-----+-----+-----+
 	 | SRCOFF1 @ |	 . . .   | SRCOFFn @ |
 	 +-----+-----+----   ----+-----+-----+
 
 	* These fields are variable size.
 	@ These fields are optional.

As usual the initial stuff is handled by PreParseReference.

    ULONG ulSize = PreParseReference(pbRecord);
    BYTE
      flTarget = pbRecord[0];

The ordinal is always there, either as a 1 or 2 byte value and the additive fixup value is optionally there, either as a 2 or 4 byte value.

 if (flTarget & NR16OBJMOD) // "16-bit Object Number/Module Ordinal" flag.
   ulSize += 2;
 else
   ulSize += 1;
 
 if (flTarget & NRADD) // "Additive Fixup" flag
 {
   if (flTarget & NR32BITADD) // "32-bit Additive Fixup" flag
     ulSize += 4;
   else
     ulSize += 2;
 } 

 VOID UpdateFixupInfo(BYTE sourceType, RMXMODULE* pModule);

Let's then have a look at what UpdateFixupInfo does. It is very trivial; based on the source type flags it sets the appropriate flags in RMXMODULE to TRUE.

 switch (sourceType & NRSTYP)
 {
  case NRSBYT:
   pModule->fixup8		 = TRUE;
   break;
 
  case NRSSEG:
   pModule->fixup16Selector  = TRUE;
   break;
 
  case NRSPTR:
   pModule->fixup1616Pointer = TRUE;
   break;
 
  case NRSOFF:
   pModule->fixup16Offset	 = TRUE;
   break;
 
  case NRPTR48:
   pModule->fixup1632Pointer = TRUE;
   break;
 
  case NROFF32:
   pModule->fixup32Offset	 = TRUE;
   break;
 
  case NRSOFF32:
   pModule->fixup32SROffset  = TRUE;
 }

The whole concept here is very brute-force. Instead of scanning through all relocation records for a module, we could stop immediately when something is found that makes patching impossible. But that would make everything more complex and the scanning of all relocation records really doesn't take too long.

Patching

So, having obtained all RMXMODULEs of an executable we scan through them and if the name of the module matches one of the DLLs we want to replace, we use the offset to find the proper place in the executable file and simply write the new name. However, there are a few problems:

  • The name of the replacement DLL must have the same length as the name of the original DLL.
  • If OS/2 would use the checksum entries that are present in the executable header, then this crude approach would not work.

There is a fix for these problems. If the patching would be performed as a proper link, i.e., a completely new executable header is created, there wouldn't be any constraints on the name and the checksum would also not be a problem. However, that would require a great deal more effort and it'll have to wait until I have more time <grin>.

RMXPATCH.EXE

Along with this article is the full source for rmxpatch.exe. You can't use it for anything particular yet as I haven't distributed RMX itself, but it would probably be fairly easy to use rmxpatch as a base for a tdump or exehdr kind of utility. And anyway, once RMX is available (real soon now <grin>) rmxpatch can be used for patching and unpatching PM programs.

If you start rmxpatch.exe without any arguments it prints:

    usage: rmxpatch (-p|-u) [-d] file [file ...]
 
 	    -p: Patch the files for RMX.
 	    -u: Unpatch the files, i.e., remove references to RMX.
 	    -d: Do	it.  Without this flag rmxpatch  only shows
 		  what it would do had the flag been given.

So, without -d rmxpatch only shows what it would do, but it doesn't patch anything. I chose to make it rather difficult to patch a program so that people wouldn't patch programs by mistake. Let's see then what the output of rmxpatch look like. I use here mine.exe - the excellent mine sweeper game - as example.

    [C:\]rmxpatch -p mine.exe
 mine.exe:
     Offset	  From	To	    Comment
     ---------------------------------------
     6193	  dllc101		    ignored
     6201	  DOSCALLS		    ignored
     6210	  PMGPI	RXGPI     replacable
     6216	  PMWIN	RXWIN     replacable
     6222	  PMSHAPI	RXSHAPI   replacable
     6230	  HELPMGR	RXLPMGR   replacable
 
     Success: the executable can be patched.

With -u the output obviously is different as there is nothing to do.

    [C:\]rmxpatch -u mine.exe
 mine.exe:
     Offset	  From	To	    Comment
     ---------------------------------------
     6193	  dllc101		    ignored
     6201	  DOSCALLS		    ignored
     6210	  PMGPI		    ignored
     6216	  PMWIN		    ignored
     6222	  PMSHAPI		    ignored
     6230	  HELPMGR		    ignored
 
     Success: the executable can be patched.

As expected, there is nothing to unpatch on a program. It would have been possible to have deduced from the DLL names what the user actually wants to do, but I chose to require the user to tell it explicitly. A smaller risk for confusion that way. To actually patch the program -d is required.

    [C:\]rmxpatch -p -d mine.exe
 mine.exe:
     Offset     From	To	    Comment
     ---------------------------------------
     6193       dllc101	           ignored
     6201       DOSCALLS	   ignored
     6210	PMGPI	 RXGPI     successfully replaced
     6216	PMWIN	 RXWIN     successfully replaced
     6222	PMSHAPI	 RXSHAPI   successfully replaced
     6230	HELPMGR	 RXLPMGR   successfully replaced
 
     Success: the executable was successfully patched.

The unpatching works in a similar way.

Conclusion

That was briefly about the mechanism used for replacing DLLs and the tool required for patching applications. Feel free to use the source of the tool for anything you want and don't hesitate to mail me if there is something you are wondering about. I haven't decided yet what the next article will be about so you'll just have to wait <grin>.