-
Notifications
You must be signed in to change notification settings - Fork 295
ROM Analysis in Basilisk II Emulation
- Table of Contents
- Assumptions
- Disassemble original 68k Mac ROM
- Mac ROM in real Macintosh hardware
- Mac ROM in Basilisk II emulation
- Bibliography
The analysis below is based on the study of the Performa old world 32bit clean ROM (with MD5 hash af343f3f1362bf29cefd630687efaa25
). I found that this Performa 630 ROM works great for System 7 and Mac OS 8.1.
Assuming you get a Mac ROM binary file for BII from somewhere, the next step is to poke around it. A ROM file is a collection of data and M68K machine code which performs hardware test at boot time, provides low level common routines such A-Trap and etc.
For emulation purposes, BII patches the original ROM so that it can bypass hardware test and complete overwrite Macintosh hardware drive with new emulated driver. I will demonstrate disassembling this Mac ROM with cxmon and radare2.
$ cd macemu/cxmon
$ ./bootstrap
$ ./configure
$ make
$ sudo make install
The command below disassemble ROM file and dump the text into an external file.
cxmon -m ‘[ 0 “PERFORMA.ROM”’ ‘d68 0 fffff’ > PERFORMA.ROM.DISAM
You can also run cxmon in standalone interactive mode or even enable --with-mon
option in BII build to debug at runtime. Please refer to cxmon help
NOTE:
After reading the disassemble code, cxmon didn't disassemble correctly in some cases. This is due to the fact that ROM mixed with data and code. For example, read ROM starting from 0x2a offset. The machine code 0x46fc2700 should be at address $0000008c. The correct assembly should be move #?2700, sr
, instead of ori.b #$fc,d0
and move.l d0,-(a3)
.
11 000000000000002a: 4efa 0060 jmp ($0000008c,pc)
...
37 000000000000008a: 0000 46fc ori.b #$fc,d0
38 000000000000008e: 2700 move.l d0,-(a3)
39 0000000000000090: 4dfa 000a lea ($0000009c,pc),a6
Build radare2 from git repo. You also need to build acr.
The documentation of radare2 is long. TLDR; Here is an example of using it:
[Ricky@xps ROM.Disas]$ radare2 PERFORMA.ROM
-- Switch between print modes using the 'p' and 'P' keys in visual mode
[0x00000000]> e asm.arch=m68k
[0x00000000]> pd 10 arch=m68k
0x00000000 066842140000 addi.w 0x4214, 0x0(a0)
0x00000006 002a067c4efa ori.b 0x7c, 0x4efa(a2)
| 0x0000000c 00804efa007c ori.l 0x4efa007c, d0
| 0x00000012 32f10100 move.w (a1, d0.w), (a1)+
| 0x00000016 00000044 ori.b 0x44, d0
| 0x0000001a 0007ec10 ori.b 0x10, d7
| 0x0000001e 4efa1220 jmp 0x1220(pc)
| 0x00000022 000d2da0 invalid
| 0x00000026 4efa22e8 jmp 0x22e8(pc)
| 0x0000002a 4efa0060 jmp 0x60(pc)
NOTE: radare2 has the same issue as cxmon. This comes to the dead end. To read ROM disassemble code, you need to tag code segment manually. [Update Aug 25, 2017] In MPW GM, ROMMap folder provides a long list of Macintosh ROMs that segment A-Trap. However, I can't find Performa 640 ROM mapping there.
In Apple Mac IIci and Mac Quadra 900 Developer Note, it specified that ROM must be in a fixed address space. Because ROM machine code is written as position independent code. In reality, its location in memory is relocatable. That’s why BII can load ROM to whatever memory address in guest OS without breaking its logic.
So far I haven’t found an automatic way to disassemble Performa ROM without human tagging code segment. I can’t easily get a full picture of what Performa ROM contains. But reading BII ROM patches, Macintosh ToolBox trap and Macintosh OS trap, I think I need to have basic understanding of using illegal instruction exception technique in M68k CPU.
M68k CPU suspends execution instruction flow if it encounters an illegal instruction. An illegal instruction is an instruction that contains any bit pattern in its first word that does
not correspond to the bit pattern of the first word of a valid M68k instruction or is a
MOVEC
instruction with an undefined register specification field in the first extension word.
When an illegal instruction exception happens, there are four steps in processing exception [1]:
- The processor makes an internal copy of the status register. Then it sets the S bit, changing to the supervisor privilege level. Next, it inhibits tracing of the exception handler by clearing the T1 and T0 bits.
- The processor determines the vector number of the exception.
- The processor saves the current processor context and creates an exception stack frame on the active supervisor stack and fills it with context information appropriate for the type of exception.
- The processor multiples the vector number of the exception by 4 to get the offset of vector table. Vector Base Register
VBR
points to the address of vector table. So add the offset withVBR
to get the address of the exception handler routine. Then, load Program CounterPC
with the look-up address and resume CPU execution.
The following is exception vector number table:
Macintosh OS reserves the first word 1010
(0xA) unimplemented instruction to implement so called A-Trap to provide ToolBox and OS API. Its vector number is 0xA
Basilisk II also uses undefined instruction with prefix 0x71xx
to patch Mac ROM and implement its emulated drivers. However, the implementation of illegal 0x71xx
instruction in BII bypasses M68k CPU illegal instruction exception. We will discuss this in next section.
BII loads the unmodified ROM file from disk into memory at RomBaseHost address. Depending on addressing mode, ROM location (i.e. ROMBaseMac) varies from Macintosh guest OS point of view.
0067 bool Init680x0(void)
0068 {
0069 #if REAL_ADDRESSING
0070 // Mac address space = host address space
0071 RAMBaseMac = (uintptr)RAMBaseHost;
0072 ROMBaseMac = (uintptr)ROMBaseHost;
0073 #elif DIRECT_ADDRESSING
0074 // Mac address space = host address space minus constant offset (MEMBaseDiff)
0075 // NOTE: MEMBaseDiff is set up in main_unix.cpp/main()
0076 RAMBaseMac = 0;
0077 ROMBaseMac = Host2MacAddr(ROMBaseHost);
0078 #else
0079 // Initialize UAE memory banks
0080 RAMBaseMac = 0;
0081 switch (ROMVersion) {
0082 case ROM_VERSION_64K:
0083 case ROM_VERSION_PLUS:
0084 case ROM_VERSION_CLASSIC:
0085 ROMBaseMac = 0x00400000;
0086 break;
0087 case ROM_VERSION_II:
0088 ROMBaseMac = 0x00a00000;
0089 break;
0090 case ROM_VERSION_32:
0091 ROMBaseMac = 0x40800000;
0092 break;
0093 default:
0094 return false;
0095 }
0096 memory_init();
0097 #endif
...
As pointed out above, ROM is written in position independent manner. Thus, the starting address of ROM is relocatable in memory. You may wonder how guest OS knows the location of ROM at runtime. After ROM startup code perform memory test, it initializes a list of global variables in the fixed memory address. You can find the whole list of global variables and its memory location from Part 1. Global Variables in Memory Address Order .
To verify this, I did an experiment in BII. From the list, we know that global variable ROMBase is in address 0x2AE
. So trigger a segfault to make BII runs into cxmon. We will poke around the guest OS memory from there.
First, we shows the content at address 0x2AE
.
[00000000ff3f32ff]-> m 02ae
00000000000002ae: 3ff00000 00002000 000031a0 0008cbe0 '?..... ...1.....'
Because I runs in Intel little endian CPU. 0x3ff00000
should be 0x3ff00000
in big endian. Here is the patched ROM in memory with respect to guest OS address space.
[0000000f03f00100]-> m 3ff00000
000000003ff00000: 06684214 0000002a 067c4efa 00804efa '.hB....*.|N...N.'
000000003ff00010: 007c32f1 01000000 00440007 ec104efa '.|2......D....N.'
000000003ff00020: 1220000d 2da04efa 22e84efa 00600000 '. ..-.N.".N..`..'
000000003ff00030: 0101ede8 01251b34 01023267 01214c0a '.....%.4..2g.!L.'
Now, compare above with the PERFORMA original ROM file.
$ hexdump PERFORMA.ROM | head -n 4
0000000 6806 1442 0000 2a00 7c06 fa4e 8000 fa4e
0000010 7c00 f132 0001 0000 4400 0700 10ec fa4e
0000020 2012 0d00 a02d fa4e e822 fa4e 6000 0000
0000030 0101 e8ed 2501 341b 0201 6732 2101 0a4c
Again, because hexdump respects Intel platform little endianness. After do some word swapping, it matches the exact ROM contents from cxmon.
After BII loads ROM from file into memory, it patches ROM in PatchRom()
function from file src/rom_patches.cpp
.
Patching Macintosh ROM is a daunting task:
- You need to know M68K assembly and how to write Macintosh driver.
- You need to find the right place to patch. This requires you have some basic understanding of ROM logic.
- Because the size of ROM is fixed, there is very limited room left to extend new functionality.
To tackle the first obstacle, you need to read a lot of documentation. Motorola 68K CPU programming reference, Inside Macintosh Volume I to VI, Apple technical note from archive.org are helpful resources.
For the second one, you may get help from annotated disassembled ROM in Mini vMac tool FDisasm. So far there are only 64K, 128K and 256K ROM formatting information available. But it is far better than reading ROM by cxmon or radare2. It helped me find the right spot to install emulated hard drive driver in 24-bit ROM.
The last puzzle is solved by the technique of adding undefined M68 instruction 0x71xx
in BII emulator. BII emulator patches ROM driver with a set of new instructions. It replaces original driver installation step with BII internal 0x71xx
instruction trap. Like Apple A-trap, those undefined 0x71xx
instruction calling the core driver logic, which is implemented in C++ inside BII. In this way, it can easily squeeze new functionality in the limited ROM and also implement them in the high level language.
For technical details, this commit shows a simple example of adding a new instruction that suspends BII CPU and traps into cxmon.
Emulation starts at relative address 0x2a
i.e (MacROMBaseMac + 0x2a) in the patched ROM. TODO add more