The Windows for Workgroups 3.11 VXDLDR

Windows for Workgroups 3.11 uses VXDLDR.386 to load files with the D32 extension in the SYSTEM subdirectory, to make an I/O subsystem of VxDs managed by IOS.386. This article begins with the structures and interfaces that support the Windows for Workgroups 3.11 VXDLDR. An appendix details those elements of the LE file header that are actually relevant to VXDLDR when loading VxDs.

Microsoft did not publish documentation for the Windows for Workgroups 3.11 VXDLDR. Strictly speaking, the article is therefore not alternative documentation. It was first written in 1994 and made available on Compuserve in WINSDK library 17 as a Word document titled Dynamic VxD Loading (VXDLDR.386). The intention was to demonstrate to VxD programmers that high-quality technical details of significant new system components could be discovered independently of the system’s manufacturer and to encourage interest in extending the documentation project to other new components such as IOS.386 and IFSMGR.386. In the interests of standardisation and readability now that the document must be meaningful to programmers who know of VXDLDR only from the Windows 95 DDK, the version that follows was updated in 1997 to use symbols from header files that Microsoft provides with the Windows 95 DDK. It has been further edited in 2008 for conversion to HTML and for consistency with other pages at this website.

Dynamic VxD Loading (VXDLDR.386)

Windows for Workgroups 3.11 introduces the means to load specially-constructed VxDs as components of a subsystem of drivers that is largely disjoint from the set of VxDs that the real mode portion of WIN386 loads in response to device entries in the SYSTEM.INI configuration file. A side-effect is that these special VxDs can be loaded and unloaded after normal system initialisation. It is important to note however that the VxDs managed by VXDLDR must supply two control functions especially for this purpose: it is not as if VXDLDR can just load any old VxD that some program decides it would have liked WIN386 to have loaded for initialisation by the VMM.

Another, telling, indication that VXDLDR is intended to enable a normal VxD to build a subsystem of other drivers is that VxDs loaded by VXDLDR do not appear in the linked list of Driver Descriptor Blocks (DDBs) built by the VMM. Instead, VXDLDR builds a separate descriptor block for each VxD it loads, and links these in a list whose first entry may be located through a VXDLDR service. This descriptor block is a DeviceInfo structure:

Offset Size Description
00h dword address of DeviceInfo structure for next most recently loaded VxD; or 00000000h
04h byte driver status: 00h inactive, 01h active
05h dword address of Driver Descriptor Block
09h word VxD ID
0Bh dword address of driver name as ASCIIZ string
0Fh 4 bytes signature: XVLD
13h dword number of objects in driver
17h dword address of array of ObjectInfo structures for successive objects

The status flag at offset 04h is set only after the driver has completed its initialisation, meaning that it has not only been loaded but also that its control function for initialisation has been called and that VXDLDR has released the driver’s discardable objects from memory. This status flag may be cleared again if the driver is unloaded: when VXDLDR unloads a driver, it does not remove the DeviceInfo structure.

The driver’s Descriptor Block is accessible from the DeviceInfo structure. Note that DDBs for drivers loaded by VXDLDR are not linked to DDBs for other VxDs (as would usually be done through the DDB_Next field). A side-effect is that the usual int 20h mechanism is unavailable for calling services in dynamically loadable VxDs.

The VxD ID recorded in the DeviceInfo structure is taken from offset C0h in the LE Header when the VxD is loaded. The VxD ID in the VxD’s DDB is irrelevant to VXDLDR, though presumably, the linker ensures that the two IDs agree.

It is not actually necessary for the dynamically load-able VxD to have a VxD ID. It may instead be referred to by name—for instance, when calling relevant VXDLDR services. The driver name is stored in a separate heap allocation from the DeviceInfo structure. The name is taken from the executable file’s Resident Names Table, the location of which is indicated by the field at offset 58h in the LE Header.

It is arguable that the fields from offset 0Fh onwards in the DeviceInfo structure are for VXDLDR’s internal use. The signature at offset 0Fh is used to verify that an address passed to the relevant VXDLDR services is in fact that of a DeviceInfo structure. The other fields point the way to the record of objects that have been loaded into memory. It happens that the array addressed by the field at offset 17h is actually placed immediately after the DeviceInfo structure—but the existence of a pointer to its location suggests that this arrangement should not be relied upon (and so, the position of the ObjectInfo array is not shown above as part of the DeviceInfo).

The ObjectInfo structure has the following format:

Offset Size Description
00h dword address of object; or 00000000h if object not present
04h dword size of object
08h dword type of object
0Ch dword 00000000h if object loaded from this instance of the driver;
00000001h if object is resident and persists from an earlier instance of the driver

The object type given at offset 08h is important since some types require special treatment when the driver is loaded or unloaded:

Type

Description

00000001h

nondiscardable

no I/O privilege

32-bit

swappable

preload

code

00000002h

nondiscardable

no I/O privilege

32-bit

swappable

preload

shared data

00000003h

nondiscardable

no I/O privilege

32-bit

swappable

loadoncall

code

00000004h

nondiscardable

no I/O privilege

32-bit

swappable

loadoncall

shared data

00000005h

nondiscardable

no I/O privilege

32-bit

resident

 

code

00000006h

nondiscardable

no I/O privilege

32-bit

resident

 

shared data

00000007h

nondiscardable

no I/O privilege

16-bit

swappable

preload

code

00000008h

nondiscardable

I/O privilege

32-bit

swappable

preload

code

00000009h

nondiscardable

I/O privilege

32-bit

swappable

loadoncall

code

00000011h

discardable

no I/O privilege

32-bit

swappable

 

code

00000012h

discardable

no I/O privilege

32-bit

swappable

 

shared data

00000013h

discardable

no I/O privilege

16-bit

swappable

preload

code

00000014h

discardable

I/O privilege

32-bit

swappable

 

code

FFFFFFFFh

 

no I/O privilege

16-bit

swappable

loadoncall

code

Note especially that a driver is invalid if even one of its objects falls outside the descriptions in this list. Also, types 3 and 4 are not permitted for the object that contains the DDB. Objects of type FFFFFFFFh are neither loaded nor assigned any memory. Types 5 and 6 denote resident objects that are left in memory if the driver is unloaded and are not refreshed if the driver is reloaded.

Control Functions

Two control functions are defined to manage the initialisation and deactivation of VxDs designed to be loaded by VXDLDR. At the very least, the driver must expect to be called to process the Sys_Dynamic_Device_Init control, this being its first chance to execute after being loaded into memory by VXDLDR. The driver will receive the Sys_Dynamic_Device_Exit control only if an attempt is made to unload the driver. The attempt may be refused simply by setting the carry flag, as if the control function were unrecognised.

Control Function 001Bh: Sys_Dynamic_Device_Init

Input:

EAX 0000001Bh

Success:

cf clear

Failure:

cf set

VXDLDR preserves all general registers across this call: it does not expect the driver to preserve any registers, nor to return values in them.

Observe that this control function is not only of interest to drivers designed to be loaded by VXDLDR. When a subsystem driver is loaded, it is possible to specify that VXDLDR should not issue control function 001Bh, but instead leave this to the caller—to the VxD that is building the subsystem. (See VXDLDR Service 0001h for details.)

Control Function 001Ch: Sys_Dynamic_Device_Exit

Input:

EAX 0000001Ch

Success:

cf clear

Failure:

cf set

VXDLDR preserves all general registers across this call: it does not expect the driver to preserve any registers, nor to return values in them.

VxD Services

In addition to the necessary Get Version service, VXDLDR provides two services (numbers 0001h and 0002h, with names VXDLDR_LoadDevice and VXDLDR_UnloadDevice) to load and unload subsystem drivers. These three core services are also available to V86 and PM clients, as discussed later.

Two more services are provided to allow the calling driver to separate the loading of the subsystem driver’s objects from the initialisation of that driver. Thus, the caller who specifies that the VXDLDR_LoadDevice service should only load the driver (but not also initialise it) takes on the responsibility of also calling the driver’s control function 001Bh and then afterwards, of calling VXDLDR Service 0003h or 0004h, depending on whether the driver’s initialisation has succeeded or failed. These two services are named VXDLDR_DevInitSucceeded and VXDLDR_DevInitFailed respectively. They direct VXDLDR to clean up appropriately. When VXDLDR_LoadDevice both loads and initialises, these two extra services are still called—but internally by VXDLDR.

Finally, VXDLDR Service 0005h (named VXDLDR_GetDeviceList) returns the address of the first DeviceInfo structure, so that the caller may determine which drivers have been installed.

VXDLDR Service 0000h: VXDLDR_Get_Version

Success:

cf clear
eax 00000100h

Failure:

cf set
eax 00000000h

As is true for all virtual device drivers, not just VXDLDR, the failure response to service 0000h is determined by the VMM when its int 20h handler cannot find the DDB for the target VxD.

VXDLDR Service 0001h: VXDLDR_LoadDevice

Input:

EAX bit flags:
00000001h initialise driver (as well as loading it)
EDX address of ASCIIZ pathname for file containing VxD

Success:

cf clear
eax address of VxD Descriptor Block (DDB)
edx address of DeviceInfo structure

Failure:

cf set
eax error code:
00000001h memory allocation error
00000002h DOS busy
00000003h file not found
00000004h error reading from file or moving file pointer
00000005h driver already loaded and active
00000006h unsuitable file format
00000007h driver initialisation failure

If VXDLDR has already received the Init_Complete control, then this service, which relies on executing int 21h functions for file operations, cannot be processed if DOS is in a critical section or if either the critical error or In DOS flag is non-zero.

The VxD file must be a DOS executable. The first two bytes of the file must be the letters ‘MZ’. The material that constitutes the VxD is defined by an LE Header, whose properties, as imposed by or understood by VXDLDR, are shown in the Appendix.

In the simplest case, the LE Header is located directly: the dword at offset 3Ch in the executable file gives the offset from the start of the file to the LE Header. As an alternative however, the dword at offset 3Ch may point to an NE Header in which the LE component is identified as a resource of type 0014h, with 0001h as the resource ID. Apart from this brief discussion, the details of this possible inclusion of a VxD as a resource are not discussed here. The only difference of any consequence once the LE Header has been located as a resource (instead of directly) is that the field at offset 80h in the LE Header is taken with respect to the start of the resource rather than the start of the whole file.

If the driver is already loaded, then this service proceeds only if the earlier instance is really just an unloaded driver that has left resident objects behind. The earlier instance is sought by progressing through the DeviceInfo structures looking for a driver whose name matches the one given in the new driver’s Resident Names Table (located via the field at offset 58h in the LE Header). The format of the Resident Name is a single byte that counts the characters in the name that follows. Only this number of characters are compared—which has the side-effect that a driver may seem to be installed already if its name just happens to be a shortening of the name of a driver that really is installed.

A driver is deemed invalid if it contains any object not covered by the types given earlier. Also, the object that contains the DDB may not have type 3 or 4. Objects of type ‑1 are ignored: they are not loaded into memory, but neither does their existence invalidate the driver. Resident objects may persist from a deactivated instance of the same driver. The coding assumes that the driver carries no more than one object of each supported resident type (5 and 6, for code and data respectively).

The pages that comprise an object may be either physical or virtual. In the latter case, the pages are part of the driver’s image in memory but do not appear in the file: VXDLDR sets aside the appropriate amount of memory and fills it with zeros. The presence of virtual pages that are not marked for initialisation with zeros causes the driver to be invalidated.

Physical pages are read from the file; curiously, VXDLDR does not check the success or failure of its int 21h calls during this step. Physical pages require run-time relocations, also called fixups. An unrecognised fixup results in the rejection of the driver. The present implementation of VXDLDR contains code that seems intended to support external fixups where the target is imported from another module, but this code almost certainly doesn’t work.

Once the driver’s image in memory is properly formed, a DeviceInfo structure is built for the driver (or refreshed, if one exists already from a deactivated instance). VXDLDR then sets the DDB_DYNAMIC_VXD bit in the DDB_Flags field at offset 0Ah in the driver’s DDB—which enables the driver to determine, when it is eventually called, whether it has been loaded normally or by VXDLDR.

The remaining housekeeping, at least as much as the loading of the new driver is concerned, is to tell the debugger that new material has been loaded. Thus, if the debugger is running, it is notified of the address, size, etc of each object via int 41h function 0150h (which is roughly equivalent to the int 68h function 50h that the real-mode portion of WIN386.EXE uses after loading objects for VxDs specified by device statements in SYSTEM.INI). There seems to be a slight problem with the notification, however, in that VXDLDR decides an object’s identity as code or data by testing bit 0 of the object type at offset 08h in the ObjectInfo record: the notion that odd-numbered types correspond to code and even-numbered types to data is nearly true, but not wholly true.

The driver is now loaded and must now, if it is ever to do anything useful, be given the chance to execute some code. Initialisation consists of calling the driver’s Sys_Dynamic_Device_Init control and then the appropriate VXDLDR service to tidy up: this is VXDLDR_DevInitSucceeded if the control function indicates success by clearing the carry flag or VXDLDR_DevInitFailed if the driver fails the control function. Depending on the flags passed in EAX to VXDLDR_LoadDevice, these final steps may be taken either by VXDLDR as part of this service (as if to make this a load-and-initialise service) or they may be left to the caller.

VXDLDR Service 0002h: VXDLDR_UnloadDevice

Input:

BX VxD ID; or 0000h to use name
EDX address of driver name as ASCIIZ string, if VxD ID not supplied

Success:

cf clear

Failure:

cf set
eax error code:
00000007h driver declined deactivation
00000008h driver not installed

VXDLDR begins by seeking the DeviceInfo structure for the designated driver. The VxD ID, if non-zero, is used in preference to the driver name. If the driver is installed and is still active, then VXDLDR calls the driver’s Sys_Dynamic_Device_Exit control. If the driver approves its deactivation, then all its swappable, non-discardable objects are released and the driver is marked inactive in the DeviceInfo structure. If the debugger is running, then it is notified of the discards via int 41h function 0152h. Note that the driver may have resident objects: these are left in memory. The DeviceInfo structure is left in the chain of these structures, but is marked inactive by clearing the status flag at offset 04h. Leaving the DeviceInfo structure in memory allows VXDLDR to locate resident objects in any subsequent attempt to reload the driver.

The driver must already have been loaded and initialised before this service is called. Note in particular, that this service assumes there is no need to release discardable objects but presumes instead that these will have been released already when the driver’s initialisation was completed (see the notes to the VXDLDR_DevInitSucceeded service). If the driver has been loaded but not fully initialised, then the correct procedure is not to unload the driver via VXDLDR_UnloadDevice, but instead to call VXDLDR_DevInitFailed, which will release all the driver’s objects.

VXDLDR Service 0003h: VXDLDR_DevInitSucceeded

Input:

EDX address of DeviceInfo structure for driver

Success:

cf clear

Failure:

cf set
eax error code:
00000008h plausible DeviceInfo structure not found at given address

This service is intended to be called after the driver’s Sys_Dynamic_Device_Init control has returned successfully. Indeed, if the driver was initialised as part of the VXDLDR_LoadDevice service, then VXDLDR calls this service immediately after the driver returns successfully from the control function. Put another way, this service performs VXDLDR’s housekeeping after a successful driver initialisation. It exists so that when the VXDLDR_LoadDevice service is used to load but not initialise, the caller may perform additional work while the driver’s discardable objects are still present.

The service checks for the ‘XVLD’ signature 0Fh bytes from the address passed in EDX and also that the number of objects is non-zero. If the DeviceInfo structure is not yet linked into the chain, then it is placed at the start (so that its address will be the one returned by the VXDLDR_GetDeviceList service). The status flag at offset 04h in the DeviceInfo structure is set to 01h to indicate that the driver is operational. Then, all discardable objects are released from memory. If the debugger is running, then it is notified of the discards via int 41h function 0152h.

VXDLDR Service 0004h: VXDLDR_DevInitFailed

Input:

EDX address of DeviceInfo structure for driver

Success:

cf clear

Failure:

cf set
eax error code:
00000008h plausible DeviceInfo structure not found at given address

This service is intended to be called if the driver has been loaded successfully but cannot be initialised. The typical circumstance is that the driver responded to the Sys_Dynamic_Device_Init control by setting the carry flag, which indicates that the driver failed to initialise and must now be removed. VXDLDR calls this service internally when it aborts the initialisation of a driver that it has been asked to both load and initialise through the VXDLDR_LoadDevice service.

When VXDLDR_LoadDevice is used to load but not initialise, it is conceivable that the load may succeed but that the caller decides it cannot proceed with the initialisation after all. In such a case, VXDLDR_DevInitFailed would be called to abort the driver, even though the Sys_Dynamic_Device_Init control had not been issued.

The service checks for the ‘XVLD’ signature 0Fh bytes from the address passed in EDX and also that the number of objects is non-zero. All the driver’s objects are then released from memory. If the debugger is running, it is notified of each discard via int 41h function 0152h. Finally, if the DeviceInfo structure is not linked into the chain (there having been no prior instance of the driver), then the memory occupied by the DeviceInfo structure is also released. Presumably through oversight, the separate heap allocation obtained for the driver name is not freed.

If the VXDLDR_DevInitSucceeded service has already been called to complete the driver’s initialisation, then the correct way to unload the driver is through the VXDLDR_UnloadDevice service, not this one. Indeed, using this service would be slightly unsafe since an attempt would be made to release heap space that had previously been used for discardable objects; there would arise the faint possibility that the same address had since been allocated to another driver, which would not welcome the memory’s unexpected release.

VXDLDR Service 0005h: VXDLDR_Get_Device_List

Output:

eax address of DeviceInfo structure for most recently loaded driver

V86 and PM API Functions

VXDLDR provides V86 and PM clients with three API functions that are nearly equivalent to the first three driver services. For details, refer to the notes which follow the corresponding driver services.

The API functions take the function number in AX. The general scheme is to indicate success or failure via the carry flag, and with an error code in ax. For unsupported functions, VXDLDR sets the carry flag (but does not set an error code in ax).

API Function 0000h: VXDLDR_APIFUNC_GETVERSION

Input:

AX 0000h

Output:

cf clear
ax 0000h
dx 0100h

API Function 0001h: VXDLDR_APIFUNC_LOADDEVICE

Input:

AX 0001h
DS:(E)DX address of ASCIIZ pathname for file containing VxD

Success:

cf clear
ax 0000h

Failure:

cf set
ax error code:
0001h memory allocation error
0002h DOS busy
0003h file not found
0004h error reading from file or moving file pointer
0005h driver already loaded and active
0006h unsuitable file format
0007h driver initialisation failure

The designated driver is necessarily both loaded and initialised through the VXDLDR_LoadDevice service. The facility for separating the two stages is not available.

API Function 0002h: VXDLDR_APIFUNC_UNLOADDEVICE

Input:

AX 0002h
BX VxD ID; or 0000h to use name
DS:(E)DX address of driver name as ASCIIZ string, if VxD ID not supplied

Success:

cf clear
ax 0000h

Failure:

cf set
ax error code:
0007h driver declined deactivation
0008h driver not installed

Appendix: The VxD as a File on Disk

Although the VxD operates in memory, it is stored on disk. When VXDLDR loads a driver, it has to create a memory image from the disk image. The memory image is treated in logical units, called pages, whose size is fixed (typically to 4KB, the same as a CPU page). Some of these pages have corresponding physical pages in the file; others are wholly virtual. To help manage the pages, there is a hierarchical organisation in which pages are grouped into objects. The pages in an object are kept together in linear address space and they share the memory management attributes of the object.

The LE Header

The top-level structure for correlating the memory and disk images is the LE Header at the start of the driver’s disk image. Some fields in the LE Header are concerned with identifying the nature of the module: the LE Header appears to have been designed for wider applicability than just Windows VxDs. The following table shows the format of the LE Header only as far as used by VXDLDR.

Offset Size Description
00h 2 bytes signature: LE
08h word target CPU: must be ≥ 0002h to indicate 80386 or higher
0Ah word target operating system: must be 0004h to indicate Windows 386 Driver
10h dword bit flags describing module type:
00038000h mask must produce 00038000h to denote dynamically loadable driver
14h dword number of physical pages
28h dword size of page
2Ch dword size of last physical page
40h dword offset from LE Header to Object Table
44h dword number of entries in Object Table
48h dword offset from LE Header to Page Map
58h dword offset from LE Header to Resident Names Table
5Ch dword offset from LE Header to Entry Table
68h dword offset from LE Header to Fixup Page Table
6Ch dword offset from LE Header to Fixup Record Table
70h dword offset from LE Header to Imported Modules Name Table
74h dword number of imported module names
80h dword offset from start of file to first physical page
C0h word VxD ID
C2h word target Windows version: may range from 0300h to 030Ah inclusive

Observe that for some fields, the description gives conditions that must be met if VXDLDR is to consider the file as valid for a subsystem driver that it is allowed to load.

Objects

The number of objects comprising the driver’s memory image is given by the dword at offset 44h. VXDLDR does not check the number of objects against an upper bound: the present implementation allows space in its internal records for 14 objects. Each entry in the Object Table addressed by the field at offset 40h in the LE Header is 18h bytes long:

Offset Size Description
00h dword object’s size in memory
08h dword bit flags giving object’s properties:
00000004h executable
00000010h discardable
00000020h shared
00000040h preload
00000700h mask giving residency properties:
00000000h swappable
00000200h resident
00002000h 32-bit
00008000h IOPL
0Ch dword 1-based number of first logical page in object
10h dword number of logical pages in object

Note that only some of the fields and some of the flags hold any interest to VXDLDR. The full list of permitted combinations of the bit flags described above is given earlier.

See that the fields at offsets 0Ch and 10h in the Object Table Entry show which logical pages form the object. The amount of memory VXDLDR obtains for an object—its virtual size—is given by the dword at offset 00h. It is allowed for an object’s virtual size to exceed the sum of the logical pages, in which case, VXDLDR fills the excess memory with zeros.

Pages

It remains to determine how the logical pages are to be prepared. The field at offset 48h in the LE Header points the way to an array of Page Table Entries, one for each of the driver’s logical pages:

Offset Size Description
00h byte high 8 bits of 1-based physical page number
01h byte middle 8 bits of 1-based physical page number
02h byte low 8 bits of 1-based physical page number
03h byte page type:
00h page has physical image in file
03h page must be initialised with zeros

In a round-about fashion (at least to eyes accustomed to 80x86 machines), the first three bytes give the correspondence between the logical page and its physical counterpart, if one exists. The physical page number is 1-based, with zero denoting that no physical counterpart exists. Only two page types are permitted: anything other than 00h or 03h in the last field of a Page Table Entry causes VXDLDR to invalidate the whole driver.

The Driver Name

The Resident Names Table, pointed to by the field at offset 58h in the LE Header, provides VXDLDR with the name of the driver, stored as a string in the Pascal style, where the first byte is a count of the characters that actually form the string.

The Entry Table

Only one exported label is sought, namely the Driver Descriptor Block (DDB). VXDLDR needs this structure only to find the driver’s control procedure and to set the DDB_DYNAMIC_VXD bit in the DDB_Flags field (to distinguish loading by VXDLDR from normal loading by WIN386).

The field at offset 5Ch in the LE Header addresses an Entry Table, which seems to provide for a set of exported labels, even though VXDLDR is interested in just one. The coding suggests that the Entry Table consists of a header that identifies the object and describes the type of entry point, followed immediately by a second structure that actually locates the entry point. Presumably, this second structure has variable size, depending on the type specified in the header, or is actually the beginning of an array. It must also be borne in mind, while speculating on the more general purpose, that the header must be replicable in order to support exported labels defined in more than one object.

The Entry Table itself has the form:

Offset Size Description
00h byte must be non-zero
01h byte bit flags:
7Fh mask giving entry point type: must be 03h
02h word 1-based number of object containing DDB

If the Entry Table does not meet the conditions given for the fields at offsets 00h and 01h, then VXDLDR deems the driver to be invalid.

The Entry Table is followed immediately by a structure of the form:

Offset Size Description
01h dword offset of DDB with respect to object

Run-time Relocations

The contents of a physical page are prepared in the file’s image without knowing just where that page will be loaded into memory. Corrections, called fixups, have to be made when the page is loaded from the file.

The Fixup Page Table is an array of dwords for each page in the module. The dword is an offset from the start of the Fixup Record Table to the first Fixup Record for the designated page. In effect, the dwords in the Fixup Page Table are fence posts that separate Fixup Records for successive pages.

Each Fixup Record is a fixed-sized header followed immediately by variably-sized fields:

Offset Size Description
00h word flags
000Fh mask giving type of fixup:
0007h offset
0008h relative offset
0020h set if record provides multiple fixups to same target
0300h mask giving fixup style
0000h internal fixup
0100h external fixup (not properly supported)
1000h set if target offset is dword rather than word
4000h set if object number is word rather than byte
02h word for single fixup offset where fixup is to be applied, i.e., source offset
byte for multiple fixups number of source offsets that follow this Fixup Record
varies byte or word 1-based object number for the fixup’s target, if internal fixup;
1-based module number for the fixup’s target, if external fixup
varies word or dword offset component of target

Note that the second field (at offset 02h) is a word or byte, with a different meaning in each case, depending on the 0020h bit in the flags. The third field is a byte or word depending on the 4000h bit in the flags. The fourth field is a word or dword depending on the 1000h bit in the flags.