没有人期待......

2021-04-18 18:25:17

好吧,也是没有人预计,磨坊的跑道Novell Ne02000 NDIS驱动程序只会因为它在486或更高版本的CPU上运行而崩溃/挂起。

我想尝试使用Microsoft的LAN Manager 2.0(1990)附带的“基本”DOS重定向器,越来越多的硬币翻转我决定使用包装附带的NE2000 NDIS驱动程序。此前我毫无疑问,Microsoft的NE2000.DOS驱动程序与LAN Manager 2.1和Microsoft网络客户端2.0一起发布。

但旧的LAN经理NE2000.DOS驱动程序(16,342字节,日期为11-19-90,拨打自己版本0.31)加载,然后一旦NetBind启动,就会及时悬挂:

起初我自然怀疑了卡配置或网卡硬件的一些问题,但我发现的东西更令人惊讶。

驱动程序挂起的原因实际上与网络无关。驾驶员在一个明显试图检测到CPU类型的例程中挂起的例程。有人怎么能如此简单地搞砸?出色地…

问题可能是说明英特尔与微软之间的滥用关系。英特尔告诉开发人员如何检测CPU生成(在CPUID中简化的东西之前)。微软继续彻底忽略了这一建议,也许在英特尔不敢打破微软的代码的知识中感到安全。除了在这种情况下,英特尔别无选择。

有点背景:NE2000驱动程序有一个充分的理由检测它是否在286或更高版本的CPU上运行。如果它确实,它可以使用REC INSW和REP OUTSW指令从卡中获取数据。即使NE2000处于8位插槽,驾驶员仍然可以使用REC INSB和REP OutsB,显着优于循环中的时间按下数据一个字节。

驾驶员的压力较少但仍然明智的原因来检测386 CPU。它可以使用Rep Movsd用于内存副本,这可能对网络驱动程序性能产生一些显着的影响。

司机在我看来绝对无需检测CPU是否为486,但它确实如此,这就是事情的错误。驾驶员中的CPU检测很有意思,并且足够缺陷,我将完整引用它(标签和评论是我的):

SGDT_BUFFER DB 6 DUP(0FFH)检测_CPU PROP接近推式BP MOV BP,SP POP AX CMP SP,AX;将在80186+ JZ SHORT NOT_8086 MOV AX,1;表示8086 STC JMP短QuitNot_8086:; 286 Won' T写下最后一个字节SGDT FWOW PTR DS:SGDT_BUF SAR DS:SGDT_BUF + 5,1;低点携带标志JNC短not_286;将设置在286 STC MOV AX,11h;表示286 JMP短QuitNot_286:MOV EBX,CR0;读取CR0,保存在EBX MOV EAX,EBX XOR EAX,20000000H;尝试翻转WT位MOV CR0,EAX MOV EAX,CR0;阅读新的CR0值MOV CR0,EBX;恢复原始值CMP EAX,EBX; CR0实际上是否改变了? JNZ CR0_DIFFERS CLC MOV AX,20h; CR0不变:386 JMP短语QuitCr0_differs:Mov Ax,40h; CR0已更改,必须是486+ Clcquit:Mov SP,BP POP BP RetnDetect_CPU ENDP

代码的第一部分实际上非常常见,并且利用了8086/8088的事实,推动SP实际上将SP的新值推动在堆栈上,而所有后来的CPU都会推动旧值。检测代码(如果有的话)演示8086行为没有意义:一系列推送SP / POP SP实际上改变了SP,并且在较新的CPU上它没有。

下一个部分是事情开始发生问题的地方。代码假定如果CPU不是8086,则它必须至少为80286并且能够执行SGDT指令。确实,具有80186的PC并不常见,但此代码将在80186上崩溃并刻录,因为SGDT不存在。

在286及更高版本上,SGDT将愉快地执行,并且由于设计相当可疑,它不是一个特权指令。现在,SGDT指令有点有趣,而不是英特尔的SGDT / SIDT文档是。编写代码以假设286将将第六个字节存储为所有(我相信发生的事情)或根本不写它。代码还假设386+ SGDT将始终写入六个字节,这实际上仍然发生在英特尔的文档可能会说。

代码进一步假设32位GDT基地址的高字节不会具有其低位集。这实际上是一个糟糕的假设,因为虽然CPU重置后它将是真的,但CPU可能以V86模式运行,或者它可能已切换到受保护模式和后退,并且没有讲述GDTR可能包含的GDR。当然,如果你绝对确定PC不能有超过16 MB的RAM,那么GDTR的高字节可能会很清楚,但这只是一个安全的假设。

使用SIDT指令将稍微更好,因为至少在实模式下它必须指向IVT,但即便如此,它可能会在V86模式下混淆检测。换句话说......有原因是为什么不使用这种检测386的方法。

也就是说,可能发生的最糟糕的是,当驾驶员在386或更新的处理器上真正运行时,驾驶员会在286上运行,它可能会略微慢,但很可能没有人会注意到。

但现在我们达到真正的问题,这是486检测。同样,我不知道代码甚至试图检测到486,因为NE2000驱动器真的不关心CPU是否是386或486.我只能假设检测例程被复制并从其他地方粘贴。

无论如何,检测程序试图在CR0寄存器中翻转WT位;如果该位不改变,则CPU必须是386,如果它确实将其更改为486。

这是一个表面上不好的想法,因为往返CR0的移动是特权指令(与SGDT不同)。这种检测对于OS / 2或NT的初始化代码,但是在可能使用内存管理器等中运行的DOS代码中的初始化代码也不伟大,但这不是它最糟糕的问题。

现在,强调这个司机是在1990年11月的时间戳中的时间戳。它可能是几个年龄较大的蛾,几乎肯定是。 1990年年中没有很多486岁,但有一些,微软肯定会有一些。

谁写的代码清楚地看着英特尔的初始I486数据表于1989年4月,英特尔订单号240440-001。在第24页,它说设置CR0。 WT位将启用内部缓存写入并无效。由于486年初的型号没有写入缓存,因此该位实际上是一个无人,但仍然可以翻转。

除...哎呀。英特尔的原版486设计是通过设置CR0.CE位(CR0的位30)来实现缓存,这是完全逻辑的,只有它结果是一个非常糟糕的想法。因为在更新CR0并清除CE位时,几乎所有现有的386代码都会及时禁用缓存。

因此,Intel修改了486,使得CE(缓存启用)和WT(写入)位重命名为CD(缓存禁用)和NW(未写入),并且它们的含义被反转。现有386个代码,将这些位写为零,然后将启用缓存和写入。从1989年11月(英特尔订单号240440-002)的更新I486数据表中,比特的新含义很好地记录了。

对于NE2000驱动程序中的检测代码,此更改具有不幸的副作用,即CD清除,NW集的比特组合变得无效,而先前CE清除,WT集是有效的。在CD和WT位既清晰的典型方案中,当NE2000驱动器CPU检测码运行时,在编写CR0寄存器时,翻转WT位会在WT位产生无效组合和GP故障。这正是我看到司机挂起的原因。

现在这是奇怪的事情:即使是最早的I486,也始终记录了高速缓存的组合,写入禁用禁用。如果缓存禁用,则翻转WT / NW位仅适用。即使在486年代早期(可能)或实际接受无效组合(也可能),也可以崩溃。

NE2000.DOS驱动程序很可能与LAN Manager 2.0发布的486台机器最佳的最低测试。它可能只在早期修订486s,或根本没有测试。 CPU检测码在386和较低的CPU上足够安全,仅在486+上易于崩溃。

像往常一样,这表明了解太多的危险。如果作者没有试图展示他们在(可能)1989年(可能)1989中检测到486的聪明,则该代码将有效。如果他们实际上遵循英特尔的指导方针并试图翻转EFLAGS.Ac位,则检测到486,该代码也会工作。

相反,这是测试软件中最糟糕或完全未经测试的代码的另一个例子,躺在等待之前,直到用户升级他们的硬件,然后在他们身上弹出一个令人讨厌的惊喜。

许多程序员不喜欢英特尔的官方CPU检测代码,因为它有点大而笨重,并不总是完美的,但至少它比旧的Microsoft Ne02000司机的憎恶更好。