The IBM ThinkPad 365ED can start Windows 98 DOS from a CompactFlash card in a PCMCIA ATA adapter, but its PCMCIA BIOS is not fully compatible with the Windows 98 IO.SYS boot path.
A normally prepared active FAT16 card reaches IO.SYS and then stops on a black screen with a blinking cursor. Two independent firmware compatibility problems cause the failure:
- the PCMCIA BIOS hangs when
IO.SYSrereads CHS 0/0/1 throughINT 13h; - the BIOS leaves the head-count byte in the geometry structure at zero, causing a 32-bit division by zero inside
IO.SYS.
The MBR bootstrap included in this Gist works around both problems. It caches sector 0, services the problematic BIOS request from memory, repairs the geometry value at the verified IO.SYS instruction, removes its temporary trace handler, and lets DOS continue at normal speed.
Warning
This is highly version-specific boot code that overwrites the executable part of an MBR. Verify every device name, keep a sector-0 backup, and read the limitations before writing anything.
| Component | Confirmed value |
|---|---|
| Computer | IBM ThinkPad 365ED |
| Boot medium | 8 GB CF card in a PCMCIA ATA adapter |
| BIOS disk number | DL=80h |
| Partition | Primary, active, FAT16, type 06h |
| Partition start | LBA 63, CHS 0/1/1 |
| Partition length | 179,361 sectors, 91,832,832 bytes |
| BPB sectors per track | 63 |
| BPB heads | 16 |
| DOS version | Windows 98 4.10.2222 |
IO.SYS size |
222,390 bytes |
IO.SYS SHA-256 |
36889c640067e58019b819cc160d9516bde023affe9529ade7b2ddfdaef56ad9 |
| MBR bootstrap size | 446 bytes |
| MBR bootstrap SHA-256 | 5b85b6d1c26ce2bbbf5455ffe917877b9ad8457620eeb199b12deb4c2965a96f |
| Assembly source SHA-256 | f4e259c396a8bbb95085d97fadd2b6d6caff65ea60e6f9404c30fead1add08c9 |
The rest of the CF card may remain unallocated. Keeping the first FAT16 partition below 2 GiB avoids additional limitations in old DOS and BIOS implementations.
The CF card contains an active FAT16 partition, a valid FAT16 boot sector, and the required system files:
IO.SYS
MSDOS.SYS
COMMAND.COM
The firmware starts the card, but the machine displays only a blinking cursor instead of the Windows 98 banner and C:\> prompt.
At this point:
- the MBR has executed;
- the FAT16 partition boot sector has loaded;
- the partition boot code has found and loaded
IO.SYS; - execution has failed after control reached
IO.SYS.
This is different from IBM errors such as I9990302 and I9990305, which occur at an earlier stage when the partition or boot sector is not usable.
During startup, IO.SYS rereads the physical MBR sector using this register state:
AH=02h read sectors
AL=01h one sector
CH=00h
CL=01h cylinder 0, sector 1
DH=00h head 0
DL=80h first BIOS hard disk
AX=0201h
CX=0001h
DX=0080h
The ThinkPad PCMCIA BIOS does not return from this call. An earlier read of the FAT16 partition boot sector at CHS 0/1/1 succeeds, so the failure is specific to this later CHS 0/0/1 request.
The workaround stores a copy of sector 0 in reserved conventional memory and hooks INT 13h. It handles only the exact AX=0201h, CX=0001h, DX=0080h request from the cache. Every other disk request is chained to the original BIOS handler.
The traced return frame after the cached read was:
return CS:IP = 0070:0522
FLAGS = 0046h
CF = 0
After bypassing the hanging read, execution reaches 0070:12A2. The next bytes are:
66 F7 F3 66
In 16-bit real mode, the first three bytes decode as:
div ebxThe real ThinkPad register state immediately before that instruction was:
AX=0001 BX=0000 CX=0001 DX=0000 DS=9EE0 ES=0560
EBX=00000000
IO.SYS obtains this divisor from the head-count byte at ES:[DI+15h]. The ThinkPad PCMCIA BIOS leaves that byte at zero.
Running an exact image of the CF card in QEMU shows the expected values at the same point:
ES:[DI+15h] = 10h
EBX = 00000010h
The value 16 matches the heads field in the FAT16 BPB. On the real machine, DIV EBX with EBX=0 raises a divide exception before the next disk request, leaving the visible blinking cursor.
The included thinkpad-365ed-pcmcia-mbr.s performs this sequence:
- Relocates from
0000:7C00to0000:0600. - Reduces the conventional-memory value at BDA
0040:0013by 2 KiB. - Copies itself into those reserved 2 KiB at the top of conventional memory.
- Keeps a sector-0 copy at offset
0200hin its resident segment. - Saves and replaces the
INT 13hvector. - Loads the FAT16 boot sector from CHS 0/1/1 and transfers control to it.
- Services the exact repeated MBR read from its in-memory copy.
- On the second matching request, temporarily installs an
INT 1handler and sets the Trap Flag in the return frame. - When saved IP reaches
12A2h, also verifies the bytes66 F7 F3 66at saved CS:IP. - Writes
10htoES:[DI+15h]and00000010htoEBX. - Restores the original
INT 1vector, clears the Trap Flag, and continues without tracing.
Checking both the instruction address and opcode prevents the geometry modification from being applied to unrelated code.
The confirmed layout is:
primary DOS partition
active = yes
type = 06h FAT16
start LBA = 63
sectors per track = 63
heads = 16
size <= 2 GiB
For a card larger than 2 GiB, create a small first FAT16 partition and leave the remaining space unallocated.
Modern LBA-2048 alignment is not compatible with this bootstrap. It loads the partition boot sector from fixed CHS 0/1/1, which corresponds to LBA 63 with 16 heads and 63 sectors per track.
Format the partition as FAT16, then transfer Windows 98 DOS to the correct drive letter:
SYS X:
Determine X: by inspecting the drive capacity. Do not select a drive letter by assumption.
Verify the root directory:
DIR X:\ /A
It must contain at least IO.SYS, MSDOS.SYS, and COMMAND.COM. If FORMAT /S reports insufficient memory, format the partition separately and run SYS X: afterward.
The source is included as thinkpad-365ed-pcmcia-mbr.s. The tested build uses Clang and GNU objcopy from Homebrew binutils:
clang -target i386-unknown-linux-gnu \
-c thinkpad-365ed-pcmcia-mbr.s \
-o thinkpad-365ed-pcmcia-mbr.o
/opt/homebrew/opt/binutils/bin/objcopy \
-O binary -j .text \
thinkpad-365ed-pcmcia-mbr.o \
thinkpad-365ed-pcmcia-mbr.bin
stat -f '%z' thinkpad-365ed-pcmcia-mbr.bin
shasum -a 256 thinkpad-365ed-pcmcia-mbr.binExpected output for the included source:
size: 446
SHA-256: 5b85b6d1c26ce2bbbf5455ffe917877b9ad8457620eeb199b12deb4c2965a96f
Do not write a binary with any other size as the MBR bootstrap area.
diskutil list
diskutil info /dev/disk5
diskutil info /dev/disk5s1/dev/disk5 is only an example. Determine the device number again after every connection.
Confirm all of the following before continuing:
Whole: Yesfor the whole-disk device;Device Location: External;Removable Media: Removable;- the expected total CF capacity;
- a FAT16 first partition;
Partition Offset: 32256 Bytes, or 63 sectors.
Caution
Using the wrong /dev/diskN can overwrite the MBR of an unrelated disk.
diskutil unmountDisk /dev/disk5
sudo dd if=/dev/rdisk5 \
of=cf-sector0-original.bin \
bs=512 count=1
stat -f '%z' cf-sector0-original.bin
shasum -a 256 cf-sector0-original.binThe backup must be exactly 512 bytes. Keep it until the card has passed a real boot test.
The executable MBR area is bytes 0..445. Bytes 446..509 contain the partition table, and bytes 510..511 contain the 55 AA signature. Preserve the last 66 bytes from the original sector:
cp cf-sector0-original.bin cf-sector0-patched.bin
dd if=thinkpad-365ed-pcmcia-mbr.bin \
of=cf-sector0-patched.bin \
bs=446 count=1 conv=notrunc
stat -f '%z' cf-sector0-patched.bin
xxd -g1 -s 446 -l 66 cf-sector0-patched.binThe patched file must remain exactly 512 bytes. Its last two bytes must be 55 aa, and its partition entry must match the original backup.
Writing only 446 bytes directly to /dev/rdiskN can fail with Invalid argument on macOS because the raw device requires a 512-byte-aligned operation. Construct and write the complete 512-byte sector instead.
sudo dd if=cf-sector0-patched.bin \
of=/dev/rdisk5 \
bs=512 count=1 conv=notrunc
sync
sudo dd if=/dev/rdisk5 \
of=cf-sector0-readback.bin \
bs=512 count=1
cmp cf-sector0-patched.bin cf-sector0-readback.bin
shasum -a 256 cf-sector0-readback.bin
diskutil eject /dev/disk5An empty cmp result means the readback matches. Remove the CF card only after a successful comparison and diskutil eject.
- Insert the CF card and PCMCIA adapter while the ThinkPad is powered off.
- Configure the machine to boot from the PCMCIA disk.
- Wait for the Windows 98 banner and
C:\>prompt. - Run:
VER
DIR C:\ /A
COPY /B C:\COMMAND.COM NUL
The expected results are Windows 98 version 4.10.2222, the system files in the root directory, and a successful one-file copy to NUL.
If the card does not behave as expected, write the saved 512-byte sector back using the same verified device:
diskutil unmountDisk /dev/disk5
sudo dd if=cf-sector0-original.bin \
of=/dev/rdisk5 \
bs=512 count=1 conv=notrunc
sync
diskutil eject /dev/disk5This restores the original bootstrap, partition table, and MBR signature from the backup.
- The workaround is tied to the confirmed
IO.SYShash and its instruction layout around saved IP12A2h. - The hard-coded head count of 16 must match the FAT16 BPB.
- The first partition must begin at LBA 63 and map to CHS 0/1/1.
- The CF card must be exposed by the BIOS as drive
80h. - The code has been tested on an IBM ThinkPad 365ED. Other ThinkPad models may have different PCMCIA BIOS behavior.
SYSinstalls the system files and partition boot sector, but it does not install this custom MBR.- Installing the MBR does not format the FAT16 partition. Selecting the wrong physical device can still destroy another disk's partition table.
BIOS loads MBR
-> MBR loads FAT16 boot sector
-> boot sector loads IO.SYS
-> IO.SYS rereads CHS 0/0/1
-> PCMCIA BIOS hangs
cached sector-0 workaround
-> IO.SYS continues
-> BIOS geometry reports heads=0
-> IO.SYS executes DIV EBX with EBX=0
-> divide exception and blinking cursor
geometry workaround
-> heads=16 and EBX=16
-> IO.SYS continues
-> COMMAND.COM displays C:\>
README.md: diagnosis, workaround design, installation, verification, and recovery procedure.thinkpad-365ed-pcmcia-mbr.s: the 16-bit resident MBR bootstrap source.