Archives
- October 2025
- September 2025
- August 2025
- July 2025
- June 2025
- May 2025
- April 2025
- March 2025
- February 2025
- January 2025
- December 2024
- November 2024
- October 2024
- September 2024
- August 2024
- July 2024
- June 2024
- May 2024
- April 2024
- March 2024
- February 2024
- January 2024
- October 2023
- September 2023
- August 2023
- July 2023
- June 2023
- May 2023
- April 2023
- March 2023
- January 2023
- December 2022
- November 2022
- October 2022
- September 2022
- July 2022
- June 2022
- May 2022
- April 2022
- March 2022
- February 2022
- January 2022
- December 2021
- November 2021
- October 2021
- September 2021
- August 2021
- July 2021
- June 2021
- May 2021
- April 2021
- March 2021
- February 2021
- January 2021
- December 2020
- November 2020
- October 2020
- September 2020
- August 2020
- July 2020
- June 2020
- May 2020
- April 2020
- March 2020
- February 2020
- January 2020
- December 2019
- November 2019
- October 2019
- September 2019
- August 2019
- July 2019
- June 2019
- May 2019
- April 2019
- March 2019
- February 2019
- January 2019
- December 2018
- November 2018
- October 2018
- August 2018
- July 2018
- June 2018
- May 2018
- April 2018
- March 2018
- February 2018
- January 2018
- December 2017
- November 2017
- October 2017
- August 2017
- July 2017
- June 2017
- May 2017
- April 2017
- March 2017
- February 2017
- January 2017
- December 2016
- November 2016
- October 2016
- September 2016
- August 2016
- July 2016
- June 2016
- May 2016
- April 2016
- March 2016
- February 2016
- January 2016
- December 2015
- November 2015
- October 2015
- September 2015
- August 2015
- July 2015
- June 2015
- May 2015
- April 2015
- March 2015
- February 2015
- January 2015
- December 2014
- November 2014
- October 2014
- September 2014
- August 2014
- July 2014
- June 2014
- May 2014
- April 2014
- March 2014
- February 2014
- January 2014
- December 2013
- November 2013
- October 2013
- September 2013
- August 2013
- July 2013
- June 2013
- May 2013
- April 2013
- March 2013
- February 2013
- January 2013
- December 2012
- November 2012
- October 2012
- September 2012
- August 2012
- July 2012
- June 2012
- May 2012
- April 2012
- March 2012
- February 2012
- January 2012
- December 2011
- November 2011
- October 2011
- September 2011
- August 2011
- July 2011
- June 2011
- May 2011
- April 2011
- March 2011
- January 2011
- November 2010
- October 2010
- August 2010
- July 2010
Nobody Expects…
…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.
7 Responses to Nobody Expects…
I think Intel also changed the opcode for CMPXCHG, which used to be the same as IBTS/XBTS.
Yes. Because that (recycling the opcode) upset code trying to detect old broken 386 steppings.
You are, as usual, quite right on the topic on the 486.
On a slightly side topic, there is something else where this code is not completely correct: as it happened, INSB/W and OUTSB/W were introduced in the 80186/80188 (and I clearly remember having read code like this for this exact reason, taking advantage of that feature); in other words, detection of the 80286 and hence the protected mode features was not strictly necessary either…
In fact, while reading, I realize you know it (see your comment about the 80186+, which is not correct: a 186 pushes SP the same way as an 8086 and unlike a 286; the classical code to detect a 186 involves shifts for 32 bits or more, not push SP.) So in fact, there is no problem of a potential failure of executing SGDT on a 186.
The real problem is that the code (or the coder) is not knowledgeable enough and quite simply, ignores the 80186/8 processors (treating them as mere 8088s) and go directly for the big fish… and the fails spectacularly!
You’ve piqued my curiosity about the 80188/80186. It’s pretty clear that a lot of confusion (including that NE2000 driver CPU detection) stems from the fact that there were almost no PCs with an 80188/186 processor. Not zero but very few. Another compounding factor is that Intel’s CPU documentation tends to ignore the 186 entirely, and only talks about the 8086 and 286 as if there were nothing in between.
The situation with 186/188 CPUs is perhaps nicely illustrated by the fact that in my hardware pile there’s probably at least a dozen 80186/188 processors, but they are all in SCSI HBAs, Ethernet NICs, maybe even some hard disks. Because that’s where those CPUs (or perhaps more like SoCs?) went.
I find it somewhat difficult to believe that the 186 would have the same (real mode) instruction set as a 286 and generally the same microcode (shifts, writes to offset 0xffff) but differ in the PUSH SP behavior. Not impossible, just difficult to believe.
There are probably no 80186/188 PCs where a NE1000/NE2000 driver would be useful anyway. NEC V20/V30 is much more likely.
As I understand it, an NEC V20 also has INSW so would benefit if the detection would treat it as a 286.
I also doubt that there were any PCs with an 80188 or 80186 that would have any reason to run a NE2000 driver.
The 80188 and 80186 were released in 1982. At the time it was kind of not known that 100% PC hardware compatibility would be an important feature for x86 based computers, so the extra stuff built in in an 80188/80186 not only differs from the separate stuff in a PC but is also hardcoded in a way that conflicts with the address map on a PC (at least unless you turn off a bunch of the built in stuff, which kind of makes an 80188 or 80186 a bad idea).
It’s interesting though that if the 80188 and 80186 had been a bit different but with a feature set to what they actually ended up with, they could had been the first “PC on a chip”. Instead they ended up with “something else x86 based on a chip” which is why they were used in so many “advanced embedded” systems and also some non-PC-compatible MS-DOS / CP/M-86 computers. It’s really nice to not need separate chips for timers, interrupt controllers and whatnot.
A product using those processors that many of us probably have fond memories of where the USRobotics modems.
Broken link to: http://www.rcollins.org/ddj/Sep96/Sep96.html
It is on the Wayback Machine:
https://web.archive.org/web/20210728061106/http://www.rcollins.org/ddj/Sep96/Sep96.html
-Bill
This site uses Akismet to reduce spam. Learn how your comment data is processed.