wwarthen / RomWBW

System Software for Z80/Z180/Z280 Computers
GNU Affero General Public License v3.0
339 stars 99 forks source link

z180 - illegal opcode trap handler #193

Closed feilipu closed 3 years ago

feilipu commented 3 years ago

The z180 traps opcodes that are illegal, including those that are undocumented in the z80. The trap is a RST 0 call, with the appropriate Z180_ITC trap flag set to enable the address of the trapped opcode(s) to be located.

Is it worth putting a trap function into the RST 0 handling for z180 builds? This should at least take away some mystery for users as to why their Z80 code failed.

z88dk has an implementation of the trap code here, with a weak function for the user to implement their own trap handling.

I could write a PR based on this code if desired?

wwarthen commented 3 years ago

I strongly agree with the idea of adding the invalid opcode trap. I had already started looking at this because of the recent issues with TDLBASIC. Before working on any code, I want to discuss the approach.

It is a little challenging with RomWBW because RomWBW works hard to abstract the OS layer from the hardware layer. The CP/M CBIOS has no idea what kind of hardware it is running on. Note that the invalid opcode trap jumps to logical address 0 and, of course, CP/M currently owns that address as a restart vector. CP/M must get control on a jump to 0 because many programs terminate this way.

Given all this, I came up with a couple approaches, neither of which I like. So, I am hoping you can help me think this through and come up with a good solution.

Handle in OS:

The OSes already capture jumps to 0, so they could each be modified to check if they are running on a Z180 and, if so, diagnose the issue before continuing with the normal restart behavior. The problems with this approach are that 1) it requires that the OSes become platform aware, 2) multiple OSes will need to be modified, and 3) ROM apps like BASIC that are run without an OS will not be covered.

Handle in RomWBW:

RomWBW HBIOS would be modified to own the address 0 jump instruction. It would vector to the HBIOS proxy in high ram which would then call an invalid opcode handler in the HBIOS bank. After handling it, the HBIOS would return via a vector that is owned by the OSes. This OSes would set the vector via an HBIOS API call instead of setting the address 0 jump. This would allow the OSes to remain hardware agnostic and would allow trapping of invalid opcodes for ROM-based code that has no OS. However, the issues with this are: 1) space in the proxy area to record the problem address, route to the HBIOS handler, and then route to the userland vector, 2) all OSes would need to be modified to use the new HBIOS userland vector instead of address zero.

I would very much like thoughts on this.

By the way, RomWBW already handles invalid opcodes on Z280 because the Z280 has a much nicer arrangement for the invalid opcode vector.

Thanks,

Wayne

feilipu commented 3 years ago

Hmm. "it's complicated", to say the least. As you write, it doesn't look like there's an easy and good solution.

Alvin, who wrote the solution for z88dk, took the approach of leaving it to the user to sort out, and only prescribing the minimum. For z88dk it is relatively easy as the solution is only available in ROM builds, where logical and physical 0x0000 are the same.

I would take the approach of simply flagging the error in the RomWBW, but allow the OS to do more (with a redefinable call) if it was equipped to do so.

By that I mean there would need to be a small handler in HBIOS high memory that set logical addressing to reset state, and jumped to 0x0000 again. In ROM there would be a a further trap handler that if it found a trap would pop the original RST0 return address and provide a message that an illegal Opcode was found, and return to the dbgmon. Otherwise the ROM trap handler would just do a normal reset and return to dbgmon.

Then each OS has to do only the minimum with the trap like below, and it can be relatively easy to maintain. Or, it can be as complicated as desired at the OS level.

This example code is also at 0x00000 location in ROM.

ORG 0x0000
    JP    __TEST_TRAP ; with __TEST_TRAP replacing the normal RST0 vector
__TEST_TRAP_RETURN:
; For CP/M this would have to be somewhere else, as the I/O bytes etc are here.
; Perhaps after `0x003B` or in the BIOS scratch area at `0x0040`?
    POP AF
    JP    _OS_RST0_LOCATION ; with still enough space for RST8 following

then in HBIOS Proxy

__TEST_TRAP:
   push af
   ld a,__IO_BASE_ADDRESS
   out0 (0x3f),a        ; out0 (ICR),a
   in0 a,(ITC)
   rla
   jp NC,__TEST_TRAP_RETURN    ; there was no trap, and the return address could be a fixed location, `0x003B` for example.
   xor a
   out0 (CBR),a    ; probably just CBAR needs to be set to 0xFFFF here, but to be sure...
   out0 (BBR),a
   dec a
   out0 (CBAR),a
   jp 0x0000    ; the ROM Trap Test will now trigger because the trap bit has not been reset.
wwarthen commented 3 years ago

Yes, I think I agree that it is preferable to have RomWBW get control first and announce the problem if it is an actual opcode error.

I think the existing interrupt handling framework that already exists in the proxy can be leveraged to make this easier and smaller.

wwarthen commented 3 years ago

As it currently stands, there are only 4 bytes of space available for new code in the proxy.

However, by leveraging your idea of using the page zero $40 scratch area, perhaps this would work:

    ORG 0
    JP  $40
    ...
    ORG $40
    HB_DI               ; MAYBE???? [1]
    POP HL          ; CAPTURE PC OF OFFENDING OPCODE [1]
    LD  BC,$####        ; SETUP HBIOS API CALL [3]
    CALL    HBX_INVOKE      ; DO IT [3], HL := USER RESET VECTOR
    HB_EI               ; MAYBE???? [1]
    JP  (HL)            ; GO TO USER RESET [1]

All of the real work can be done inside the HBIOS API call. It will check for the bad opcode trap flag and diagnose it if so. A 2 byte reserved location in the proxy would hold the "user reset vector". If it has been set, then the API call will return with HL set to the user's vector. If not, the system can be halted or cold started.

The OSes will need to be updated to set the user reset vector in the proxy instead of $0001. If an OS or user code does take over the $0001 vector, then no harm is really done -- it is just like it is now.

Thoughts?

feilipu commented 3 years ago

That looks good, and putting it in $0040 would be OK. I think that from $003B is reserved anyway, so you could actually start there too.

For z88dk the assumption is that the user may have purposefully issued an invalid opcode, and therefore the af register is pushed and other registers are preserved on entry so that the registers are passed to the "z80" emulation or handler or whatever. For RomWBW I don't think we're working to that constraint. So popping the PC+1(2) into HL is OK, as long as the ITC register bits for Trap and UFO are read from ITC and handled within the HBIOS API trap call.

The HBIOS trap handler needs to differentiate firstly, trap or no trap, and if no trap go to normal RST0 location. Then it differentiates on 2nd or 3rd opcode by subtracting 1 or 2 from the popped PC depending on UFO. Then it could print a smart error message with the two or three bytes containing the offending opcode. All of this may get too much for the space free in Page 0 and, if the Proxy is full, then we might be limited in what can be done.

wwarthen commented 3 years ago

I'll take a whack at this today. Thanks for your assistance Phillip.

wwarthen commented 3 years ago

Arrrgh!!! I forgot that many CP/M programs use the address stored at 0001H as a way to locate the CBIOS jump table. This completely blocks the ability of RomWBW HBIOS to intercept the trap.

I see no choice but to add a hook in the OSes. I am going to try to just have them make an HBIOS API call that checks for the trap and returns.

Any other thoughts?

wwarthen commented 3 years ago

OK, this worked out better than I expected.

If an OS usurps the JP instruction at logical address 0000H, then is is expected to make the HBIOS call that handles bad opcodes.

If no OS is loaded (ROM app), then a default JP instruction is placed at $0000 which handles bad opcodes.

This seems to be working well and is fairly robust. I will be checking this in shortly and would appreciate any further thoughts.

Thanks,

Wayne

wwarthen commented 3 years ago

Oh, here is an example of what you should see on a Z180 if an invalid opcode is encountered:

22:35 A0:SYSTEM>tdlbasic

Highest Memory?

+++ BAD OPCODE @27CCH: DD 4C DD 7D DD 67 DD 6A

22:36 A0:SYSTEM>
feilipu commented 3 years ago

Just a thought, if there's enough space... +++ UNDOCUMENTED OPCODE might be more informative? Added a few inline comments on the commit.

But otherwise it looks done. Welcome to close the issue. :+1:

wwarthen commented 3 years ago

Space is not a significant issue inside the main HBIOS bank, so no problem with improving the message. I agree that BAD OPCODE is a bit nebulous since it could be a perfectly valid Z80 opcode. However, it could be complete garbage as well.

How about +++ INVALID Z180 OPCODE?

Thanks for the code comments. I will make those changes.

feilipu commented 3 years ago

+++ INVALID Z180 OPCODE works for me.

wwarthen commented 3 years ago

I have checked in the latest changes suggested, so will close this thread.

Thanks Phillip!

feilipu commented 3 years ago

Just some small things to fix. The RES 7,A can be removed, now we've done the XOR. https://github.com/wwarthen/RomWBW/blob/953dd700520f6a983e083f1284394038db1d3785/Source/HBIOS/hbios.asm#L2979

And the pedant says the comment context means, "(3 byte opcode)". https://github.com/wwarthen/RomWBW/blob/953dd700520f6a983e083f1284394038db1d3785/Source/HBIOS/hbios.asm#L2974

wwarthen commented 3 years ago

Oops, yes. Thanks.