• Hello MLAers! We've re-enabled auto-approval for accounts. If you are still waiting on account approval, please check this thread for more information.

Classic II possible ROM bug, weird 68030 instruction

dougg3

68000
Hey everyone,

I'm not sure exactly where to post this, so I'll put it in the Compact Mac forum. I've been talking with @Arbee about a weird issue I found in MAME when emulating the Classic II, and thought I should share it with the whole group. BTW, if you're not familiar with MAME, you really should check it out. It's getting more and more complete to the point that almost all 68k Macs can be emulated. You may think of arcade games when you think of MAME, but it has really turned into an excellent Mac emulator too, thanks to Arbee!

Anyway, the original problem I found is that an emulated Mac Classic II (macclas2) doesn't boot up in MAME if you have the computer set for 32-bit addressing in the Memory control panel. It displays a Sad Mac with the codes 0000000F and 00000001. I don't have a Classic II myself, but I know for sure that this problem doesn't happen on actual hardware. Trying to track this down has led me down a huge rabbit hole.

I've been disassembling the Classic II's ROM with IDA and playing in MAME's amazing debugger to try to figure out what's going on, and tracked down the problem to this section of the ROM code:

Code:
ROM:40A43B4A V8SNDINTPATCH1RTN:
ROM:40A43B4A                 moveq   #0,d0
ROM:40A43B4C                 move.b  ($CB3).w,d0
ROM:40A43B50                 bpl.s   loc_40A43B54
ROM:40A43B52                 rts
ROM:40A43B54
ROM:40A43B54 loc_40A43B54:
ROM:40A43B54                 movea.l ($2B6).w,a0
ROM:40A43B58                 movea.l $110(a0),a0
ROM:40A43B5C                 clr.l   $22(a0)
ROM:40A43B60                 clr.l   $1E(a0)
ROM:40A43B64                 lea     loc_40A43BD8,a0
ROM:40A43B68                 move.l  a0,($D80).w
ROM:40A43B6C                 add.w   d0,d0
ROM:40A43B6E                 jmp     loc_40A43B72(pc,d0.w)
ROM:40A43B72
ROM:40A43B72 loc_40A43B72:
ROM:40A43B72                 bra.s   loc_40A43B92    ; case 0
ROM:40A43B74                 bra.s   loc_40A43B92    ; case 1
ROM:40A43B76                 bra.s   loc_40A43B92    ; case 2
ROM:40A43B78                 bra.s   loc_40A43B92    ; case 3
ROM:40A43B7A                 bra.s   locret_40A43BA4 ; case 4
ROM:40A43B7C                 bra.s   loc_40A43BA6    ; case 5
ROM:40A43B7E                 bra.s   loc_40A43B92    ; case 6
ROM:40A43B80                 bra.s   loc_40A43BB2    ; case 7
ROM:40A43B82                 bra.s   loc_40A43BA6    ; case 8
ROM:40A43B84                 bra.s   loc_40A43BA6    ; case 9
ROM:40A43B86                 bra.s   loc_40A43BA6    ; case 10
ROM:40A43B88                 bra.s   loc_40A43BA6    ; case 11
ROM:40A43B8A                 bra.s   loc_40A43BA6    ; case 12
ROM:40A43B8C                 bra.s   locret_40A43BA4 ; case 13
ROM:40A43B8E                 bra.s   loc_40A43BA6    ; case 14
ROM:40A43B90                 bra.s   loc_40A43BA6    ; case 15
ROM:40A43B92
ROM:40A43B92 loc_40A43B92:
ROM:40A43B92                 movea.l ($CEC).w,a1
ROM:40A43B96                 bclr    #4,$1800(a1)
ROM:40A43B9C                 move.b  #$90,$1C00(a1)
ROM:40A43BA2                 rts
ROM:40A43BA4
ROM:40A43BA4 locret_40A43BA4:
ROM:40A43BA4                 rts
ROM:40A43BA6
ROM:40A43BA6 loc_40A43BA6:
ROM:40A43BA6                 movea.l ($CEC).w,a0
ROM:40A43BAA                 move.b  #$90,$13(a0)
ROM:40A43BB0                 rts
ROM:40A43BB2
ROM:40A43BB2 loc_40A43BB2:
ROM:40A43BB2                 movea.l ($CEC).w,a0
ROM:40A43BB6                 move.b  #2,8(a0)
ROM:40A43BBC                 rts

The purpose of this function is to configure the VIA2 (address of it is stored at $CEC) so that the sound interrupt is enabled. There's a jump table that selects the correct code to run on different Mac models. I haven't found anything in the ROM that would indicate that this function would be skipped. Although I don't have a Classic II that I can use to confirm this for certain, I am pretty confident that this code runs on actual hardware.

What's happening is when this routine runs at startup, it loads a byte from 0xCB3 into D0. The value it loads is 0x11 = 17. This byte is an identifier of which computer it is. The value of 17 is correct for the Classic II. It ends up being doubled so it can be used as an offset into the jump table, but as you can see, it's past the end of the table. So when it runs, it ends up jumping to 0x40A43B94, which is inside the movea.l ($CEC).w,a1 instruction.

This causes MAME's debugger to display the following instructions as the next few instructions:

Code:
 40A43B94  cas.w   D1, D0, ($4,A4); (2+)                       0CEC 08A9 0004
 40A43B9A  move.b  D0, D4                                      1800
 40A43B9C  move.b  #$90, ($1c00,A1)                            137C 0090 1C00

So basically it gets out of sync until 0x40A43B9C. When that last instruction move.b #$90, ($1c00,A1) finally runs, that is what causes the bus error because A1 was left at its existing value of 0xFFFF8FBA, and that section of the address space isn't mapped to anything in the MMU.

What I've discovered is that this CAS.W instruction is not quite what MAME thinks it is. To be honest, I have no idea precisely what it does. It doesn't fit the pattern of any valid CAS instruction. Apple's MacsBug debugger interprets it slightly differently:

Code:
CAS.W      D1,D2,$0004(A4)        | 0CEC 08A9 0004

IDA and objdump refuse to disassemble it at all.

I've determined experimentally by playing around with different 68030 machines on actual hardware (Mac IIci, LC II) that this instruction somehow ends up modifying A1. It doesn't touch any of the data registers, and it doesn't touch any of the address registers except A1. I haven't been able to figure out exactly what the operation is that it does, but what I can tell is that A1's existing value, A7, and PC all factor into what A1's final value becomes. It's something along the lines of A1 = (A1 | PC) & A7 | 0x80, but I think the 0x80 might be changing based on something else (memory contents somewhere? not sure?).

If I had an actual Classic II, I would do some ROM hacking to prove that this is what's also happening on hardware, but unfortunately I don't have one. However, since I've observed on multiple 68030-based Macs in MacsBug that this instruction changes the value of A1, it's plausible that the Classic II's ROM had a bug with this out-of-bounds jump table access and this instruction was unknowingly saving the day for Apple by putting a valid value into A1 so that the move.b #$90, ($1c00,A1) instruction wouldn't crash.

Anyone have a Classic II, while also being comfortable with ROM hacking? It would be awesome if we could somehow confirm that this is what's actually happening on hardware. If we hacked the ROM code to do something like wait forever instead of executing the move.b #$90, ($1c00,A1) instruction, we could prove that this code is actually executing on hardware. If that proves to be successful, I would dole out bonus points to someone who could perform some sort of hack to the ROM code that allows determining what A1 is actually being loaded with after that instruction.
 
Even if you're not comfortable doing any ROM hacking, it's also possible to install MacsBug and run some commands inside it to gather some knowledge.
 
Hey look, they fixed this problem in the IIvx/IIvi ROM. It has the exact same function (ROM offset $40800000 instead of $40A00000 though), but the jump table is much bigger now. So big, in fact, that I got lazy and stopped figuring out how many cases there were. All the rest of the added cases just do the same thing as cases 13 and onward -- in other words, nothing.

Code:
ROM:40843B4A V8SNDINTPATCH1RTN:
ROM:40843B4A                 moveq   #0,d0
ROM:40843B4C                 move.b  (byte_CB3).w,d0
ROM:40843B50                 bpl.s   loc_40843B54
ROM:40843B52                 rts
ROM:40843B54
ROM:40843B54 loc_40843B54:
ROM:40843B54                 movea.l (dword_2B6).w,a0
ROM:40843B58                 movea.l $110(a0),a0
ROM:40843B5C                 clr.l   $22(a0)
ROM:40843B60                 clr.l   $1E(a0)
ROM:40843B64                 lea     loc_40843C1C,a0
ROM:40843B68                 move.l  a0,(dword_D80).w
ROM:40843B6C                 add.w   d0,d0
ROM:40843B6E                 jmp     loc_40843B72(pc,d0.w)
ROM:40843B72
ROM:40843B72 loc_40843B72:
ROM:40843B72                 bra.s   loc_40843BD6    ; case 0
ROM:40843B74                 bra.s   loc_40843BD6    ; case 1
ROM:40843B76                 bra.s   loc_40843BD6    ; case 2
ROM:40843B78                 bra.s   loc_40843BD6    ; case 3
ROM:40843B7A                 bra.s   locret_40843BE8 ; case 4
ROM:40843B7C                 bra.s   loc_40843BEA    ; case 5
ROM:40843B7E                 bra.s   loc_40843BD6    ; case 6
ROM:40843B80                 bra.s   loc_40843BF6    ; case 7
ROM:40843B82                 bra.s   loc_40843BEA    ; case 8
ROM:40843B84                 bra.s   loc_40843BEA    ; case 9
ROM:40843B86                 bra.s   loc_40843BEA    ; case 10
ROM:40843B88                 bra.s   locret_40843BE8 ; case 11
ROM:40843B8A                 bra.s   loc_40843BEA    ; case 12
ROM:40843B8C                 bra.s   locret_40843BE8 ; case 13
ROM:40843B8E                 bra.s   locret_40843BE8 ; case 14
ROM:40843B90                 bra.s   locret_40843BE8 ; case 15
ROM:40843B92                 bra.s   locret_40843BE8 ; case 16
ROM:40843B94                 bra.s   locret_40843BE8 ; case 17
ROM:40843B96                 bra.s   locret_40843BE8 ; case 18
...
 
If you haven't heard from anyone else in a week and a half, let me know and I may be able to help. Not sure I have any of the right EPROMS in stock but that can be sorted out...
 
If you haven't heard from anyone else in a week and a half, let me know and I may be able to help. Not sure I have any of the right EPROMS in stock but that can be sorted out...

Thank you, I will keep that in mind! Even if you can't do EPROMs, just the output of some random commands I provide in MacsBug would be very informative.
 
You just need someone with a real 68030 to test the CAS instruction in Macsbug? It doesn't matter if it is a Classic II or LC II, right?

I see you already did that.
 
You just need someone with a real 68030 to test the CAS instruction in Macsbug? It doesn't matter if it is a Classic II or LC II, right?

That's what I've been doing -- I tested the bad CAS instruction in MacsBug in both my LC II and my IIci. The more the merrier though if you want to test the CAS instruction and see if you can reproduce what I'm seeing! If you just put the following bytes in memory somewhere and set your PC to point at them, see if you can reproduce what I'm seeing (it modifies the value of A1 and nothing else) after stepping into the code:

Code:
0C EC 08 A9 00 04 18 00 13 7C 00 90 1C 00

I put a few extra instructions after it since I'm not sure exactly how many bytes the 68030 is interpreting when it does it.

I think what I'm hoping to gain from testing in the actual Classic II is finding out what the value of A1 ends up being when executed with those conditions in the Classic II. I saw inconsistencies between my test in the IIci and the LC II with the same register values (the A1 value is different) which makes me think it may also involve a memory access at some level. If it depends on RAM it may be impossible to know exactly what the register is supposed to end up as.

It would also be nice to get proof that the ROM is actually executing the buggy code, but that would require someone to put in a hacked ROM that is able to somehow indicate when that instruction is reached.

I mean, I just set up my Classic II in my garage, so if you need anything run...

Ooh, thanks! I would greatly appreciate it if you would be so kind as to test this out. @Arbee might have a few additional ideas, but here's one idea: First, install MacsBug and boot in 32-bit mode. I use MacsBug 6.5.3 which I obtained from Macintosh Garden.

After it has finished booting, press command-power. MacsBug should pop up and take over the entire screen. This might be annoying to type this all in, so I won't be offended if you don't want to do it! Anyway, type the following commands to set up an environment just like what the ROM is running in, pressing return after each line:

Code:
D0=22
D1=55B4
D2=40A16500
D3=FFFFFFF6
D4=800000
D5=0
D6=FFFFC
D7=30011
A0=40A43BD8
A1=FFFF8FBA
A2=FFFF73D4
A3=40A16ABA
A4=40A09AE6
A5=FC7F0
A6=FCA60
A7=FC63C
PC=40A43B94

Then, type the letter s and press return. On the bottom half of the left side of the screen it should show all the registers. Can you list out what has changed from the values you typed in after that? (Leading zeros are OK, so for example if D0 is 00000022 it hasn't changed). I would expect that only A1 changes and it ends up with a new value that is not FFFF8FBA.

I just realized that the value of A5-A7 is going to vary based on how much RAM is installed, but I suppose let's start with that.

When you're done, it's probably best to just power off the machine at that point.

Thanks!
 
Are you sure the Classic II has the ASC enabled in the gestalt? It doesn't have a full ASC, so if the gestalt doesn't have the ASC bit set then that code is unreachable.
 
Are you sure the Classic II has the ASC enabled in the gestalt? It doesn't have a full ASC, so if the gestalt doesn't have the ASC bit set then that code is unreachable.

Yep, I spent way too much time tracing my way through how it all works and comparing it against the SuperMario sources. I really wanted to find a simpler answer than what I've concluded above :D. Here are the relevant table entries from the universal tables in the Classic II ROM:

1735520978699.png

1735520849167.png

The important thing is that the ASCExists bit is set in the flags in infoClassicII. The code that handles the 'hdwr' gestalt ends up looping through another table and converting these bits into its own gestaltHasXXX bits:

1735521200423.png

The end result is that because the Classic II's table entry has bit 12 set (ASCExists), the result of the 'hdwr' gestalt will have gestaltHasASC (bit 3) set, so the code at V8SNDINTPATCH1 decides to jump to V8SNDINTPATCH1RTN.
 
Well I tested it just now and it seems to pass that code without issue on real hardware at the very least.

Thanks! It looks like you've confirmed that what we see on MAME really happens on hardware too with how it loaded 0x11 (becoming 0x22) into D0 and jumped out of bounds in the table to that incorrect CAS.W instruction.

When I step through the ROM in MAME, A1 ends up with the value FFFF8FBA right before this code executes. That value is originally loaded into A1 at 40A4AB86 while it's performing a jump. I believe if you start with A1 set to FFFF8FBA, hardware will change A1 to something in the RAM range instead when it executes that bad instruction. Are you seeing that too?

Thank you for testing this!
 
Well I tested it just now and it seems to pass that code without issue on real hardware at the very least.

>> Anyway, the original problem I found is that an emulated Mac Classic II (macclas2) doesn't boot up in MAME if you have the computer set for 32-bit addressing in the Memory control panel.

@SuperSVGA does it crash in 32-bit mode? It looks like you are in 24-bit mode based on the MacsBug screenshot.
 
Yes, it looks like right when it is running the CAS instruction is where it is changing A1. I think the problem is some sort of either undefined or undocumented behavior because the second word of the CAS instruction is invalid.

Screenshot 2024-12-29 at 9.37.49 PM.png

The first word is fine. The size is word, addressing mode is (d16,An), and register is A4.
The second word has bit 7 set for D2, and bit 0 set for D1. Bits 11, 5, and 3 should not be set but they are in this case. This seems to put something into A1 (for example if the PC is 0x00180000 then A1 will change to 0x00080000, and PC at 0x00190000 will change A1 to 0x00090000). If you clear bit 3 from the second word of the instruction this stops happening.
 
It looks like you are in 24-bit mode based on the MacsBug screenshot.

That's a great point. I think for the purposes of this code it won't matter because the weird behavior of the invalid instruction will likely put a good RAM address into A1 which will succeed in both 24-bit and 32-bit modes, but it would still be good to test in 32-bit mode as well for completeness.

Yes, it looks like right when it is running the CAS instruction is where it is changing A1. I think the problem is some sort of either undefined or undocumented behavior because the second word of the CAS instruction is invalid.

Exactly. IDA and objdump were probably correct in their refusal to disassemble it. It's an invalid instruction, which curiously doesn't cause the 68030 to report an illegal instruction.

This seems to put something into A1 (for example if the PC is 0x00180000 then A1 will change to 0x00080000, and PC at 0x00190000 will change A1 to 0x00090000).

This is what I observed too. Messing with A7 also seems to change the resulting A1 value. The initial value of A1 also factors into the equation.

This odd instruction, which certainly wasn't even intended to be jumped to in the first place because it results from an out-of-bounds table jump and starts in the middle of an actual legitimate instruction, is what is saving the Classic II from showing a Sad Mac at startup, at least in 32-bit mode, because it sets A1 to a valid address just before the "MOVE.B #$90, $1C00(A1)" instruction.
 
I just thought of another test I could try with my IIci, since I have all kinds of programmable ROM SIMMs. I programmed a ROM SIMM so that the Classic II ROM is after the IIci ROM, mapped at 0x40A00000 just like it would be on hardware on the Classic II, so I can jump to the exact same address that would be executing on the Classic II.

Just before executing the instruction, setting up MacsBug on my IIci exactly as I instructed above:

1735575721798.png

After the instruction (pressing s):

1735575746602.png

This also shows that A1 transformed into a valid RAM address. It fits the equation I came up with earlier, but from further testing I think the real equation might be something more like A1 = (A1 | PC) & (A7 | 0x80), because bit 7 isn't always set in the resulting A1 when I tinker with different register values.

Oh, I just realized I can run the same test using the actual 0x40A43B94 program counter on my LC II as well, because the exact same code from the Classic II is already in its ROM at the same location, so I'll do that too.

LC II Before:

1735575824513.png

LC II After:

1735575840113.png

Interestingly enough, I get a slightly different A1 result on the LC II (FA6BE versus F86BC) even though all of the register values were identical, including the program counter. I'm guessing this means that it also depends on the content of memory somewhere. Either that or the behavior of the undocumented/undefined instruction isn't 100% repeatable across different CPUs.
 
I'd think the likely suspect for the contents of memory that affects it would be 4(A4).

Also, a thought: CAS and TAS in their legit forms are the only 680x0 instructions that do a special read-modify-write bus cycle so that the operation is not splittable in the middle on a multiprocessor system. Some well-known 68K hardware including the Sega Megadrive/Genesis and some Amigas don't support those cycles and either lock up or do other odd things.
 
Back
Top