5 ms·
The 8086/88 does in fact execute 90h (the NOP opcode) as XCHG AX, AX. It does not generate any change in the user-visible architectural state, however, and so
by kiwidrew 6y ago
The 8086/88 does in fact execute 90h (the NOP opcode) as XCHG AX, AX. It does not generate any change in the user-visible architectural state, however, and so it appears to be a NOP.
[Internally the CPU uses the "TMP" register as a temporary storage location during execution of the XCHG operation, and so the NOP will clobber whatever value was previously held there.]
One can observe two of the internal registers ("TMP" and "IND") using an invalid JMP FAR m32 opcode. Normally this is encoded as opcode FF/5 with a mod-rm byte that specifies a memory location (mod=00, mod=01, or mod=10) from which to fetch a doubleword CS:IP pointer. Specifying a mod-rm byte with mod=11 (register) - which is an undefined encoding - skips the "load doubleword from memory" microcode subroutine and causes the CPU to jump directly to the location IND:TMP instead, allowing one to observe the values held in these internal registers.
At some point I'll get around to writing this up in more detail, as I don't think anyone else has ever described the behaviour of this particular invalid 8086 opcode.
- userbinator 6y agoAt some point I'll get around to writing this up in more detail, as I don't think anyone else has ever described the behaviour of this particular invalid 8086 opcode. I'd love to see more about that --- there has been plenty of documentation on "first-byte" undocumented opcodes, but far less research and information on undocumented operand combinations/groups and such. In particular, the infamous FF FF (that gets disassembled by DEBUG and a few other assemblers as "??? DI" ) falls in that area, as does LEA with a register operand ("LEA AX, AX") and several others that don't immediately come to mind. If I remember correctly, on the Z80 some of the hidden architectural registers' state was also visible as undefined flags. What you've described sounds very similar to that.
- kiwidrew 6y ago> LEA with a register operand ("LEA AX, AX") Yep, reading about this (from a post that I can't find at the moment) was one of the things that inspired me to try illegal/undefined mod-rm byte encodings.
- kens 6y agoHow did you figure this out? I'll keep my eyes open for this in the microcode.
- kiwidrew 6y agoI was inspired by a post from Raúl Gutiérrez Sanz [1] that described some of the undocumented 8086 opcodes, and was tantalizingly titled "Part I". Unfortunately it seems that Parts II and III were never posted. I had also seen a comment (which I cannot find at the moment) describing the behaviour of LEA r16, m16 when it's given an r16 source operand (so the mod-rm byte has mod=11): the value loaded into the destination register is the value of the internal "IND" register, i.e. whatever memory address the CPU had most recently calculated. So I tried all of the undefined mod-rm encodings (especially the instructions where the 3 'reg' bits encode a sub-opcode) and mostly just found a bunch of aliases for existing instructions. But the FE and FF opcodes -- which encode JMP and CALL -- would randomly crash my test code in strange and beautiful ways. Single-stepping the execution of the opcode would crash the DOS 2.1 version of DEBUG.COM, so I ended up writing my own minimal INT1 handler to capture the value of CS:IP immediately after the malformed JMP or CALL, dump it to the screen, and then restore a sane CS:IP before exiting single-step mode. My hypothesis (from a close reading of the 8086 patent) is that the group decode ROM marks which opcodes have a mod-rm byte and the microcode has a sort of subroutine call (triggered by mod != 11) that calculates the EA and leaves the result in the hidden temporary registers before falling through to the instruction-specific microcode. Skip that subroutine (using mod=11) and interesting things happen. [1] http://www.os2museum.com/wp/undocumented-8086-opcodes-part-i/ http://www.os2museum.com/wp/undocumented-8086-opcodes-part-i...
- billforsternz 6y agoGreat work. I used to keep the MSDOS 2.11 version of debug.com too. All the debug.com goodness in a tiny .com file compared to bloated more recent debug.exe versions. Of course you had to patch it (with debug) to stop it exiting immediately due to the wrong OS version.
- anticensor 6y agoHave you also tried JMP SHORT +0 as an alternative NOP?
- kiwidrew 6y agoI don't really think there is a need for an "alternative" NOP, because the XCHG side effect (of clobbering the TMP register) is of little practical concern. In fact, as far as I have been able to determine, the malformed JMP/CALL FAR m32 operation -- which isn't even remotely useful! -- is the only way that code is even able to 'observe' the TMP register in the first place. For all practical purposes 90h is indeed a NOP, despite the fact that the CPU actually executes the opcode.
- ajenner 6y agoI have written code for a cycle-exact 8088 emulator https://github.com/reenigne/reenigne/blob/master/8088/xtce/xtce.h https://github.com/reenigne/reenigne/blob/master/8088/xtce/x... which handles all the invalid opcodes the same way that real hardware does. There is more potentially-useful way of observing TMP and IND that doesn't involve jumping to code that you might not control: use the LDS or LES opcodes with mod=11.
- kiwidrew 6y agoHmm... I thought that would be the case, but when I tested it [on an AMD 80C88], LDS/LES with mod=11 just skipped the EA calculation subroutine and then proceeds as normal, loading a doubleword from whichever memory address happens to have been left in the IND register. But I'll probably go back and re-test LDS/LES more thoroughly at some point just to make sure I haven't missed something [or more likely, that I'm not misinterpreting my scattered notes]. By the way, I really appreciated your detailed bus sniffer logs of the 8088 executing various instructions. It was enlightening to read through the traces and helped me understand what was going on "under the hood" of the CPU.
- ajenner 6y agoIt might be different on an Intel 8088. What I saw was two bytes loaded from the bus (at the address that was left in the IND register) placed in ES or DS, and the offset part was loaded from a second hidden register that contained the previous word read from or written to the bus (excluding instruction fetches). Glad the sniffer logs were useful! I have been using them pretty regularly for debugging and profiling things.