Whenever possible, Windows 95 made application compatibility tweaks through things like compatibility flags that alter the behavior of the system for any program the flag was applied to. Using compatibility flags allows the fix to be generalized: If one program has a problem, there’s a good chance that another program will also have that same problem. So you can apply the flag to the second program and take advantage of the same fix.

On very rare occasions, the problem is too deeply embedded in the program, and the only reasonable option is to patch it. Out of safety, the Windows 95 team got written permission from the vendor whenever they needed to patch a program. The consultation included detailed information on what the problem was and how it was going to be patched. In exchange, the team requested information from the vendor on what versions of their product are affected (and if they could send those versions for analysis), as well as a promise to fix the problem in their next version, because the next version won’t have the benefit of the patch.

The patches themselves were kept in the registry under HKLM\System\CurrentControlSet\Control\SessionManager\AppPatches\〈ModuleName〉\〈Detection string〉. When a 16-bit module is loaded and its target Windows version is less than 4.0, the kernel looks through all the detection strings and tries them one by one to see if any of them triggers.

The detection string is decoded into bytes. The first byte represents the match algorithm, and the rest are parameters to the algorithm.

Type 01: Matching the NE header using 8-bit offsets. The value of is the 8-bit offset into the header, and the xx values are the bytes to match.

01nn ofxx xx … xx00nn bytesTerminatorRepeat as needed

Type 02: Matching the NE header using 16-bit offsets. The value offs is the 16-bit offset (little-endian) into the header, and the xx values are the bytes to match.

02nn offsxx xx … xx00nn bytesTerminatorRepeat as needed

Type 03: Matching the file contents using 16-bit offsets. The value offs is the 16-bit offset (little-endian) into the file, and the xx values are the bytes to match.

03nn offsxx xx … xx00nn bytesTerminatorRepeat as needed

Type 04: Matching the file contents using 24-bit offsets. The value offset is the 24-bit offset (little-endian) into the file, and the xx values are the bytes to match.

04nn offsetxx xx … xx00nn bytesTerminatorRepeat as needed

Type 05: Matching the file contents using 32-bit offsets. The value offset32 is the 32-bit offset (little-endian) into the file, and the xx values are the bytes to match.

05nn offset32xx xx … xx00nn bytesTerminatorRepeat as needed

Type 06: Matching the 16-bit file size. The value size is the 16-bit file size (little-endian).

06size

Type 07: Matching the 24-bit file size. The value size24 is the 24-bit file size (little-endian).

07size24

Type 08: Matching the 32-bit file size. The value filesize is the 32-bit file size (little-endian).

08filesize

Finally, there is the “combo” detector. This allows you to combine multiple detectors (which must all be satisfied).

Type FF: Combo detector. Each xx block is one of the other detector types (starting with the type byte and ending with the null terminator).

FFnnxx xx … xx00nn bytesTerminatorRepeat as needed

For example, one of the detectors that comes with Windows 95 is ff0601023e0a03000306f05c00. This breaks down as

FFCombo detector06First detector is 6 bytes long01Type 1: Match NE header with 1-byte offsets02Match two bytes3eAt offset 0x3E in the NE header (expected Windows version)0a 03Bytes 0a, 03, indicating Windows version 3.100Terminator03Second detector is 3 bytes long06Type 6: Match 16-bit file sizef0 5cFile size 0x5cf000No more detectors

In practice, you tend to see a lot of file size matches, because any change to a program is highly like to alter the file size. Conversely, you are unlikely to see many file contents matches because those incur additional I/O and are therefore more expensive.

If a match is found, the subkeys indicate the segments to patch, and the values of those subkeys are binary data providing the patch to apply. The names of the values are not significant, but traditionally “Add” patches are named Add and “Change” patches are named Change. If there is more than one Add or Replace patch, tradition dictates that they are given numeric suffixes to distinguish them.

Type 01: Change bytes. The sz is the total size of the patch value. The offs is a 16-bit (little-endian) offset into the segment. The xx values are the bytes expected to be there, and the yy values are the bytes that they will be changed to.

01sz offs nnxx xx … xxyy yy … yynn bytesnn bytes

Type 02: Append bytes. The sz is the total size of the patch value. The offs is a 16-bit offset (little-endian) to where the bytes should be added. (The offset must be greater than or equal to the actual segment size, and the segment will be grown to accommodate the extra bytes.) The xx values are the bytes to add.

02sz offs nnxx xx … xxnn bytes

For example, the detector above comes with this patch: 0109700002ff76eb15. This breaks down as follows:

01Change bytes09Total size of this entry is 9 bytes7000Segment offset is 0x007002Change two bytesff 76Original bytes are ff 76eb 15Change them to eb 15

I chose this example because it’s one of the patches I wrote. It fixes a bug in a sound card driver that corrupts the upper 16 bits of extended 32-bit registers in a hardware interrupt handler. The corruption happens in a debug logging function, so the patch replaces a section of the logging function with eb 15, which is the encoding of an unconditional jump forward by 0x15 bytes. This skips over the section that corrupts the registers and resumes execution at a harmless point later in that function.

The corruption happens because the driver calls a function which corrupts the upper 16 bits of extended 32-bit registers, as is permitted by normal 16-bit code. However, hardware interrupt handlers operate under stricter conditions than normal 16-bit code, and the function in question is not documented as safe to call from a hardware interrupt. This code was always broken, but they mostly got away with it prior to Windows 95.

Before this change, the driver corrupted registers during a hardware interrupts, resulting in unpredictable behavior in the code that was interrupted. (See also: Space aliens.)

Now, with this change, the logging never executes either, but the only place the message gets logged to is the debug terminal, so the only people who see these messages are developers. If the sound card vendor wants to see these messages on their debug terminal, they can fix their bug.

The post Behind the scenes on how Windows 95 application compatibility patched broken programs appeared first on The Old New Thing.


From The Old New Thing via this RSS feed