- Mac Classic IIはROMの範囲外ジャンプバグにより誤ったアドレスへ書き込む直前まで進んだが、実際のMotorola MC68030 CPUが文書化されていない命令を実行してA1レジスタを変更し、クラッシュを回避していた
- この問題は、MAMEのClassic IIエミュレーションで32ビットアドレッシングを有効にしたときにSad MacとChimes of Deathが現れて発覚し、24ビットアドレッシングでは正常起動しているように見えていた
- ROMの
InstallSoundIntHandler経路はClassic IIのBoxFlag 17を16個の分岐テーブルに適用して0x40A43B94へジャンプし、この位置は本来MOVEA.L命令の途中だった
- 該当バイト列
0C EC 08 A9 00 04はCAS D1,D2,$0004(A4)に似ているが、予約ビットが立った無効なCAS系命令であり、実際の68030ではA1をFFFF8FBAから40A4BBB2へ変更した
- 実機Classic IIに3種類のカスタムROMを入れて検証した結果、この命令をNOPで除去するとハードウェアでもSad Macで失敗し、MAMEは正確な挙動が分かるまでROMパッチで起動を回避している
MAMEで明らかになったClassic II起動失敗
- MAMEはアーケードゲームエミュレータとしてよく知られているが、68000ベースのMacモデルのエミュレーションもサポートしている
- command+powerキーの組み合わせでデバッガを呼び出す機能をMAMEで修正していたところ、Classic IIで奇妙な挙動が見つかった
- Classic IIには物理的な割り込みボタンがあり、キーボード・マウスなどを処理する「Egret」68HC05マイクロコントローラもある
- MacsBugにはcommand+powerキーの組み合わせを有効にする命令を送るコードがある
- Classic IIは24ビットアドレッシングでは正常起動したが、MacsBugの読み込みに必要な32ビットアドレッシングを有効にするとSad MacとChimes of Deathが現れた
- Apple Tech Info Library文書によると、Sad Macコード
0000000Fは例外、00000001はバスエラーを意味する
- 68k Macでのバスエラーは通常、存在しない拡張カードへのアクセスのような誤ったアドレスアクセスを意味する
ROM追跡: サウンド割り込み初期化経路
- MAMEデバッガでバスエラーハンドラアドレス
0x40A026F0にブレークポイントを設定し、ROMシンボルマップで呼び出し経路を追跡した
- このアドレスはROMマップで
GenExcpsと表示される
- 共通エラー処理ルーチンは
ToDeepShitという名前で表示される
- バスエラーを引き起こした命令は
0x40A43B9Cの次のコードだった
move.b #$90, ($1c00,A1)
- このコードはROMマップで
InstallSoundIntHandlerと表示されたルーチン内にあり、Classic IIのサウンド割り込み初期化に関連している
- Classic IIは元のMacintosh ClassicよりもMacintosh LC系の構造に近く、LCのV8チップに似たEAGLEゲートアレイを使用している
- ROMコード内に
V8SndIntPatch1のような名前が登場する理由はここにある
- EAGLEにはApple Sound Chip(ASC)の縮小版に相当する機能が含まれている
誤った分岐テーブルとA1レジスタの問題
V8SndIntPatch1はGestalt trapのgestaltHardwareAttrセレクタ、つまり'hdwr'を使ってハードウェア属性を確認する
gestaltHasASCビットがなければ戻る
- Classic IIでは
gestaltHasASCが設定されている
- その後のコードがRAMの
0xCB3からBoxFlag値を読み取り、D0に入れる
- Classic IIの
BoxFlag値は0x11、つまり17である
- 問題の分岐コードはD0を2倍にしたうえで、PC相対ジャンプオフセットとして使用していた
add.w d0,d0
jmp loc_40A43B72(pc,d0.w)
- 各
BRA.S命令が2バイトなので、BoxFlag値を2倍して分岐テーブルのインデックスに使う構造だった
- 実際のテーブルにはBoxFlag 0〜15に対応する16個のエントリしかなく、Classic IIの値17は範囲外だった
- 計算されたジャンプ先は
0x40A43B72 + 0x22 = 0x40A43B94
- この位置は意図された命令の開始点ではなく、
MOVEA.L命令の途中だった
- 本来は
MOVEA.LがA1に正常なアドレスをロードするはずだったが、命令の途中へジャンプしたことでA1は直前の相対ジャンプ計算に使われた値0xFFFF8FBAのまま残った
- 続いて
move.b #$90, ($1c00,A1)が実行されると、書き込みアドレスは0xFFFF8FBA + 0x1C00 = 0xFFFFABBAとなり、Classic IIでは不正なアドレスになる
文書化されていない68030の挙動
0C EC 08 A9 00 04
- IDAとGNU objdumpはこの位置で正常に命令を逆アセンブルできなかった
- Macintosh IIciのMacsBugで同じバイト列を実行すると
CAS.W D1,D2,$0004(A4)のように表示されたが、実行後にA1の値が変わった
- A1は
0xFFFF8FBAからRAMアドレスのように見える値へ変化した
- 新しいA1値は元のA1、A7、プログラムカウンタに応じて変わることが観察された
- Motorola M68000 Family Programmer’s Reference Manualによると、最初のワードはCAS命令のように見えるが、2番目のワードには0であるべき3つのビットが1に設定されている
- したがってこの命令は、文書化された有効なCASではなく、予約ビットが有効になった不正な命令である
- 実際の68030はこの命令をillegal instruction例外として処理せず、A4+4に対するread-modify-writeバスサイクルを実行した
- A4を不正なアドレスに設定すると、MacsBugはA4+4でのread-modify-write中にバスエラーを表示する
- 正常なCASであれば変更しないはずのA1も変化する
実機Classic IIハードウェアでの検証
- 仮説を検証するため、1991年製のClassic IIを購入して修理したうえで、ROMをプログラマブルなSST29EE010 EEPROMに交換した
- 古いMacの表面実装コンデンサは腐食性のある液漏れを起こすことがあるため、通電前に取り外して修理した
- CRTとアナログボードの代わりにロジックボードを単独で動かし、Raspberry Pi PicoベースのVGA変換器で画面を確認した
- A1値を画面に表示する68030アセンブリコードをROMの空き領域に入れ、3種類のカスタムROMを作成した
- Custom ROM 1: MAMEでSad Macを引き起こした
0x40A43B9CのMOVE.BをA1表示コードに置き換え
- Custom ROM 2: 範囲外ジャンプ先である
0x40A43B94のCAS風命令をA1表示コードに置き換え
- Custom ROM 3:
0x40A43B94のCAS風命令をNOPに置き換え
- Custom ROM 1は、そのコードが実機でも実行され、MAMEでクラッシュした地点のA1が
0x40A4BBB2であることを示した
- この値はROMアドレスなので書き込み先としては適切ではないが、書き込みを試みてもバスエラーは起こさない
- Custom ROM 2は、範囲外ジャンプ直前のA1がMAMEで見た値と同じ
0xFFFF8FBAであることを示した
- 範囲外ジャンプが実際に発生している
- その後にCAS風命令がA1を変更するという仮説とも一致する
- Custom ROM 3は、CAS風命令を除去すると実機Classic IIでもSad Macで失敗することを示した
- このテストではハードウェアが24ビットモードでも同じSad Macを示した
- MAMEが24ビットモードで誤った書き込みに対してバスエラーを出さなかったのは、実機より甘い挙動だった
MAMEとエミュレーションに残る課題
- 発見された挙動は、文書化されていないMC68030命令がread-modify-writeバスサイクルを実行し、A1レジスタまで変更する事例である
- Classic II ROMのバグはこの挙動のおかげで隠され、実機は正常に動作していた
- 同じコード片はより新しいMacintosh IIvxのROMにも見つかったが、そのROMではジャンプテーブルのサイズが拡張され、Classic IIケースは直接RTSへジャンプする
- この挙動のため、現時点では100%完全なMotorola MC68030エミュレータやクローン実装が存在しない可能性が高い
- この命令を実行してA1の結果を確認すれば、物理68030とエミュレータを見分ける小さなコードを作れる
- MAMEは正確な命令の挙動が解明されるまで、Classic II ROMバグをパッチして起動可能にしている
- 別件として、command+powerキーの組み合わせは実機Classic IIではMacsBugインストール時に動作するが、MAMEではまだ動作しない
1件のコメント
Hacker News のコメント
発見された MC68030 の挙動は、CPU 設計者が意図的に作った「本物の」命令というより、不正命令に対して CPU 内部ロジックが don't-care 入力を受けたまま偶然実行された結果かもしれない
通常なら CPU が不正命令を検出して例外を出すべきだが、場合によってはそうならないようだ
https://www.nxp.com/docs/en/reference-manual/MC68030UM.pdf の 8-9 ページには、不正命令は「最初のワード」のビットパターンを基準に定義するとある
今回の命令は 3 ワードで、最初のワードは正常、奇妙なビットは 2 番目のワードにあるため、68030 が 2 番目のワードを検証せずに CAS 命令の実装ロジックへそのまま進んだ可能性が高い
「有効な」命令コードとは、ドキュメント化するだけの価値がある有用な組み合わせにすぎず、「無効な」組み合わせは役に立たないか意味のない組み合わせだった
そう考えると、不正/謎の命令コードは驚くべき例外ではなく、ほとんど必然に近い
普通なら、仕様上意味のない 0 ビットは完全に無視されると期待する
ところが 68030 では、そのビットが 0 でなければならず、6502 の不正命令コードのように、そのビットが立つと別の命令用のハードワイヤードロジックが有効になる場合もあり得る
CAS はいつも厄介で、エミュレーションのバグ報告を最も多く受けた命令もおそらくこれだった
昔の King of Fighters にも、SBCD 命令のキャリーフラグを「間違って」確認するバグがあり、それをラウンドタイマーの減少とラウンド終了に使っていた
たとえ文書化されていない挙動でも、二進化十進演算で算術ステータスフラグをエミュレートしないと、KOF のラウンドタイマーが 00 から 99 へ延々と循環して終わらなくなる
SNK は本当に 68000 チップの神々だった
本当に長い旅だ
最近は筆者のようにそこまで深くウサギ穴へ降りていく忍耐力はないが、何かを知っていると思うことと実際に知っていることの間から来る達成感には大いに共感する
時間はかなりかかっても、最後まで行けば本当に報われる
30 年以上たった今でも、あれほど小さな画面解像度で Mac デバッガ UI がどれほど効果的であり得たのか、いつも驚かされる
本当に職人技を感じる
ほぼすべての CPU には文書化されていない命令があり、68k も例外ではない
ただ当時、十分な関心と低レベルの知識を持つ人々の大半が x86/PC に集中していただけで、x86/PC は Apple 側よりもはるかに開かれていて安定したアーキテクチャだったと言える
8088 と 8086 のマイクロコードは数年前に逆アセンブルされ、多く研究されており、トランジスタレベルのシミュレーションの試みもあったと理解している
x86 の命令コード空間の構造も、次のような文書で詳しく探求されてきた
http://ref.x86asm.net/geek.html
https://gist.github.com/seanjensengrey/f971c20d05d4d0efc0781...
この命令が正確に何をするのかはまだ分からない
限定的なテストでは、A1 の結果値は元の A1 の値、A7 の値、プログラムカウンタによって変わるように見えたが、確かではない
誰かがさまざまなレジスタ値とメモリ内容を入れてみるプログラムを作り、正確な動作を推論できれば、エミュレーションもさらに正確になるはずだ
現時点では、誰かが掘り下げる価値があると見るまでは、MAME は Classic II の起動のために ROM のこのバグをパッチしている
個人的には、正確なエミュレーションのために十分解明する価値があると思う
68k には詳しくないが、命令のビットが手がかりを与えている。2 番目のワードの 5:3 ビットが別のモードフィールドのように見え、モード 000 で Dn レジスタを選ぶ代わりに 101 が再び (d16, An) を選び、001 が入っている Dc フィールドが A1 と解釈されているのではないか、という仮説だ
68030 は当時かなり優れた汎用 CPU で、Amiga 3000、Atari Falcon、Sun 3、NeXT Cube などにも使われていた
デモシーンでは依然として 8 ビット家庭用コンピュータにこだわっており、16 ビット家庭用システムを使っていた人々は Atari と Amiga に集中していた
家庭で PC と x86 が本格的に台頭したのは、VGA とサウンドカードが標準的な PC 構成に入ってからだった
子どものころ、68000 プロセッサを搭載した Amiga 2000 を持っていた
68020 や 68030 の話を聞いたときは本当に興奮したし、その後の RISC アーキテクチャにも同じように興奮した
Amiga にロボットのような声で「Hello, how are you?」のような文を話させると、友人たちは月に行って帰ってきたかのように驚いていた
40 年もたたないうちに、LLM でコンピュータと自然言語で会話し、Python、VS Code、LLM を道具として、望むことのほとんどを自動化できるようになるとは想像もしていなかった
本当に狂った時代だ
68000 ファミリーが懐かしい
本当に素晴らしいチップたちだった
優れたエンジニアリングが常にそうあるべきように明快で論理的で、ざっと眺めるだけで何が起きているのかすぐ理解できた
逆に x86 アセンブリは、ほとんどが徹夜明けに出てきた賢い小細工のように見える、純然たる寄せ集めに思える
0 を得るためにレジスタから自分自身を引くようなやり方は、本当にひどすぎる
これが特定のシステムで動かないようにするためのコピー防止だったのか、それともすべての68030で起きる現象なのか気になる
ここで絡んでいるジャンプテーブルに該当機種用の項目が抜けていて、おそらくROM担当者が新しい項目の追加を全員忘れたものの、まったくの偶然でテーブル項目がなくても動作していたように見える
現在のMacでは、こういうことは今後不可能に見える
最近のAppleの技術文書はひどい
昔のようにユーザーがカードを挿し、サードパーティ開発者がハードウェアと直接やり取りする必要があるシステムバスをコネクタとして露出していない
現代のMacには、ユーザー空間プログラムから扱えるUSBとThunderboltがある
もちろん、最新のmacOS APIの一部の文書が非常にお粗末だという点は否定しない
ヘッダーに青いグラデーションがあり、「Apple documentation archive」と書かれた古いページにたどり着いたら、何かすごいものに触れたのだと分かる
アドレス空間がはるかに大きく、ほとんどのアドレスがマップされていないため、ゴミのようなアドレスでメモリ操作を行うとたいてい失敗するだろうし、無効な命令もおそらく失敗する
範囲外のインデックスでジャンプテーブルを読むバグ自体は現代のソフトウェアでも起こり得るが、ここでのように走り続けるよりはプロセスが落ちる可能性が高い
ちなみにWebAssemblyは単一の線形アドレス空間を持ち、すべてのアドレスが読み書き可能なので、この種のバグはより起こりやすい
ゴミアドレスが範囲内にあれば、誤った読み取り、書き込み、CASを行ってもそのまま進んでしまうことがある
ただし本文のような無効命令はWASMモジュールのロード失敗を引き起こすため、Mac ROMのこの問題と1対1で同じではない
040/060もこの「文書化されていない命令」をサポートしているのか気になる
A1にはまったく触れておらず、これ以上テストしていないが、単に通常のCASとして処理しているのかもしれない
LC 475のMacsBugでこの命令を初めてステップ実行したときはシステムエラーになったが、その後は問題なかった
その命令ワードだけをざっとテストした結果である