我正在浏览大约 1992 年的一个反汇编的 16 位 DOS 游戏。原始系统要求指出该游戏需要 IBM AT 兼容机器或更高版本的 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
这是我目前能想到的:
检查 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:从ES读取64K到AL。看起来很忙;ES 没有被设置为任何有用的东西,也没有读取 AL。测试的核心似乎是围绕 CX 永远不会达到零的想法构建的,可能是因为循环期间某处的中断?不是应该中断程序iret
并返回这里完成吗?
检查5:自修改代码?看起来它用 替换了测试的最后几条指令STI
,从而删除了INC
否则会影响返回值的 ?在什么情况下它不会覆盖,从而执行INC
?
(旁白:push cs; pop es
可以改写为mov es,cs
还是不是法律形式?)
我觉得我对它的理解还很远,但显然还有一些漏洞。我也不太熟悉 x86,所以我翻译的评论中也可能存在误解。我感觉到这里有一些真正的聪明之处,由非常详细地了解这些机器复杂性的人所写。如果可以的话,我想在某种程度上了解他们的魔法。