If you’ve been reading my blog for a while, you might remember back in 2012 when I changed the startup sound on my Power Mac G3 (Blue and White). That was a fun introduction to the Forth programming language. I had to reverse-engineer just enough of Apple’s firmware update script to understand what was going on.

Recently, Aidan Halpin, a reader of this site, asked me if I could do the same kind of startup sound customization on his iMac. This particular iMac is officially known as the “iMac (Slot Loading)” and has a model identifier of PowerMac2,1. As you can guess from the name, it has a slot-loading CD-ROM drive unlike the original iMac that had a laptop-style tray-loading drive. By the way, Aidan’s iMac is special because it has a PowerPC G4 processor soldered onto the logic board instead of the original G3.

Photo courtesy of Aidan Halpin

He sent me Apple’s last firmware update for this model: iMac Firmware Update 4.1.9. I went to work looking at the update contents to see if I could figure out how to modify the chime the same way I did with my Power Mac G3. I thought it would be fun to take everyone along for a ride and show exactly what was involved in changing the sound. And of course, this post wouldn’t be complete without also sharing the code for the utility I created to inject the new chime into the firmware update file.

Before we get started, I need to provide some background info from my 2012 work on customizing the startup sound in my Power Mac G3 because a good portion of it was relevant for the iMac too. When I dumped my G3’s ROM, I imported it into Audacity as a raw file. I chose signed 16-bit PCM, big-endian, 44,100 Hz as the format. Then I played the entire 1 MB ROM as a big sound. It mostly sounded like a scratchy mess, but there were two locations that both obviously contained the startup sound data. It sounded very staticky and played way too quickly, but it was clearly the startup chime. Headphone/earbud users beware, it’s pretty loud:

I could get it closer to the correct length and pitch by lowering the sample rate, but it still sounded scratchy. See the section depicted below from about 2.75 seconds to 3.5 seconds or so for an example of what it looked like in Audacity.

At the time, I had been doing a lot of digging into sound formats for other Mac ROM investigations, so some of the common sound compression algorithms were already fresh in my head. I quickly stumbled upon a couple of IMA ADPCM step tables in the ROM. Common sense suggested that Apple probably would have used their own QuickTime IMA ADPCM format, which groups the data into 34-byte packets. That knowledge allowed me to figure out exactly where the sound data began and ended in the ROM, and I was able to decode it.

So when Aidan asked if I could look into changing the sound on the iMac, I took the compressed sound data I had found 10 years earlier in my Power Mac’s ROM and searched for the same chunk of data in the iMac’s firmware update file. Sure enough, the sound was the exact same compressed sound, right down to every byte. In this update file, there is only one copy of the startup sound. It’s located here:

However, it wasn’t as easy as just doing the exact same patch to the firmware update file. Although the iMac’s firmware update file is a Forth script just like the Power Mac G3’s, it’s arranged differently. With that in mind, I pulled up some Forth tutorials and faked my way through understanding the code. What follows is my analysis of the firmware update file and the process I followed to modify the sound.

The Forth code starts out by defining a bunch of structures. This one is interesting because it refers to a boot beep and corresponding size:

More on that later. Further down in the code, there are some some section-related structures. It creates space for the overall header and 6 section headers: sbb, srec, sboot, ssys, stst, and snv. Each section has a checksum. This update script is super useful! It tells me the meaning of a lot of the data in the update file.

I’m not going to go through the Forth code that actually uses these structures and sets everything up — partially because it’s kind of long, and partially because I don’t fully understand everything it does. The important part is that after the end of the script (doit followed by a carriage return), a bunch of raw non-ASCII data immediately follows. The first chunk of this data turns out to be the content of the >h and >s structs described above.

Using the struct definitions, you can decode all of the highlighted data above (keep in mind that at least on Macs, PowerPC is big-endian):

  • Header:
    • file-size = 0x000E11C0 = 922048 (this is the exact size of the firmware file)
    • rom-size = 0x0010 = 16
    • header-size = 0x00A4 (so the header ends at 0x68DC)
    • build-version = 0x000419F1 = 4.1.9f1
    • build-date = 0x20010914 = September 14, 2001
    • model = 0xFFFF
    • fill-byte = 0xFF
    • num-sections = 6
  • sbb:
    • type = 0x00
    • flags = 0x81
    • reserved = 0x0000
    • position = 0x000068DC (immediately after the header)
    • offset = 0x00000000
    • size = 0x00003F00
    • actual = 0x00003A00
    • checksum = 0x43B8671B
  • srec:
    • type = 0x01
    • flags = 0x81
    • reserved = 0x0000
    • position = 0x0000A2DC (immediately after the sbb data)
    • offset = 0x00008000
    • size = 0x00078000
    • actual = 0x00063DA0
    • checksum = 0xF05F6B03
  • sboot:
    • type = 0x02
    • flags = 0x81
    • reserved = 0x0000
    • position = 0x0006E07C (immediately after the srec data)
    • offset = 0x00080000
    • size = 0x00080000
    • actual = 0x00072280
    • checksum = 0xD85D3F5A
  • I’m going to stop here, but you can decode the other sections by continuing in the data.

It’s clear to me after staring more at the content of these structs that position is the start location of the data for that section in the firmware update file, offset is probably the location where it’s stored in the flash chip, size is the reserved size for the section in the flash chip, and actual is the actual amount of data included for that section in the firmware update.

Up above, I determined the data for the startup sound was located from 0xD1E2C to 0xE02DF in the file. This puts it right inside of the sboot section, almost at the end. The sboot section runs from 0x6E07C to 0xE02FC in the file.

Looking back at the >dir struct containing the BOOT-BEEP field, I guessed that maybe the sboot section would start with that structure. It would contain 20 32-bit words for a total of 0x50 bytes. The data is highlighted in the picture below:

Interpreting it using the struct definition above, we get:

  • inst0 = 0x48000080
  • inst1 = 0x000419F1
  • filler0 = 0x20010914
  • filler1 = 0x00100000
  • HWINIT = 0x00000000
  • HWINIT-size = 0x00006BF8
  • NUB = 0x00000000
  • NUB-size = 0x00000000
  • OF = 0x00006C01
  • OF-size = 0x0005D195
  • unused{0,1,2,3} = 0x00000000
  • unused{0,1,2,3}-size = 0x00000000 (and unused3-size is missing the -size in the struct definition, probably a typo)
  • BOOT-BEEP = 0x00063DA0
  • BOOT-BEEP-size = 0x0000E4C4

This data seems correct! 0xE4C4 is a reasonable length for the sound. In the image above where I showed the sound data, it said that the length of data I had extracted was 0xE4B4, which is 16 bytes smaller. The offset of 0x63DA0 for the sound is also reasonable. Adding it to the sboot position of 0x6E07C, we get 0xD1E1C, which is 16 bytes earlier in the file than the sound data I found. So it appears that the sound also has a 16-byte header:

I’m not sure exactly what all of the content of this header is. The first 32-bit value of 0x00000010 might represent the length of the header. A wild guess is that 0x0000002C = 44 is supposed to indicate that the sample rate of the final sound is 44.1 kHz. I could definitely be wrong on that one though. I’m more confident about the final 32-bit value though: 0x0001AE80 is the number of samples. The sound data is 0xE4B4 bytes long, and each block of IMA data is 34 bytes long. That means there are 0xE4B4 / 34 = 1,722 blocks. Each block represents 64 samples, so there are 64 * 1,722 = 110,208 = 0x1AE80 samples in the sound.

Anyway, this served as confirmation that I definitely discovered the startup sound’s location in ROM. I think that this even gives me enough information to lengthen or shorten the sound, but maybe someone else who’s feeling more brave can experiment with that!

So at this point, the process of injecting a new chime was looking like this:

  • Start with a sound that is exactly the same length and sample rate of the original sound: 110,208 samples at 44.1 kHz = just under 2.5 seconds.
  • Compress the sound using Apple’s IMA ADPCM format.
  • Replace the sound data in the firmware update with the new sound data, which should come out to exactly the same compressed length as the original.

The one thing this list is missing is updating any checksums. Recall that each section header contains a checksum as the final 32-bit value. Looking at the Forth code at the beginning of the update file, it’s pretty clear that it’s an Adler-32 checksum.

It seems to be checking if the section’s flags have the checksum flag set, and if so, computing the Adler-32 checksum of the entire section size minus 4 bytes. This makes sense, because the final 4 bytes would be reserved for actually storing the checksum in the flash chip.

I played around a bit with this to try to recalculate the original firmware update’s existing checksum. My first few attempts at just checksumming the “actual” length of the section failed miserably. I should have been looking closer at the code, because it made it clear that the “size” length was the important one. The overall header struct even contains a fill-byte member (0xFF) so I knew that I should pad the section with that value. To calculate the correct checksum, I started with the bytes for that section in the file (the “actual” bytes), then appended 0xFF until the entire section was exactly “size – 4” in length. This finally caused the checksum calculation to perfectly match the original.

While I was searching through the firmware update file, I found one more reference to the “adler32” word in the script. The script also checks the checksum of itself to make sure the entire firmware update file is safely intact.

Trying my best to understand this Forth code without spending too much time studying the syntax, I can say that it’s calculating the checksum of the entire firmware update file minus the last four bytes (which contain the expected checksum), and comparing the calculated checksum to the expected checksum.

This attempt at replicating the existing checksum succeeded on the first try. Yay! I felt very good about the checksum situation after this little experiment. I knew the correct way to calculate both checksums. The only fear in my mind was that there could potentially be another checksum involving the sound somewhere, but I considered that unlikely because my original Power Mac G3 startup chime patch also involved updating two checksums similar to the ones I found here and nothing else.

I bundled this entire procedure into a C++ program that ensures the replacement chime data is the correct length, compresses it with IMA ADPCM, sticks it into the firmware update file in the correct location, and recalculates the checksum. It is just a modified version of a similar firmware patching utility I made for changing my G3’s startup sound.

After running my new utility with Aidan’s desired sound and the original firmware update file as inputs, I was equipped with a patched firmware update file to try. I performed this patching process on Windows. In order to get the modified file back onto a Mac without losing the resource fork, I went through my Linux server running netatalk and Samba. I can access it through Samba on Windows to modify the data fork of a file, and it’ll preserve the resource fork information when accessed through netatalk on a Mac. The resource fork is stored by netatalk in a special .AppleDouble folder.

There’s probably a way I could have instructed Aidan to run this patched firmware update manually by booting into Open Firmware and running a command, but I honestly didn’t feel comfortable enough in OF to attempt it. So the next order of business was to figure out how to modify Apple’s firmware updater program to patch the firmware when it’s already up to date.

I had also gone through this process on my G3 in 2012. My G3’s firmware updater had an allow list of firmware versions it was able to update. This meant it was super easy to patch; I just modified one of the entries in the allow list to match the newest firmware version.

No such list existed in the iMac updater. I looked all over the data fork and resource fork, but there was nothing. This basically meant one thing: it was time to open up Ghidra!

I can at least kind of read Intel and ARM assembly language, but PowerPC has always been intimidating to me. Fortunately, Ghidra includes a decompiler so I can look at something a bit more familiar. Here’s the top of main():

Ghidra was able to automatically determine the function names, which made my job a lot easier. Most of the code displayed above is doing various sanity checks to make sure the update is actually allowed to be installed. For example, IsBlueBox() is making sure that the update isn’t being run inside of the Classic environment in Mac OS X. Here’s what ALRT resource 138 looks like in ResEdit:

I decided that CompareWithCurrentVersion() would be a good function to patch. If it returns 5 it displays ALRT resource 131, which matches the message depicted earlier:

Here’s a closer look at CompareWithCurrentVersion(). I did manually give a name to the global variable firmwareVersion to make it clearer what’s going on:

This function compares the parameter passed in (0x419f1 in the earlier decompilation of main()) to a global variable and returns one of three values: 6 if the firmware version matches, 7 if the current firmware is old enough to be updated, or 5 if the firmware is too new to be updated. There’s also a special case that I don’t understand where it modifies both versions by looking for a “d” in the location where the “f” is in 0x419f1 and turning it into a “9” prior to comparing the versions, but that doesn’t matter.

I originally opted to modify the function to return 7 instead of 5 by turning “39 80 00 05” into “39 80 00 07”, but Aidan reported that it didn’t work. If you read the previous paragraph closely, you’re probably wondering, “WTF is Doug thinking? The firmware version matches.” I realized my mistake the next day — I should have been modifying the case where it returns 6, not 5. In hindsight, this should have been obvious. My confusion stemmed from the disassembly of main(), where a return value of 5 caused ALRT 131 to be displayed. I knew ALRT 131 was being displayed, so I assumed that was the patch needed. It turns out that FirmwareIsUpdated(), which is called when CompareWithCurrentVersion() returns 6, is also capable of displaying ALRT 131. I should have paid more attention to what CompareWithCurrentVersion() was actually doing.

Although this doesn’t matter, maybe you’re curious about what’s going on in this function. It knows whether this is the first boot since a firmware flash attempt, and if so, it shows ALRT 141 instead, which says that the update was successfully installed.

To summarize the patch I settled on for the firmware updater, I simply needed to change the “39 80 00 06” in CompareWithCurrentVersion() to “39 80 00 07”. This modifies it to allow the firmware to be updated even if the current firmware version matches the update version. This does mean that on the next boot a message will pop up erroneously claiming that the firmware didn’t update correctly, but that’s just a minor inconvenience.

If you plan on attempting this patch yourself, beware that there are actually two “39 80 00 06” hex sequences in the firmware updater program. The second one is the one that needs patching.

I’m sure some of you would like to experience the final result. I bundled the files into a Disk Copy disk image, encoded it as MacBinary in order to preserve the resource fork of the disk image file, and sent it off to Aidan. Here is his video showing the firmware update installation and the subsequent boot.

For a fun little trivia fact, this is the short-lived Power Mac 6100/7100/8100 12-string guitar startup sound created by Stanley Jordan [source].

I was a little nervous about compatibility with the custom G4 soldered onto this particular iMac’s logic board, but it worked just fine. Aidan pointed out that it was already running the stock firmware, so in hindsight I had nothing to worry about. I just knew that the G3 Blue and White firmware, for example, had been intentionally patched by Apple to remove compatibility with the G4, and required a custom firmware patch to re-enable G4 support. To be safe, I asked him if he could test on a different iMac first, because I didn’t want to ruin his rare G4 iMac in case I made a mistake during the patching process. That test was also successful.

I have uploaded the patcher to GitHub in case anyone is interested in attempting this. I also included the patcher I created for the Power Mac G3 (Blue and White). If you do this, make sure to preserve the resource fork of both the update file and the updater application when you patch them. Like I said earlier, one way of accomplishing this is to use netatalk on a Linux or NetBSD server to act as the intermediary between a Mac and your development computer.

So there you have it! That’s the process I followed to patch the startup sound on an iMac (Slot Loading). When I look back on it, it wasn’t really that difficult. The Forth code was essentially serving me the solution on a silver platter. There’s enough spare room in the sboot section that theoretically you could probably insert a chime that’s about 4.5 seconds long or so, but it would get more complicated to patch the firmware update file because you’d have to shift the positions in the section structures after sboot. I’ll leave it up to someone else to attempt.

Trackback

10 comments

  1. […] Doug Brown ☛ Customizing the startup chime on a 1999 G3 iMac […]

  2. Aidan Halpin @ 2023-03-06 09:51

    So happy to see this finally seeing the light of day! I’m so proud to be apart of this pipe dream years in the making.

  3. Teen.Aeg @ 2023-03-07 06:01

    I believe this will be very useful for your future investigations into Apple’s bootrom:

    https://norwex-healthy-cleaning.com/files/yosemite_bootrom.htm

  4. I’m so glad we were able to make it work Aidan!

    Thanks for the link Teen.Aeg! That is definitely interesting info. I remember stumbling upon it once before in the past too. It seems like it would provide enough inside knowledge to do a more thorough disassembly of the B&W G3 ROM.

  5. […] out how to modify the chime the same way I did with my Power Mac G3. I thought it would be fun to take everyone along for a ride and show exactly what was involved in changing the sound. And of course, this post wouldn’t be complete without also sharing the code for the utility I […]

  6. […] See the video below and more on the methodology here. […]

  7. Teen.Aeg @ 2023-03-08 09:46

    So, what would be your next cool idea along the lines in the post? Squeezing “Doom” into the PowerMac’s bootrom to transform it into an arcade machine, perhaps?

  8. Whew…I don’t think I’d have the patience for something like that! If I find any more time to play with this, it would likely be more geared toward adding support for patching the startup sound on additional Mac models. The G4 Cube firmware update, for example, seems to be identical to this one, so it’s likely also compatible. There are a lot of other machines with official firmware updates that I’d like to look at. It would be nice to consolidate the code I linked at GitHub to work with all possible firmware updates. It would also be interesting, but potentially difficult, to figure out how to update machines that never received a firmware updater from Apple.

  9. Teen.Aeg @ 2023-03-09 10:17

    “….interesting, but potentially difficult, to figure out how to update machines that never received a firmware updater from Apple….”

    That’s indeed can be very difficult… One such machine of which I know, and which I happen to own, is PowerMac G5 late 2005 model. It’s chipset is different from the previous PowerMac G5 models, and there were no firmware updates from Apple since they were going to transition to the Intel architecture asap. So, if you succeed in quest to figure out how to update this machine, definitely let me know!

  10. I just added support for the original tray loading iMac from 1998. Thanks to Aidan for funding the project and testing it out!

    https://youtu.be/Gt3jeEWpBMk

    The code is pushed to GitHub:

    https://github.com/dougg3/MacChimePatcher

    The original iMac firmware update was laid out in the exact same way as the G3 Blue and White update. All I had to do was tweak the offsets of where the chime is located.

    I wonder if I could update the patcher utility to automatically detect the type of firmware update and location of the chime in the ROM. Then it could work with any update that’s in the same style.

    Maybe someday when I have more free time on my hands!

Add your comment now