今更ながらに「西暦2000年問題」を修正した話

MSXの西暦2000年問題に直面して「ROMさえ焼ければ数バイトのパッチあてで修正できそう」などと言っていたんですが……意外や意外、ROMライターって安く手に入るんですね。amazonで6K円弱。なんかROMライターを手に入れるのが現実味を帯びてきたのと、最近ハードウェア寄りのネタばっかりだったので、今回はソフトウェア寄りでパナソニックMSX2+内蔵ソフトの西暦2000年問題パッチを作ってみようかと。
手元にある動作するパナソニックMSX2+のうち、FS-A1WSXは2000年問題無しなので関係なし、FS-A1WXは内蔵ソフトの規模が大きいのと問題があまり深刻ではないので保留として、内蔵ソフトの規模が手ごろで解析しやすく問題(日付が1988年に強制変更されてしまう)も深刻なFS-A1FXを対象にします。
メインボードに載っているROMは2Mbit(256KB)あるものの、内蔵ソフトに使われている領域はそのうち32KBだけで、2MbitのROMを物理的に引っこ抜いて吸い出すような真似はしなくても、スロット#3-3のページ1〜2に該当の32KB分が現れているので、実機のツールで吸い出してディスクにセーブすればイメージは得られます。それを、昔からお世話になっているZ80アセンブラ「HOJA(ほぉじゃあ)」のJava版*1で逆アセンブルして解析用のソースコードを作成します。
ではいざ解析開始。まず取っ掛かりとして「初期化(1988年1月1日に設定)しているコード」を探します。RTCの内容が書き換わっている以上、どこかでその値を書き込んでいるはず。ということで「WRTCLK」(RTCへの書き込みBIOS)ルーチンのアドレスである「01F9h」を検索します。このBIOSはSUB-ROMにあるので、IXレジスタに呼び出し先である01F9hを入れて「EXTROM」ルーチン(アドレス015Fh)を経由して呼び出しを行っているはずです。なので検索するのは「LD IX,01F9H」命令。これでRTCに書き込みを行っている場所を抽出します。
順に見ていくとアドレス77C2hからのルーチンがそれっぽいです。RTCのバンク0・レジスタ#7からに1,0,1,0、つまり「1月1日」を書き込んでいます。*2

	LD	C,07H		;77C2	0E 07
	LD	HL,0784CH	;77C4	21 4C 78
	LD	A,(HL)		;77C7	7E
	LD	IX,01F9H	;77C8	DD 21 F9 01
	PUSH	HL		;77CC	E5
	PUSH	BC		;77CD	C5
	CALL	015FH		;77CE	CD 5F 01
	POP	BC		;77D1	C1
	POP	HL		;77D2	E1
	INC	HL		;77D3	23
	INC	C		;77D4	0C
	LD	A,(HL)		;77D5	7E
	CP	0FFH		;77D6	FE FF
	JR	NZ,077C8H	;77D8	20 EE
	(中略)
	DB	01H,00H,01H,00H	;784C	01 00 01 00
	DB	08H,00H,0FFH	;7850	08 00 FF

となると、このコードの前に判定部分があるかも……と思って見てみたら、本当に直前で判定していました。

	LD	HL,(0C566H)	;77AA	2A 66 C5
	LD	DE,1900		;77AD	11 6C 07
	OR	A		;77B0	B7
	SBC	HL,DE		;77B1	ED 52
	JR	C,077C2H	;77B3	38 0D
	LD	HL,(0C566H)	;77B5	2A 66 C5
	LD	DE,2000		;77B8	11 D0 07
	PUSH	HL		;77BB	E5
	OR	A		;77BC	B7
	SBC	HL,DE		;77BD	ED 52
	POP	HL		;77BF	E1
	JR	C,07808H	;77C0	38 46
	(略)

……ぉぅふ。なんか2000って数値*3が見えるんだけど……(汗)。アドレスC566hはおそらく4桁西暦のワークエリア、アドレス7808hはRTCへの書き込み処理が終わった後の続き、77C2hは先ほど見つけたRTCへの書き込み処理の開始位置ですね。引き算してキャリーフラグで判定してジャンプしているので、要するにこれは「西暦が1900年未満または2000年以上ならRTCへの書き込み処理へ進む」というコード。ってことは2000年以降の日付が1988年に再設定されてしまうのは「バグ」じゃなく「仕様」の可能性が高いってことか。まったく、酷い仕様もあったもんだ……(苦笑)。
4桁西暦で比較して異常値の判定をしているので、その範囲を修正すれば2000年以降の西暦でもRTCが初期化されることは無くなるはず。ということで、MSXシステムの仕様(1980年〜2079年に対応)に合わせてこんな感じに修正します。

	LD	HL,(0C566H)	;77AA	2A 66 C5
	LD	DE,1980		;77AD	11 BC 07
	OR	A		;77B0	B7
	SBC	HL,DE		;77B1	ED 52
	JR	C,077C2H	;77B3	38 0D
	LD	HL,(0C566H)	;77B5	2A 66 C5
	LD	DE,2080		;77B8	11 20 08
	PUSH	HL		;77BB	E5
	OR	A		;77BC	B7
	SBC	HL,DE		;77BD	ED 52
	POP	HL		;77BF	E1
	JR	C,07808H	;77C0	38 46
	(略)

異常値として1980年より前や2079年より後がワークに入ることがあるのかどうかは不明ですが、パッチですし出来るだけ元のコードを尊重するのならこうなるのかなと。ROMのバイナリイメージ的には先頭がアドレス4000hなので-4000hしてオフセット37AEh〜37AFhを「6C 07」から「BC 07」に、同様に37B9h〜37BAhを「D0 07」から「20 08」に、それぞれ書き換えることになります。
さて。パッチは当てたものの非メガROMのROMカートリッジのイメージなので、似非RAMに書き込んでも動かせません*4。どうやって動作チェックしようか……。
……うーむ。無いならば 作ってしまおう SRAMカートリッジ(字余り)。他にも使えるかなということで、動作試験用にSRAMカートリッジをサクっと*5作ってしまいました(笑)。詳しく書くとまたハードウェア寄りの記事になってしまうので別の機会に譲りますが、書き込み禁止スイッチとバックアップ用のキャパシタの付いた64KBのRAMカートリッジです。書き込み禁止スイッチが付いているのは、MSX2+以降のシステムではブートシーケンス中でRAM上のROMヘッダを消去してしまうため。書き込み禁止スイッチONにすれば、MSXシステムからはROMのように見えて消されなくなります。

テスト用の環境も用意できたので、レッツ実行。にがさん作成の「NSLOAD.COM」*6でパッチを当てたROMイメージをカートリッジにロードし*7、WX/WSXの内蔵ソフト無効化の機能がある似非RAMも一緒に挿して電源ON。無事起動したFS-A1FXの内蔵ソフトを経由してBASIC(MSX-DOS)を起動し、DATEコマンドで日時を確認すると……あれれ? やっぱり1988年に初期化されてる?

WSXでFXの内蔵ソフトが起動しているので、起動しているのはSRAMカートリッジに読み込まれたパッチを当てたイメージであることは間違いありません。となるとまだどこかで似たような処理が動いているんでしょう。再度1988年1月1日を設定しているところを探してもいいですが、今度は観点を変えて*8「RTCを読み出しているコード」を探します。
ざっと見てみると718ChからのルーチンでC422hからのワークにRTCのレジスタを連続して転送しているようなので、718Chをコールしたあとメモリの内容をチェックしているようなコードが無いか探します。すると70DDhからのルーチンでそういう処理を行っていました。MSXのRTCであるRP5C01は1ニブル(4ビット)のレジスタに1桁のBCDを保持していて、秒の1の位、秒の10の位、分の1の位……というふうにレジスタが配置されているので、どうやらここではそれが時分秒の範囲内に収まっているかどうかを確認しているようです。範囲外だと7114hへジャンプしていますが、7114hはやはり1月1日を設定するコードになっていました。

	CALL	0718CH		;70DD	CD 8C 71
	LD	HL,0C422H	;70E0	21 22 C4
	LD	A,(HL)		;70E3	7E
	CP	0AH		;70E4	FE 0A
	JR	NC,07114H	;70E6	30 2C
	INC	HL		;70E8	23
	LD	A,(HL)		;70E9	7E
	CP	06H		;70EA	FE 06
	JR	NC,07114H	;70EC	30 26
	(略)

ただ2000年以降の日付がこのチェックで異常と判定されるようには思えませんし、このルーチンでは時分秒しかチェックしていないようです。もう少し追ってみると時分秒チェックの後に714Bhのルーチンを呼んでからキャリーフラグを判定しています。この714Bhが年月日のチェックになっていました。年月日の範囲判定は各桁単独では難しいのか、BCDを16進数値に変換したうえで比較しているようです。

	CP	018H		;7109	FE 18
	JR	NC,07114H	;710B	30 07
	INC	HL		;710D	23
	INC	HL		;710E	23
	CALL	0714BH		;710F	CD 4B 71
	JR	NC,0713DH	;7112	30 29
	(略)
	LD	A,(HL)		;714B	7E
	CP	0AH		;714C	FE 0A
	LD	B,A		;714E	47
	JR	NC,0718AH	;714F	30 39
	INC	HL		;7151	23
	LD	A,(HL)		;7152	7E
	ADD	A,A		;7153	87
	LD	C,A		;7154	4F
	ADD	A,A		;7155	87
	ADD	A,A		;7156	87
	ADD	A,C		;7157	81
	ADD	A,B		;7158	80
	JR	Z,0718AH	;7159	28 2F
	CP	020H		;715B	FE 20
	JR	NC,0718AH	;715D	30 2B
	(略)

そのまま読んでいくと、最後に年の判定が。10の位を10倍して1の位に加算しそれを……ん? 20以上だと異常値として判定されてる? この20ってのはどこから出てきた?*9 そもそも20で判定したら1920年以降全部アウトじゃないのか? うむむ?

	LD	A,(HL)		;7175	7E
	LD	B,A		;7176	47
	CP	0AH		;7177	FE 0A
	JR	NC,0718AH	;7179	30 0F
	INC	HL		;717B	23
	LD	A,(HL)		;717C	7E
	ADD	A,A		;717D	87
	LD	C,A		;717E	4F
	ADD	A,A		;717F	87
	ADD	A,A		;7180	87
	ADD	A,C		;7181	81
	ADD	A,B		;7182	80
	CP	20		;7183	FE 14
	JR	NC,0718AH	;7185	30 03
	OR	A		;7187	B7
	JR	0718BH		;7188	18 01
	SCF			;718A	37
	RET			;718B	C9

ちょっと疑問は残りますが、年の判定があって異常の場合のジャンプ先が初期化コードになっているので、ここもパッチ対象のはず。数値20の意味が不明なので数値には手を加えず、エラー戻りへのJR命令を潰して様子を見ます。

	CP	20		;7183	FE 14
	NOP			;7185	00
	NOP			;7186	00
	OR	A		;7187	B7
	JR	0718BH		;7188	18 01
	SCF			;718A	37
	RET			;718B	C9

ここまで書き換えたROMを先ほどと同様にSRAMカートリッジに書き込んで実行します。すると……。

おぉ。カレンダー表示で1988年じゃなく設定しておいた今日の日付になってる。となると、やはり先ほどの数値20が2000年を表していて、あの判定で範囲外だと判定されていることに。数値20はRTCから読み出した生の値と比較しているので……もしかして内部表現の西暦は見た目通りじゃないのか? と、ここで「じゃあ初期値の1988年はどう表現されているのか?」と思いつき、一番最初の初期化ルーチンのところに戻ります。

	DB	01H,00H,01H,00H	;784C	01 00 01 00
	DB	08H,00H,0FFH	;7850	08 00 FF

このデータ、FFhは終端コードとして、日の1の位、日の10の位、月の1の位、月の10の位、年の1の位、年の10の位と並んでいるはずなので……あれ? 88年のはずなのに数値はBCDで08h? ……って……あ!そうか! 確かRTCに書き込まれる年は80年がBCDで00hになるんだっけ。ようやく思い出した*10。となると1980年が0だから……2000年は2000−1980=20で……これでようやく辻褄が合ったよorz。今回は2000年→2080年に修正するので20+80=100(またはBCD2桁で99hまで許容するので100)。こんな風に書き換えれば解決。しかし、ここでも2000年を判定しているのなら、もう言い訳の余地なく「仕様」ですねこれは。まったく、酷い仕様もあったもんだ……(苦笑)。

	CP	100		;7183	FE 64
	JR	NC,0718AH	;7185	30 03
	OR	A		;7187	B7
	JR	0718BH		;7188	18 01
	SCF			;718A	37
	RET			;718B	C9

すでに行ったパッチ当てに加えて、ROMのバイナリイメージのオフセット3184hを「14」から「64」に書き換え。これで先ほどの仮設パッチ同様うまく行きました。休日(写真では成人の日)がズレてるのはパッチ当てのレベルではどうしようもない*11ので保留ですね。内蔵ソフトスイッチONでRTCの日時が初期化されてしまう問題は解決したので、ひとまずこれで完成ということで。内部的に西暦値が2000以上になった場合に正常動作するかが微妙なところですが、カレンダー表示以外に西暦に依存するものは無いと思われるので、気にしないことにします。最終的なパッチとしては3か所で合計4バイトの書き換えです。
念のため「01 00 01 00 08 00」の6バイトのテーブルがここまで見つけた2か所以外に無いか検索してみましたが見つからず。さすがに「LD A,1」とかで1個ずつレジスタ値を用意しているコードは……無いでしょう……無いはずです……無いと思いたいな(苦笑)。あとは上位4ビットが無視されるのを見越して上位に0以外が入ってるテーブルを使ってる、とか……そんな意地悪なコードが無いことを祈ります。
さて。FS-A1FXの内蔵ソフトのY2K対策パッチ当てが完成してしまいました。この32KBのイメージを、メインボードの2Mbit ROM全体イメージのオフセット20000h〜27FFFh*12に書き込むことが出来れば挿し替えられます。もうこれはROM焼いて挿し替えてみるしかないかなと思ってますが……。流石にコレ頒布したらパナソニックに怒られるかな。お目こぼし頂けるなら、Y2K対策品として焼いたROMチップを頒布したいところなんだけど。でもまあ、いまさらFX用のY2K対策済みROMなんて欲しい人は居ないかな? 頒布の目途が立つならWXとかFS-A1STの対策ROMもやってみたいですが……。

*1:作者さんが2014年の公開時に「もう需要がないのは百も承知」(http://d.hatena.ne.jp/njisho/20140224/1393241927)などと書かれていていましたが、そんなことはありません。私は昔っからお世話になりっぱなしです。今回はJava版があって助かりました。

*2:この時点で西暦の内部表現について見落としていたので後でちょっと引っかかりました。

*3:実際の逆アセンブルリストでは16進表記ですが、西暦に関連する数値は判りやすいよう10進表記に直してあります。

*4:メガROMはバンクの初期値が全ページで0になっていて、バンクサイズ以上のイメージの場合はバンクの初期化コードが必要になるため。

*5:と言いつつ設計したり材料買い集めたりして作っているので実質丸1日掛かってますが。

*6:「フリーウェアコーナー」のページ(http://niga2.sytes.net/msx/free.html)からダウンロードできます。

*7:作成したSRAMカートリッジは「にせRAM」カートリッジとしてNSLOAD.COMから認識されて普通に使えました。

*8:この辺は勘です(苦笑)。

*9:先に書いた通りここで引っかかりました(苦笑)。

*10:公式MSXエミュレータの時に同じ問題に当たってたはずなんですよね。忘れるなよ自分…。

*11:1988年当時は祝日が固定日だったので祝日判定ルーチンもそのように実装されているはず。直すとなると新規書き起こしになると思われるので……。

*12:これまたにがさんの「松下FS-A1FXの改造・前編」ページ(http://niga2.sytes.net/msx/FX.html)の情報から。足向けて寝られないですね。ありがたやー。