…the Spanish Inquisition!
Well, that too, but also nobody expects that a bland, run-of-the mill Novell NE2000 NDIS driver would crash/hang just because it runs on 486 or later CPUs.
I wanted to try the “basic” DOS redirector shipped with Microsoft’s LAN Manager 2.0 (1990) and more or less by a flip of a coin I decided to use the NE2000 NDIS driver that came with the package. Previously I had no trouble with Microsoft’s NE2000.DOS driver shipped with LAN Manager 2.1 and Microsoft’s Network Client 2.0.
But the old LAN Manager NE2000.DOS driver (16,342 bytes, dated 11-19-90, calls itself version 0.31) loaded and then promptly hung as soon as Netbind was started:
At first I naturally suspected some problem with the card configuration or the NIC hardware, but what I found was much more surprising.
The reason the driver hung actually wasn’t related to networking at all. The driver hung in a routine that was clearly trying to detect the CPU type. How can someone screw something so simple so badly? Well…
The problem perhaps illustrates the abusive relationship between Intel and Microsoft. Intel told developers how to detect the CPU generation (before CPUID simplified things). Microsoft went ahead and completely ignored that advice, perhaps feeling safe in the knowledge that Intel wouldn’t dare break Microsoft’s code. Except in this case Intel had no choice.
A bit of background: The NE2000 driver has a good reason to detect whether it’s running on a 286 or later CPU. If it does, it can use REP INSW and REP OUTSW instructions to get data to and from the card. Even if the NE2000 were in an 8-bit slot, the driver could still use REP INSB and REP OUTSB, significantly superior to pushing data one byte at a time in a loop.
The driver has a less pressing but still sensible reason to detect a 386 CPU. It can use REP MOVSD for memory copies, which might have some noticeable impact on the network driver performance.
The driver has in my opinion absolutely no need to detect whether the CPU is a 486, but it does, and that’s where things go wrong. The CPU detection in the driver is interesting and flawed enough that I’ll quote it in full (the labels and comments are mine):
sgdt_buffer db 6 dup(0FFh) detect_cpu proc near push bp mov bp, sp push sp pop ax cmp sp, ax ; Will be equal on 80186+ jz short not_8086 mov ax, 1 ; Indicate 8086 stc jmp short quit not_8086: ; 286 won't write last byte sgdt fword ptr ds:sgdt_buf sar ds:sgdt_buf+5, 1 ; Low bit to carry flag jnc short not_286 ; Will be set on 286 stc mov ax, 11h ; Indicate 286 jmp short quit not_286: mov ebx, cr0 ; Read CR0, save in EBX mov eax, ebx xor eax, 20000000h ; Try flipping WT bit mov cr0, eax mov eax, cr0 ; Read new CR0 value mov cr0, ebx ; Restore original value cmp eax, ebx ; Did CR0 actually change? jnz cr0_differs clc mov ax, 20h ; CR0 unchanged: 386 jmp short quit cr0_differs: mov ax, 40h ; CR0 changed, must be 486+ clc quit: mov sp, bp pop bp retn detect_cpu endp
The first part of the code is actually quite common and takes advantage of the fact that on the 8086/8088, PUSH SP actually pushed the new value of SP on the stack, while all later CPUs push the old value. The detection code, if anything, demonstrates why the 8086 behavior didn’t make sense: A sequence of PUSH SP / POP SP actually changed SP, and on newer CPUs it does not.
The next part is where things start getting problematic. The code assumes that if the CPU is not an 8086, it must be at least an 80286 and will be able to execute a SGDT instruction. It is true that PCs with 80186 were not at all common, but this code would crash and burn on an 80186 because SGDT does not exist there.
On a 286 and later, SGDT will execute happily, and due to rather questionable design, it is not a privileged instruction. Now, the SGDT instruction is a bit funny, or rather Intel’s documentation of SGDT/SIDT is. The code is written to assume that a 286 will either store the sixth byte as all ones (which I believe is what happens) or not write it at all. The code also assumes that a 386+ SGDT will always write six bytes, which in fact happens despite what Intel’s documentation might say.
The code further assumes that the high byte of the 32-bit GDT base address won’t have its low bit set. That’s actually a poor assumption, because although it will be true after CPU reset, the CPU might be running in V86 mode or it might have switched to protected mode and back, and there’s no telling what GDTR might contain. Sure, if you are absolutely certain the PC can’t have more than 16 MB RAM, then the high byte of GDTR probably will be clear, but it’s just not a safe assumption.
Using the SIDT instruction would have been slightly better because at least in real mode it has to point at the IVT, but even then it might confuse the detection in V86 mode. In other words… there’s a reason why this method of detecting a 386 isn’t often used.
That said, the worst that can happen is that the driver thinks it’s running on a 286 when it’s really running on a 386 or newer processor, and it might run a little slower but most likely no one will even notice.
But now we get to the real problem, which is 486 detection. Again, I don’t know why the code is even trying to detect a 486, since the NE2000 driver really does not care whether the CPU is a 386 or 486. I can only assume the detection routine was copied and pasted from somewhere else.
At any rate, the detection routine tries to flip the WT bit in the CR0 register; if the bit does not change, the CPU must be a 386, if it does change it’s assumed to be a 486.
This is an ostensibly bad idea because moves to and from CR0 are privileged instructions (unlike SGDT). Such detection would be fine in, say, initialization code for OS/2 or NT, but it’s not that great in a piece of DOS code that may run with a memory manager etc. But that’s not the worst problem with it.
Now, it is important to underscore that this driver is timestamped November 1990. It could well be a couple of moths older, and almost certainly is. There were not a lot of 486s around in mid-1990, but there were some, and Microsoft certainly would have had a few.
Whoever wrote the code clearly looked at Intel’s initial i486 datasheet from April 1989, Intel Order Number 240440-001. On page 24, it says that setting the CR0. WT bit will enable internal cache write-through and invalidates. Since the early 486 models had no write-through cache, the bit was actually a no-op but still could be flipped.
Except… whoops. Intel’s original 486 design was to enable cache by setting the CR0.CE bit (that is bit 30 of CR0), which is perfectly logical, only it turned out to be a really bad idea. Because nearly all existing 386 code then promptly disabled the cache when updating CR0 and clearing the CE bit.
Intel therefore revised the 486 such that the CE (Cache Enable) and WT (Write Through) bits were renamed to CD (Cache Disable) and NW (Not Write-Through) and their meaning was inverted. Existing 386 code that wrote those bits as zero would then keep caching and write-through enabled. The new meaning of the bits was well documented on page 18 of the updated i486 datasheet from November 1989 (Intel Order Number 240440-002).
For the detection code in the NE2000 driver this change had the unfortunate side effect that the bit combination of CD clear, NW set became invalid, while previously CE clear, WT set was valid. In the typical scenario where the CD and WT bits are both clear (i.e. caching and write-through enabled) when the NE2000 driver CPU detection code runs, flipping the WT bit produces an invalid combination and GP-faults when the CR0 register is written. That is exactly why I saw the driver hang.
Now here’s the weird thing: The combination of cache enabled, write-through disabled was always documented as invalid, even on the earliest i486. Flipping the WT/NW bit only works if the cache is disabled. Either the detection could crash even on those early 486s (possible) or the invalid combination was actually accepted (also possible).
It is very likely that the NE2000.DOS driver shipped with LAN Manager 2.0 saw at best minimal testing on 486 machines. It is possible that it was only tested on early revision 486s, or not at all. The CPU detection code was safe enough on 386 and lower CPUs, only on a 486+ it was prone to crashing.
As usual, this shows the danger of knowing too much. If the authors weren’t trying to show how clever they were by trying detect a 486 in (probably) 1989, the code would have worked. If they actually followed Intel’s guidelines and detected a 486 by trying to flip the EFLAGS.AC bit, the code would have worked too.
Instead this is another example of poorly tested or perhaps entirely untested code ending up in production software, lying in wait until users upgrade their hardware, and then springing a nasty surprise on them.
Many programmers didn’t like Intel’s official CPU detection code because it was kind of big and clunky, and wasn’t always perfect, but at least it worked better than the abomination in the old Microsoft NE2000 driver.