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 es rep lodsb ; read 64K, ES[DI]->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
(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.