Close

x86 instruction-handling

A project log for Improbable AVR -> 8088 substitution for PC/XT

Probability this can work: 98%, working well: 50% A LOT of work, and utterly ridiculous.

eric-hertzEric Hertz 02/03/2017 at 09:170 Comments

I want to preface this with:

I do *NOT* Plan to emulate the x86 instruction-set for this project. It's not the goal. BUT the thought has been running through my head since the start, and I've gotten a bit carried-away with it a few times.

@Mark Sherman's #Cat-644 has a great explanation of how to speed-up instruction-emulation for instruction-sets where there's only one byte per instruction. That's a little bit difficult to apply to the x86 instruction-set because many (most?) instructions are multiple bytes. Though, n many cases those additional bytes are somewhat consistent, indicating the operands (and where/if to get them).

So, in addition to processing *instructions*, there's also processing of *operands*.

I've a vague idea of using this same scheme for those operand-bytes, essentially treating them *as* instructions.

The thing that makes this a bit weird is the fact that the instruction is grabbed *before* the operands. So, in the case of Cat-644, or FORTH, or whatnot, the idea is to load the operands into registers (or the stack) in early instructions, then the following instruction is *what to do* with them. In the case of the x86 architecture, however, it's the opposite... This is the instruction, now go get the operands.

So, my idea is somewhere along the lines of queuing the requested instruction, then loading the operands into registers, as though they were "mov register, operand" instructions, then, finally, to execute the queued instruction on those registers.

I think this could effectively reduce the number of *actual* instructions that need to be emulated, as well. E.G. There's 'add' which can be applied to two registers, or two memory locations, or a register and a memory location, and so-on. In one case there's a single-byte instruction used to indicate adding between two registers. In another case the instruction could be as many as 6 bytes (memory-to-memory, requiring addresses). Oh, and there's add 8-bit vs. add 16-bit, to boot!

Instead, implement *one* add instruction, add register to register. Always 16 bits.

But, since we don't know *what* to add, we first have to look at the first instruction-byte to determine the format of the opcode. If it requests additional bytes, then we queue the 'add', then process the second byte as though it consists of 'mov' instructions... 'mov tempOutRegister, memory', 'mov tempInRegister, register'. Then, execute 'add tempOutRegister, tempInRegister', and finally 'mov memory, tempOutRegister'.

I suppose, then, that last pseudo-instruction could, essentially, be another queued-instruction.

In a sense, intermediate instructions themselves are being pushed/popped.

It's still a bit vague, but I think it makes sense, and would probably be faster (and smaller) than having to jump to a function to grab instructions' operands, then return.

------

As far as 8-bit vs 16-bit...

Since the AVR is 8-bit anyhow, this is a bit easier than having to add tests and masks... Each emulated 16-bit register consists of *two* 8-bit AVR registers, already. So, one idea is to perform the 16-bit operation on two registers, then just optionally don't-copy back the high byte. Another idea is to essentially have a "scratch" register. E.G. in the case of "add8 registerOut, registerIn" which is implemented as a jump to "add16 registerOut, registerIn", registerOut consists of two AVR registers in both cases. But the high-register could, essentially, be like /dev/NULL. There'd be a real register that will actually take the high byte of the result, but that register will never be *read*. (Similar to how there's often a dedicated register storing the value '0'... usually this register is a real one which *can* store *any* value, but it's usually not used that way).

I'm not sure how relevant this is, any longer... It made sense in my early experiments, but I also did a lot with pointers, e.g. assigning registerOutH via pointer to the "null register". Pointers can be pretty expensive (and is there such thing as a pointer to a register in AVR's instruction-set anyhow? RAM sure...). It would probably make more sense to just use the same two registers for ins and outs, and move those values to the proper registers thereafter (as described earlier, using 'queued instructions').

Discussions