我正在看一看大约在1992年拆解的16位DOS游戏。原始系统要求指出,该游戏需要运行IBM兼容机或更高版本且具有286处理器的机器。在main()周围有一个存根,用于检查处理器并显示一条错误消息(如果未找到)。它由五个有条件的子测试组成,这些子测试有条件地运行,并且根据测试结果返回范围为0..7的整数。我大致了解了代码的功能(尽管可能会出错;我仍然很缺乏经验,有时会误读/误解了指令序列的含义)。
; ... stack setup omitted ...
pushfw

; ==========================================
; === CHECK #1 =============================
; ==========================================
; Sets FLAGS to 0x0 and then immediately reads it back. On an 8086/80186, bits
; 12-15 always come back set. On a 80286+ this is not the case.
; 8086/80186 behavior: jump to check 3.
; 80286+ behavior: fall through to check 2.
xor ax,ax      ; AX=0x0
push ax
popfw          ; pop 0x0 into FLAGS
pushfw
pop ax         ; pop FLAGS into AX

and ax,0xf000  ; bits 12-13: IOPL, always 1 on 86/186
cmp ax,0xf000  ; bit 14: NT, always 1 on 86/186
               ; bit 15: Reserved, always 1 on 86/186, always 0 on 286+
jz check3

; ==========================================
; === CHECK #2 =============================
; ==========================================
; Only runs if CPU is plausibly an 80286. Last check before returning.
; Sets DL=0x6 if IOPL and NT flag bits are all clear.
; Sets DL=0x7 if any bits in IOPL/NT flags are set.
mov dl,0x6     ; DL is the proc's return val
mov ax,0x7000
push ax
popfw          ; pop 0x7000 into FLAGS
pushfw
pop ax         ; pop FLAGS into AX

and ax,0x7000  ; bits 12-13: IOPL
               ; bit 14: NT
jz done
inc dl         ; DL=0x7 if any bit was set
jmp done
nop

; ==========================================
; === CHECK #3 =============================
; ==========================================
; Only runs if CPU seems to be an 8086/80186.
; Sets DL=0x4 and moves on to...
;   check 4 if 0xff >> 21 == 0
;   check 5 otherwise (how can this happen?)
check3:
mov dl,0x4     ; DL is the proc's return val
mov al,0xff
mov cl,0x21
shr al,cl      ; AL = 0xff >> 0x21
jnz check5     ; when does this happen?

; ==========================================
; === CHECK #4 =============================
; ==========================================
; At this point, DF is still 0. ES doesn't
; point to anything sensible.
; Sets DL=0x2 if the loop completes.
; Sets DL=0x0 if the loop does not complete.
; Moves onto check 5 unconditionally.
mov dl,0x2     ; DL is the proc's return val
sti            ; are interrupts important?
push si
mov si,0x0
mov cx,0xffff
rep lods [BYTE PTR es:si] ; read 64K, ES[SI]->AL, all junk?
pop si
or cx,cx       ; test if loop reached 0
jz check5
mov dl,0x0     ; didn't hit 0. interrupted?

; ==========================================
; === CHECK #5 =============================
; ==========================================
; Leaving memory addresses here because they seem important.
; Here, DL is either 0x0 or 0x2 from check 4, or 0x4 from check 3. Looks like,
; contingent on the INC instruction getting overwritten, DL either stays at
; 0x0/0x2/0x4, or becomes 0x1/0x3/0x5.
check5:
00000B74  push cs
00000B75  pop es        ; Set ES to CS. (why not mov es,cs? illegal?)
00000B76  std           ; DF=1, rep decrements CX
00000B77  mov di,0xb88
00000B7A  mov al,0xfb   ; is this just an STI opcode?
00000B7C  mov cx,0x3
00000B7F  cli           ; are interrupts undesired?
00000B80  rep stosb     ; write 3 bytes, AL->ES[DI]
00000B82  cld           ; DF=0, why does it matter now?
00000B83  nop
00000B84  nop
00000B85  nop
00000B86  inc dx        ; destination when CX=1. overwritten?
00000B87  nop           ; destination when CX=2
00000B88  sti           ; destination when CX=3

done:
popfw
xor dh,dh      ; only keep low bits
mov ax,dx      ; return through AX
; ... stack teardown omitted ...
retf

; Return values:
; AX == 0x0: 8086, normal right-shift, loop aborted, overwrites
; AX == 0x1: 8086, normal right-shift, loop aborted, did not overwrite
; AX == 0x2: 8086, normal right-shift, loop finished, overwrites
; AX == 0x3: 8086, normal right-shift, loop finished, did not overwrite
; AX == 0x4: 8086, weird right-shift, overwrites
; AX == 0x5: 8086, weird right-shift, did not overwrite
; AX == 0x6: 286, with clear IOPL/NT flags
; AX == 0x7: 286, with set IOPL/NT flags

我在这里可以算出什么far:
检查1:看似简单。将FLAGS显式设置为0x0,然后将其读回。 8086会将所有位12..15强制为1,而286则不会。来源。
检查2:仅适用于286,似乎与检查1类似,但特别关注保护模式标志。不知道这对调用者有什么意义。
(旁白:如果我们假设CPU是286,难道不是push 0x7000而不是mov ax,0x7000; push ax吗?)
检查3:计算0xff >> 0x21并查找除0以外的结果。这是怎么发生的?非零结果是否有必要避免进行校验4的原因?
校验4:将64K从ES读取到AL中。似乎很忙。未将ES设置为任何有用的值,也不会读取AL。测试的核心似乎建立在CX永远不会达到零的想法之上,这可能是因为循环期间某处发生了中断吗?不应该中断程序iret并返回此处完成吗?
检查5:自修改代码?看起来它用STI代替了测试的最后几条指令,从而删除了会影响返回值的INC?在什么情况下无法覆盖并执行INC
(除了:push cs; pop es可以改写为mov es,cs还是不合法的形式?)
我觉得我对它的理解还很遥远,但显然还有一些漏洞。我在x86上也不太流利,因此翻译的注释中也可能会有误解。我觉得这里有些真正的聪明,是由一个非常详细地了解这些机器的复杂性的人写的。如果可以的话,我想在某种程度上了解他们的魔力。

评论

检查4码是错误的,因为rep必须首先产生效果。加载期间的中断会导致一个前缀丢失。如果代表丢失了,则退出时cx将为非零。

@peterferrie我参加聚会晚了大约两年,但是你是对的。我终于尝试重新组装它,而二进制文件不匹配。我修复了检查#4中的代表。

#1 楼

我将深入探讨过去,并尝试对您在软件中观察到的各种检查进行解释。我找到了三个解释行为的参考资料(正如我希望的那样),我将在此答案中将其称为/ 1 /,/ 2 /,/ 3 /。

/ 1 / http:// www .drdobbs.com / embedded-systems / processor-detection-schemes / 184409011

这是DrDobbs Journal的存档文章(很遗憾,多年来已经不存在了,但他们的存档仍然是有价值的资源),由Richard Leinecker于1993年6月1日提出,称为“处理器检测方案”。

/ 2 / https://github.com/lkundrak/dev86/blob/master/libc/misc /cputype.c

这是罗伯特·德·巴斯(Robert de Bath)编写的程序,于2013年10月23日发布,还涵盖了类似此处所述的问题,但不幸的是,没有太多的代码注释。 />
/ 3 / iAPX 86/88,186/188用户手册,程序员参考,英特尔,1983年5月

它是标题中所列处理器的英特尔程序员参考,仍然在许多方面都是有效的(技术飞速发展的一个很好的例子c在某些领域中使用hange ...)。

您的检查:

检查1:您自己给出了解释。可以在/ 1 / LISTING ONE(也包含在文章中)中进行验证。我不会在这里重现代码,也就不做进一步评论了,因为您的解释没有什么要补充的。我将引用/ 1 /的描述及其代码。 Quote:

; Is It an 80286?
; Determines whether processor is a 286 or higher. Going into subroutine ax = 2
; If the processor is a 386 or higher, ax will be 3 before returning. The
; method is to set ax to 7000h which represent the 386/486 NT and IOPL bits
; This value is pushed onto the stack and popped into the flags (with popf).
; The flags are then pushed back onto the stack (with pushf). Only a 386 or 486
; will keep the 7000h bits set. If it's a 286, those bits aren't defined and
; when the flags are pushed onto stack these bits will be 0. Now, when ax is
; popped these bits can be checked. If they're set, we have a 386 or 486.
IsItA286    proc
        pushf               ; Preserve the flags
        mov ax,7000h        ; Set the NT and IOPL flag
                            ; bits only available for
                            ; 386 processors and above
        push    ax          ; push ax so we can pop 7000h
                            ; into the flag register
        popf                ; pop 7000h off of the stack
        pushf               ; push the flags back on
        pop ax              ; get the pushed flags
                            ; into ax
        and ah,70h          ; see if the NT and IOPL
                            ; flags are still set
        mov ax,2            ; set ax to the 286 value
        jz  YesItIsA286     ; If NT and IOPL not set
                            ; it's a 286
        inc ax              ; ax now is 4 to indicate
                            ; 386 or higher
YesItIsA286:
        popf                ; Restore the flags

        ret                 ; Return to caller
IsItA286    endp


希望您能立即看到与代码相似的地方。

CHECK3:确定您是否有80186/80188或更早的版本。
从/ 3 /,第3-26页第SHIFTS章中引用:


“在8086,88上最多可进行255个移位。 ...

...在80186、188执行换档(或旋转)之前,它们与
值以1FH进行移位,因此将移位次数限制为32位。“


您的代码已注释: br />
CHECK4:这个还不太清楚,但是,它似乎是对某些CPU错误的测试,似乎是针对8086/88的CMOS版本进行的测试。 /> / 2 /从271ff行以下列出了带注释的以下代码:

mov dl, 0x4     ; DL is the proc's return val
mov al, 0xff    ; al contains 0xff
mov cl, 0x21    ; According to the above explanation from Intel,
                ; this value in cl is in an 80186/188 converted to 1, by ANDing with 0x1F.
shr al, cl      ; 80186/188 => al = 0x7F
                ; other: al = 0
jnz check5      ; goto check5 if you have an 80186/188


它与您的代码不完全相同,但是非常相似,因此我假设您的代码为对80C88处理器进行了良好的测试。我从未听说过此错误,因此在网络上找不到该错误的进一步信息。因此,有点猜想。

CHECK5:这是测试我们是否有一个8086/80186或8088/80188(即16位或8位计算机),您的怀疑是正确的,它是自修改代码,其想法是自修改指令是否已经在预取队列中。这个检查也包含在/ 1 /和/ 2 /中。我从/ 1 /复制了注释。

/ 1 /中的作者这样描述它:


“区分8088和8086比较棘手。我发现最简单的方法是修改IP前面五个字节的代码。
由于8088的预取队列为四个字节,而8086的预取队列为
是6个字节,比IP提前5个字节的指令
不会对8086产生任何影响。”


作为参考,英特尔在其手册/ 3 /,第3-2页“总线接口单元”:


“ 8088/188指令队列最多可容纳四个字节的指令流
,而8086/80186队列最多可以存储六个指令
字节。”


我不会从/ 1 /此处复制代码(非常相似),而是-在代码中添加一些注释,希望可以解释这种情况。

; The CMOS 8088/6 had the bug with rep lods repaired.
cmos:   push si
    sti
    mov cx, #$FFFF
rep
    lodsb
    pop si
    or cx,cx
    jne test8
    mov bx,#2   ; Intel 80C88


由于CHECK3的返回寄存器dx的值为4,因此在16位情况下CHECK5之后的值为dx。

评论


关于检查4,如果在rep期间发生中断,则恢复点将丢失前缀之一。但是,引用的代码是错误的,因为测试需要多个前缀。原始代码也是错误的,因为rep必须首先产生效果(即导致rep丢失并因此退出时cx非零)。

–彼得·弗里
18/09/21在18:21

我修复了原始代码,感谢您指出我的反汇编不准确。我一直在做更多的阅读,看来“丢失前缀”错误是8086/88的错误,但是NEC V20和V30克隆正确地处理了该错误。不过,我还在挖掘。

–smitelli
8月26日2:04

#2 楼

检查2:检查1测试是否可以清除标志字的高位时,检查2测试是否可以将其设置。在80286上,这些位不能设置为实模式,而在80386上可以设置。

检查3:这是测试处理器具有哪种移位器。一些(较新的)具有桶形移位器,可以有效地将移位计数掩盖为单词大小(并且使用0x21作为移位计数向我表明,这种差异出现在80286后时代)。因此,偏移0x21(33)所得到的结果与偏移33-32 = 1相同。我不知道桶形移位器出现在哪一代。

检查4:我不能记住细节,但是其中的一部分对我来说似乎很熟悉。它可能与最大长度循环后的重复计数错误有关,或者与具有双指令前缀的事件触发了CPU错误有关。我认为是后者,前缀的顺序很重要。当中断处理程序返回时,指令指针设置为错误的地址,并且忘记了一个或多个前缀。插图:https://www.youtube.com/watch?v=6FC-tcwMBnU请注意,此处的代码实际上首先具有es:覆盖前缀,因此循环应始终完成!这可能是一个CPU错误检测例程,它本身包含一个错误吗?

Check 5:这是检查是否独立于任何数据缓存运行的指令缓存。在80486上,您可以在处理器当前正在执行的16字节窗口中遍历所有内容,它仍将执行加载到(单独)指令高速缓存中的旧内容。我认为Pentium +处理器会检测到这种覆盖并刷新指令缓存和预取队列。即使是最早的x86处理器,其预取队列的时间也足够长(8088除外)以覆盖被覆盖的指令。新代码执行的条件:在Pentium +(IIRC)上,在单步调试器上,在v86模式下,CLI指令实际上不会生效,并且会发生中断。

评论


186和286没有桶形移位器,但是仍然掩盖了移位计数以限制迭代完成移位的速度!碰巧对于桶式移位器来说,掩盖移位计数也是很自然的。为什么任何现代x86掩码将CL中的计数转换为5个低位都可以解释这一点,并说明了它是186年的新特性。

– Peter Cordes
5月13日21:04