I'm poking through a disassembled 16-bit DOS game circa 1992. The original system requirements state that the game needs an IBM AT-compatible machine or later, with the 286 processor, to run. And there's a stub around main()
that checks for the processor and displays an error message if one is not found.
I was intrigued as to what was actually being tested for, and I tracked down what appears to be the test procedure. It's comprised of five sub-tests, which are run conditionally, and it returns an integer in the range 0..7 depending on the results of the tests. I figured out, broadly, what the code does (although there may be errors; I'm still rather inexperienced and sometimes misread/misinterpret the meanings of instruction sequences).
; ... 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
Here's what I can figure so far:
Check 1: Seems straightforward. Explicitly set FLAGS to 0x0 and then read it back. The 8086 will force all bits 12..15 to 1, and the 286 won't. Source.
Check 2: Only for the 286, seems to be similar to check 1 but with special focus on the protected mode flags. Not sure what significance this is to the caller.
(An aside: If we're assuming the CPU is a 286, couldn't it have been push 0x7000
instead of mov ax,0x7000; push ax
?)
Check 3: Computes 0xff >> 0x21
and looks for a result other than 0
. How does this ever happen? Is there a reason the nonzero result obviates the need for check 4?
Check 4: Reads 64K from ES into AL. Seems like busywork; ES has not been set to anything useful, and AL is not read from. Core of the test seems to be built around the idea of CX never reaching zero, possibly because of an interrupt somewhere during the loop? Shouldn't the interrupt procedure iret
and return here to finish?
Check 5: Self-modifying code? Looks like it replaces the last few instructions of the test with STI
, thus removing the INC
that would otherwise affect the return value? What is the circumstance under which it would fail to overwrite, and thus execute the INC
?
(An aside: Could push cs; pop es
be rewritten as mov es,cs
or is that not a legal form?)
I feel like I'm pretty far along in understanding it, but there are clearly a few holes remaining. I'm also not anywhere near fluent in x86, so there may be misinterpretations in my translated comments as well. I get the sense that there's some real cleverness here, written by somebody who knew the intricacies of these machines at a very detailed level. I'd like to understand their magic on some level, if I can.
rep lods
in check #4.