|
Alone In The DarkはSCSI CD-ROMから起動するとBGMが鳴らなかったものの修正パッチの作成に成功。
大航海時代2が起動しないと思ったらデータディスクが壊れてたんだった。Towns OS V2.1 L31のGUIからAUTOEXEC.BATをダブルクリックで普通に起動する模様。
新たに動作確認
新たに不動作確認
TAITO Chase HQ for FM TOWNSもSCSI CD-ROMから実行できるようになった。これは難しかった。そもそも、コードが暗号化されてたけど、それはRainbow Islandで使ったのと同じ方法で解決。こんな方法を当時知ってたらAfterburner 2をサイバースティックに対応させたのに!
しかし、CD-ROM I/Oへの書き込みを単純にCD-ROM BIOS(とYSSCSICD.SYSの拡張機能)に置き換えただけだと、フリーズしてしまう。RS232Cにコマンドダンプを出したところ、実に興味深いことが判明。どうやらYSSCSICD.SYSに入ってきているが抜けてない。だから、我がドライバの中でフリーズしていた。さらに調べたところ、どうやらChase HQはSCSIその他のIRQを基本Disableしているらしいことが判明。IRQが飛んでこないからSCSI BIOSから返ってこなかったらしい。
しかし、Chase HQは途中でファイルを読んでいる。ファイルを読むためにはIRQをEnableしなくてはならない。だからEnableしたりDisableしたりしているはずだ。INT 21Hでファイル読み込み関数を探し、そこからIRQをEnableする関数と再度Disableする関数を発見。パッチではまずIRQをEnableしてCD-ROM BIOSを呼び再度IRQをDisableするように変更したところ動き出した、かに見えた。
ところが、残り時間の整数部分が16秒になったとたんにCD-ROM関数を連続で呼び出し始めてフリーズ。エラーチェックなんかしてないはずなのにリトライしているように見えた。残り時間が少ないという警告メッセージ部分を再生しようとしているらしい。しかし、既にCD-ROMコマンド関数は解明していたので、そこにトラップを仕掛けて呼び出しもとのEIPをRS232Cに出すようにして、呼び出し箇所はすぐ特定。
なんと、残り時間が17秒を切ってから16秒を切るまでの間、ひたすらCD再生コマンドを送り続けていた。Chase HQは60フレーム毎秒にできていたようなので60回もCD再生コマンドを送っていたようだ。元のプログラムがそれでよかったのは、このとき使った機能はDrive Readyもチェックしていなければ再生を開始したか失敗したかエラーチェックもしていなかったから。しかしこれをCD-ROM BIOSを使って実行しようとすると一回あたり1秒以上の待ちが入る。たった1秒分のために一分以上待たなくてはならない。
しかし、修正は簡単でジャンプ命令をひとつ修正するだけで、残り時間が16秒台になった瞬間に一度だけ警告メッセージを再生するようにした。
僕はそんなにChase HQが上手じゃないからクリア確認できてないけど、ラウンド2までクリアできたから多分最後まで行けるんじゃないかな?
パッチは、今FM TOWNSを動態保存しようってぐらいの人ならCのプログラムぐらいコンパイルして実行できるだろうと思ってソースだけ公開したけどやっぱりバイナリ(当然TOWNS用EXP形式)もあった方がいいかな?
Rainbow IslandはEXPに暗号がかかっていたのでパッチを作るのが大変だったのだが、Free386作者のnabe-abkさん(https://github.com/nabe-abk)がEXPファイルフォーマットを公開していただいたおかげでファイルの末尾にパッチを当てるコードを追加するという技でI/OアクセスをYSSCSICD.SYSにリダイレクトさせることに成功。nabe-abkさん、ありがとうございます!
同じ手でChase HQもできるかと思ったんだけど、こっちは失敗。CD-ROM BIOSにリダイレクトまではできたんだけど、なぜかYSSCSICD.SYS内でクラッシュしてしまう。なんのパラメータも与えずにCD Stop (AH=52H)を送ってもクラッシュするところを見るとChase HQのEXP内でIRQをいじっているのではないか。もしもVSYNCとかのためだけだったらそれをDisableすれば動くかも。逆アセンブルはすべて取れてるからそのへんを調べてみよう。
Read TOC (AH=54H)がCD全体の長さを2秒短く返していた影響でOperation Wolfが起動しなかった。実はCDSD.SYSを読んだとき2秒短く返すように書いてあった気がしたのでそのようにしてあっただけなので、単にその2秒の引き算を削除することで対応。ひょっとしてたまたま動いていたタイトルがあったらいかんと思ったので、過去に動作確認したタイトルも5~6本テストしたけど問題無いようだった。最新バージョンはGitHubから。
https://github.com/captainys/FM/tree/master/TOWNS/YSSCSICD
Afterburner 3がCD-ROM I/Oを直接攻撃している影響でSCSI CD-ROMドライブから起動できなかったけど、そのパッチも成功。ついでに15KHz画面モードを使わないように修正した。
ただ、他のCRIのFM TOWNS用ゲームでは、パッドのAボタンを押しながら起動すると31KHzの画面モードを使うようにできていたのだが、どうしてAfterburner 3はできないのか?ソースを見たらDS:[00020DF4H]が1のとき31KHzの画面モードを使うように書いてあるのだが、なぜかどこにもDS:[00020DF4H]に1を書き込んでいる箇所が無い。なんでかと思ったんだけど、ここを無理やり1にしてやっても画面が崩れる。Afterburner 3はFM TOWNSのGraphics BIOSの標準的なCRTC設定から逸脱した画面設定を使っている。多分31KHzモードで画面を崩れないようにできなかったのかもしれない。
ただ、プレイ画面は右にゴミが出るものの問題なく表示されるからプレイに支障は無さそう。
最初失敗したけどコピープロテクトがかかってるの忘れてた。というか、プロテクト解除したバージョンを使ったつもりだったんだけど解除してなかった。アメリカの著作権法だとクラシックゲームの動態保存のためのコピープロテクト解除は合法とされてるんだけど、日本もそうなったらここに解除方法書くんだけど。まだそうなってないらしいから保留。
一応、SCSI CD-ROMドライブに遥かなるオーガスタのCD-ROM、Aドライブにプログラム/ユーザーディスク、BドライブにTowns OS V1.1+YSSCSICD.SYSによる起動ディスクを入れた状態でREIPL B:で起動できる。なお、486DXのFASTモードで使おうと思ったらマウスカーソルが飛びまくるので、起動フロッピーディスクのTBIOS.SYSとTBIOS.BINをTowns OS V2.1L20のものに置き換えておくといい。
SETUP.EXPのプロテクトの解除方法がわかる人は、起動フロッピーディスクに遥かなるオーガスタのプログラム/ユーザーディスクの内容をコピーしてしまえばディスク一枚で起動可能。
パッチが必要だけど起動できるようになった。ただしV1.1L30の起動ディスクから立ち上げる必要があるけど。パッチも近いうちにアップロード予定。
パッチ当ててないバージョンで健全なCD-ROMドライブがある状態だとV2.1L20から起動できるんだけど、クラッシュしてる場所まで特定したが何が悪いんだかわからん。
CD-ROM BIOS機能、INT 93H, AH=05H/15H, CH=0FFHの対応に成功!この機能はセクタの内容を読みだして下位4GBの物理メモリ空間に直接書き込む機能で、DMAを使って実現しているが、Real Modeのドライバではこれをエミュレートすることは不可能。そこで、Unreal Mode (https://wiki.osdev.org/Unreal_Mode) を使って読み込んだセクタの内容をハイメモリに転送するようにしてみた。これによりRAYXANBERはTowns OSV1.1L30ブートフロッピーで起動するようになった。
また、Galaxy Force 2は直接I/Oを攻撃してセクタを読み込んでいるので、これをBIOSを使うようにパッチを当てて、またTowns OS V2.1以降のRUN386.EXEでも実行できるように変更するパッチに成功。なお、どうやらV1.1のRUN386.EXEとV2.1のRUN386.EXEの違いは、V1.1のRUN386.EXEはUMB (640KB-1MB)のメモリ空間が使用中でも無視して破壊してしまうのに対してV2.1のRUN386.EXEは使用中のUMB領域は回避する。また、PUYOPUYOは起動するもののBGMが鳴らなかったので、これもパッチを当ててBGMを鳴らすことに成功した。パッチはここ。
少なくとも、RayxanberはAH=50H, CH=FFH (32bit物理アドレス空間への直接読み込み)を使っていることが判明。内蔵CD-ROM BIOSはまず間違いなくDMAを使ってこの機能を実現している。Real Modeのドライバではこれをエミュレートすることは不可能。仕方ないので、Unreal Mode (https://wiki.osdev.org/Unreal_Mode) というのを使う方法を検討している。この例題のアセンブリを参考にしながら、テストコードを書いてみた。
とりあえず、単独のEXEとして実行してハイメモリにデータを転送することには成功した。プロテクテドモードってこんなに簡単に入れるもんだったのか。何かもっと大げさな儀式が必要なのかと思ってた。
ただ、問題は、果たしてDOS ExtenderからINT 93Hでリアルモードに降りて、ハンドラの中で一瞬だけプロテクテドモードに入って、またリアルモードからDOS Extenderに処理を戻すようなことをして果たして何かが壊れないのか? それは引き続き研究中。
これは難しかったけど、解決。詳しくはこっちの更新情報に書いた。
Wing CommanderがどうしてもBGM鳴らない。一瞬鳴って、成功したかと思ったらCDSD.SYSとCDISC2.SYS環境でやってた。だから、実現はできるんだよな。何かがうまくエミュレートできてない。
V2.1 L20から実行したところ、タイトル画面でAH=50H (Start Play)コマンドが入ってきているが、開始時間、終了時間がどちらも00:00:00。V2.1 L31から実行したところ、そもそもタイトル画面ではAH=50Hコマンドが入ってきてない。バーに行ってMキーを押したらコマンドは入ってくるけど、00:00:00。
可能性としては、
(1) AH=54H (Read TOC)がうまくデータをProtected Modeに引き渡してない。
(2) Playする段階で曲番号が間違ってる。
のどちらかだと思う。まず、TOCが取れていないかどうか確認するには、CDRFRB.Hの int cdr_cdinfo() の直後または途中でメモリダンプを取ってみればいいのだが、実機でそんなことは簡単ではない。くっ、386DEBUG.EXEさえあれば。
あの、ここ読んでる人で誰かFM TOWNS版の386DEBUG.EXE持ってる人がいたらよかったら連絡ください。
AH=54H (Read TOC)は少なくともDS:DI~にTOCを書き込んでいることま確認した。が、この情報がDOS Extender経由でWing Commanderに渡っているかどうか。
ただ、int cdr_cdinfo() でCD-ROM BIOSから受け取った情報を展開する箇所、またさらに int cdr_cdinfo() 後に処理してる箇所は特定できたんで、そのへんでレジスタダンプが取れるだけでも何かわかるかもしれない。そういうわけで、INT 77HでRS232Cにレジスタダンプを送信するドライバを書いている。Wing CommanderのバイナリはどうやらCで書かれているようで、割とコードを挟む隙間がありそうだ。
また、下位1MBだけでもメモリダンプが取れればありがたいので、INT 68Hでメモリダンプをファイルに書き込むドライバも書いてみるつもり。これで、Wing Commanderのバイナリに細工してデバッグ(?)するしかないな。
ああ、objdumpが逆アセンブルしてくれるのはありがたいけどGAS表記だからめちゃくちゃ読みにくい。もう少しいい逆アセンブラ欲しい。
Towns OS V1.1 L30の起動フロッピーディスク作成にも成功した。Towns OS V1.1のCONTROL.EXえはI/O 3C32Hは使っていないらしく、DOSのDefault Driveを参照してRUN386.EXEやTMENU.EXPの位置を特定していた。その箇所は下。
26c: b4 19 mov $0x19,%ah Get Current Default Drive 26e: cd 21 int $0x21 270: a2 cb 00 mov %al,0xcb Remember default drive in 0xCb, DS base is offset 0x0A50 -> 0x0A50+0xC4=0xB1B 273: a2 c4 00 mov %al,0xc4 Remember default drive in 0xC4, DS base is offset 0x0A50 -> 0x0A50+0xC4=0xB14 276: 04 41 add $0x41,%al AL=AL+'A' 278: a2 31 00 mov %al,0x31 DS base is offset 0x0A50 0x0A50+0x31=0x0A81. Drive letter for TMENU.EXP 27b: a2 16 00 mov %al,0x16 DS base is offset 0x0A50 0x0A50+0x16=0x0A66. Drive letter for RUN386.EXE 27e: a2 3e 00 mov %al,0x3e DS base is offset 0x0A50 0x0A50+0x3E=0x0A8E. Drive letter for T_OAK2.EXE 281: 3c 51 cmp $0x51,%al 'Q' drive? 283: 75 0a jne 0x28f Jump if not Q drive. 285: c6 06 c4 00 11 movb $0x11,0xc4 0x0A50+0xC4=0xB14 (0x11 is 'Q'-'A') 28a: c6 06 16 00 52 movb $0x52,0x16 Use R drive for RUN386.EXE
だから、I/Oの3C32Hの書き換えではなく、DOSのINT 21H AH=0EHを使ってTownsOSのドライブを設定する必要があった。が、これをYSSCSICD.SYSの中からやってしまうと、AUTOEXEC.BATに届く前にデフォルトドライブが変わってしまってDOSがQドライブのAUTOEXEC.BATを読みに行くが、MSCDEX.EXEがまだインストールされてないからQドライブが読めないという状況に陥るかもしれない。
そこで、SYSDRV.EXEにINT 21H AH=0EHを呼ぶ機能を追加して、AUTOEXEC.BATの先頭で呼ぶことでTownsOS V1.1 L30も起動できるようになった。おそらくこれでWing Commanderその他Towns OS V1.1ベースのゲームは起動できるようになると思うんだけど。
なお、II MXだとFASTモードになってるとマウスが飛びまくって現実問題として操作できない模様。
ページ分割。
リダイレクタYSSCSICD.SYSとシステムドライブ変更プログラムSYSDRV.EXE
昨日の段階で、フロッピーディスクから起動してSCSI CD-ROMドライブにあるTowns System Software V2.1 L20をスタートする起動ディスクを作成することに成功していたが、動作が完全ではなかった。
例えば、コマンドモードに降りて、TownsMENUに戻ろうとすると、TownsOSのディスクが入っていないと言われた。また、AUTOEXEC.BATの先頭付近でMSCDEX.EXEでQドライブを使用可能にして、以後はCD-ROM上のファイルで起動を続行したいのだが、MSCDEX.EXEをSYSINIT.EXE以降に置かないと、「TownsOSのためのメモリが足りません」というメッセージが出て、V2.1 L20だと起動を続行してくれるものの音声メッセージが使えないと言われ、V2.1 L31だと起動に失敗して再起動してしまった。
原因はほぼ見当がついていた。FM TOWNSは起動すると、起動ドライブをどこかに記憶している。なにかにつけてファイルを起動ドライブに探しに行く。その情報をどこに記憶しているのか解明して、例えばフロッピーディスクから起動してもQドライブから起動したことにしてしまえば、おそらくこの問題は解決するはずと思われた。
問題は、どこに起動ドライブを記憶してあるのかわからない。FM TOWNSを現役で使っていた当時、さんざん探したのだが見つけることができなかった。ただ、今回は、前よりツールが揃っていた。当時は良い8086逆アセンブラが無くて困ったのだが、今はLinuxにバイナリを持って行ってobjdumpコマンドで逆アセンブルが取れる。もうひとつ最近発見したのは、386|DOS Extender SDK付属の386DEBUG.EXEはFM TOWNSで実行できないが、DOSBOX上でデバッグ開始まではできる。これを利用して、エントリアドレスを特定できる。
黙っているとCONTROL.EXEが起動ドライブにあるTMENU.EXPを実行するわけだから、CONTROL.EXEの中のどこかで起動ドライブを検出しているはず。objdumpでCONTROL.EXEの逆アセンブルを取ってみた。
開始アドレスから逆アセンブルを読んでいって、気になったのがオフセット0B00H。ALをDS:[1016H]から読み込んで、12AB:0000にFAR CALLして、戻ってきたALをDS:[1016H]に戻している。さらに気になったのが、その後ALに41H (Aのキャラクターコード)を加えて複数個所にストアしていた点。もしも、DS:[1016H]が起動ドライブだとするといろいろ説明がつく。
では、このDS:[1016H]の値がどこから来ているかというと、オフセット AFA: IN AL,DX。果たして、起動ドライブはI/Oポートに隠れていた。直前を見ればI/Oポートの3C32Hに書いてあるとわかった。
aee: 07 pop %es aef: 1f pop %ds af0: fc cld af1: 6a 01 push $0x1 af3: e8 ac 0e call 0x19a2 (17A2 in debugger) af6: 5a pop %dx af7: ba 32 3c mov $0x3c32,%dx I/O Unpublished afa: ec in (%dx),%al afb: c0 e8 02 shr $0x2,%al afe: 24 1f and $0x1f,%al b00: a2 16 10 mov %al,0x1016 DS:[0x1016] Base Drive, Probably. b03: 9a 00 00 80 01 lcall $0x180,$0x0 12AB:0000 runtime b08: a0 16 10 mov 0x1016,%al b0b: a2 0f 10 mov %al,0x100f b0e: 04 41 add $0x41,%al AL=AL+'A' ! Drive Letter! b10: a2 fe 0d mov %al,0xdfe Drive Letter for "TMENU.EXP" (File offset 0xFFE) b13: a2 95 0e mov %al,0xe95 Drive Letter for "\HCOPY\DELDRV.EXP" (File offset 0x1095) b16: a2 b4 0e mov %al,0xeb4 b19: a2 c8 0e mov %al,0xec8 b1c: a2 e0 0e mov %al,0xee0 b1f: a2 fa 0e mov %al,0xefa b22: a2 0e 0f mov %al,0xf0e b25: a2 17 0d mov %al,0xd17 Drive Letter for "RUN386.EXE" b28: a2 27 0d mov %al,0xd27 Drive Letter for "RUN386.EXE" b2b: a2 b7 0d mov %al,0xdb7 b2e: a2 ce 0d mov %al,0xdce b31: a2 e4 0d mov %al,0xde4
その後、12AB:0000がオフセットのどこに対応するのか386DEBUGを使って特定。
12AB:0000 1A00: e8 0d 00 CALL 0x1A10 Console Clear Screen 1A03: e8 0f 00 CALL 0x1A15 1A06: e8 1e 00 CALL 0x1A27 1A09: e8 1f 01 CALL 0x1B2B Command Parameters 1A0C: e8 e5 00 CALL 0x1AF4 Calling CD-ROM BIOS Set Idle Auto-Stop Time 1a0f: cb lret
コマンドパラメータを認識している関数が1B2Bと特定した。そして、CONTROL.EXEの/Dオプションを使うと、I/Oの3C32Hを上書きできることが判明。
'D' option 1ba0: e8 4b 00 call 0x1bee 1ba3: 74 46 je 0x1beb 1ba5: 3c 41 cmp $0x41,%al 1ba7: 7c 92 jl 0x1b3b If AL<'A' ignore. 1ba9: 3c 5a cmp $0x5a,%al 1bab: 7f 8e jg 0x1b3b IF AL>'Z' ignore. 1bad: 2c 41 sub $0x41,%al 1baf: 26 a2 16 10 mov %al,%es:0x1016 1bb3: 26 a2 0f 10 mov %al,%es:0x100f 1bb7: ba 32 3c mov $0x3c32,%dx 1bba: 8a e0 mov %al,%ah 1bbc: c0 e4 02 shl $0x2,%ah 1bbf: ec in (%dx),%al Read from I/O 0x3c32 1bc0: 80 e4 7c and $0x7c,%ah Leave Bit2 to Bit6 1bc3: 24 83 and $0x83,%al Leave Bit7, Bit1, and Bit0 1bc5: 0a c4 or %ah,%al 1bc7: ee out %al,(%dx) Write to I/O [0x3c32] 1bc8: e9 70 ff jmp 0x1b3b
CONTROL.EXEに/D:Qオプションを追加することで、フロッピーディスクから起動して、SCSI CD-ROMドライブのTowns System Softwareを起動することに成功したのが昨日。
ところが、MSCDEX.EXEをAUTOEXEC.BATの先頭付近に置いて、その先の初期化をCD-ROMドライブのファイルで続けようとすると、「TownsOSのためのメモリが足りません」と言われる。このメッセージを出していたのはSYSINIT.EXE。僕はメッセージを真に受けて、MSCDEX.EXEがあらぬ場所のメモリを確保してしまって、下位1MBの空間に連続したメモリ領域を確保できなくなっているのかと思った。
ところが、それはぜんぜん違った。同じ方法を使って、SYSINIT.EXEの逆アセンブルを読んだのだが、メモリの確保に失敗してそうな場所がわからない。いや、わからないのはハイメモリアクセスの部分がはっきりしなかったからで、どうやらUnreal Modeというのを使ってるようだった。
仕方がないので絨毯爆撃。INT 21Hを発生させている箇所を一通り探した。その結果、どうやらデバッグメッセージを表示していると思われる箇所を特定。
PUTC a12: 52 push %dx a13: 50 push %ax a14: 8a e2 mov %dl,%ah a16: ba 34 3a mov $0x3a34,%dx IO[3A34]? a19: ec in (%dx),%al a1a: 3c 52 cmp $0x52,%al 'R' a1c: 75 0e jne 0xa2c a1e: 83 c2 02 add $0x2,%dx IO[3A36]? a21: ec in (%dx),%al a22: 3c 41 cmp $0x41,%al 'A' a24: 75 06 jne 0xa2c a26: 8a d4 mov %ah,%dl a28: b4 02 mov $0x2,%ah Display Output a2a: cd 21 int $0x21 a2c: 58 pop %ax a2d: 5a pop %dx a2e: c3 ret PRINT DEBUG STRING from DX a2f: 56 push %si a30: 8b f2 mov %dx,%si a32: b4 02 mov $0x2,%ah a34: ac lods %ds:(%si),%al a35: 84 c0 test %al,%al a37: 74 07 je 0xa40 a39: 8a d0 mov %al,%dl a3b: e8 d4 ff call 0xa12 a3e: eb f4 jmp 0xa34 a40: 5e pop %si a41: c3 ret
だが、SYSINIT.EXEは何も表示しない。どうやら、I/Oの[3A34H][3A36H]が'R' 'A'だった場合のみメッセージを表示するらしい。このメッセージを表示させてみたい。そのためにSYSINIT.EXEに細工をした。オフセット A1D, A25にゼロを書き込んだバージョンを作って、起動してみた。
すると、なんとカレントドライブをQにして\SYSINIT\SYSINIT.EXEを呼んでも、データファイルをAドライブに探しに行っている。
SYSINIT.EXEが実行されるのはCONTROL.EXEよりも手前だから、CONTROL.EXEが起動ドライブを更新したのでは遅すぎる。なお、その後[3A34H]と[3A36H]はR/Wということがわかったので、SYSINIT.EXEに細工しなくてもデバッグ情報を表示させるSYSINIW.EXEを作った。AUTOEXEC.BATの先頭で実行しておくとSYSINIT.EXEがデバッグ情報を表示してくれる。
ということは、SYSINIT.EXEの前に起動ドライブをQドライブだったことにしてしまえばいい。それは、I/O 3C32Hを書き換えてしまえばいい。というわけで、SYSDRV.EXEを作った。どっちもGitHubからダウンロードできる。
そして、AUTOEXEC.BATの先頭でSYSDRV.EXEを実行したところ、フロッピーディスクから起動してSCSI CD-ROMドライブにあるTowns System V2.1 L20を完全に動作する状態で開始することに成功した。起動ディスク作成手順はここ。
FM TOWNS内蔵CD-ROM→SCSI CD-ROM BIOSリダイレクタ Ver. 0.5
いろいろ書かなきゃならないことが多いけど、とりあえず、公開だけ先にした。
https://github.com/captainys/FM
コンパイルしたYSSCSICD.SYSもアップロードしちゃった。20日でここまで来れるとは夢にも思わなかった。ものすごい分量の逆アセンブルを読んだ気がする。
簡単に説明すると、このドライバを使うことで、内蔵CD-ROMドライブが死んでしまったFM TOWNSを外付けSCSI CD-ROMドライブを使ってある程度救済できる(はず)。それはすでにCDSD.SYSとCDISC2.SYSで結構できていたが、起動ディスクイメージを公開しようと思ったときCDSD.SYSはバイナリの再配布禁止になってるし、許可をお願いしたくても作者に連絡つかないしという煮え切らない状態になっていた。使い方は、CDSD.SYSとCDISC2.SYSを、こんな感じでロードしていたところを:
DEVICE=CDSD.SYS ....
DEVICE=CDISC2.SYS ....
簡単に、
DEVICE=YSSCSICD.SYS
に置き換えるだけでOK。SCSI IDは勝手にスキャンするから指定の必要もなし。これで、リダイレクタがFM TOWNSのCD-ROM BIOSコールをSCSI BIOSコールに変換するから、ブートフロッピーとか使ってCD-ROMのゲームなどを起動できるようになる。結構いけるんじゃないかな。Afterburner IIとSplatter Houseだけ確認したけど。
CDSD.SYS + CDISC2.SYSだとCD入れ替えるたびに再起動しなきゃならんかったけど、YSSCSICD.SYSだとCDを入れ替えたときもきちんと判定できるようになっている。それと、数日前に発見したCDISC2.SYSの未対応機能もYSSCSICD.SYSでは少なくともエラーを返さないようにすることでSplatter Houseは起動できるようにした。
CDISC2.SYSは非常に複雑なことをやっている。ひとつはMSCDEX.EXEからのコマンドをCDSD.SYSにリダイレクトすること。もうひとつは、TOWNSのCD-ROM BIOSの呼び出しをMSCDEX.EXEの形式に変換してCDSD.SYSに送り、それを再度CD-ROM BIOSの形式に変換して戻す。だから間に二段階挟まっていた。今回作ったYSSCSICD.SYSはもっとシンプルにした。MSCDEXがドライバTOWNS_CDにコマンドを送ると、TOWNS_CDはCD-ROM BIOSにコマンドを発行する。YSSCSICD.SYSはそのコマンドを迎撃してSCSI BIOSを呼び出し、結果を返す。
副産物で、FM TOWNSを現役で使ってた時長いこと疑問に思ってた、起動ドライブはどこに書いてあるのか問題も判明。これはCONTROL.EXEの逆アセンブルを解析して見つけたんだけど、起動ドライブは、I/Oのアドレス0x3c32に書いてあって、Read/Write可。起動ドライブはそのアドレスのビット2~6に書いてあるようだ。ビット7、ビット1、ビット0にも意味があるようで、CONTROL.EXEで/Dオプションで変更すると、ビット2~6だけ更新していることもわかった。
うまくやれば多分起動フロッピーディスク+TownsOSで普通にTownsOSを起動できると思う。エミュレータ上だとメニューまで出るけどアイコンが表示されなくてクラッシュしちゃうんだけど、実機だと動くかどうか、明日以降試してみよう。Wing CommanderがTowns MENUから起動する形式だからできればこの手を使えるようにしたいところ。
CDSD.SYSの作者さんとCDISC2.SYSの作者さんにはソースコードを公開していただいて本当に助かった。
CDSD.SYS, CDISC2.SYS を代替するドライバ、とりあえずDIRができて、コマンドからCDDA再生、ポーズ、ポーズ解除ができるようになった。未公開機能のAH=61H, 0FHでひっかかるのではないか、と、心配だったうえに、最初ばっちり0FH後にクラッシュしていたのでやっぱりダメかと思ったら、実はクラッシュの原因はその直前のAH=53HのPlay Stateの取得でひっかかっていた。
AH=53Hでは、CD再生中の場合AL=1で、DS:DIからの10バイトのバッファに再生情報を返す。が、再生中で無い場合、AL=0で、10バイトのバッファの内容は変更しないのが正しい。が、自分のドライバは再生中では無い場合10バイトのバッファの中身を破壊してしまっていた。このため、クラッシュしていたようだ。この部分を修正したら普通にDIRでファイル名を見ることができ、とりあえず RUN386 CD_PLAY.EXP でTowns OS標準CD Playerの起動にまで成功した。
ただし、ステートが正しく取れないのと、CDDA再生中止ができない。SCSI2のコマンド Read Sub Channel (42H)を使って再生状態を取得すると一時停止中はPlay State=12Hが返るはずだが、再生中を表す11Hが返ってきているようだ。CDISC2.SYSのソースを見ると、CD PAUSEは内部で状態をキャッシュしているようだ。ひょっとすると本質的にPAUSE状態は知ることができないのかもしれない。
また、SCSIコマンドのSTOP PLAY/SCAN (4EH)は、どうやらSCSI3のコマンドのようで、SCSI2のCDドライブでは対応していないようだ。強制的にSeekコマンドを発行して止めることはできるが、止めるたびにトラック0にヘッドを戻したくない。例えば、ある曲の再生を中止して、数トラック先にある曲を再生したいとき、ヘッドをいったん戻してから新しい曲のトラックまでヘッドを移動させるのは遠回りだ。だから、できればSeekやReadで止めるのは避けたい。
とりあえず一通りができたらここでソースとバイナリを公開したいんだけど。
ということで、CDISC2.SYSを参考に、INT 93H, AL=C0Hの呼び出しを監視するプログラムをMASMで書いてみた。ドライバはTOWNS_CDをそのまま使うことにして。呼び出しがあるたびにレジスタの値をコンソールに出すようにしてみたところ、隠し機能 AH=61Hをばっちり使ってる。多分タイミング的にリセットとかそんな感じだと思うんだけどな。ただ、成功だったらAH=0, CX=0で返っていくようなので、必ず成功したことにしたらどうだろう?と、思って、試しにそのようにしたところ何事もなかったかのように動いているので、多分これはそれで良さそう。
あとは、ここで使ってる機能を一通りSCSI BIOS (INT 93H, AH=0FDH)にリダイレクトすれば、なんとかなりそうだな。
とりあえず、実機で INT 93H, AH=0FDH をいろいろいじくってみて、とりあえず INQURY と READ_TOC で意味のある結果を得ることができた。
当初、CDISC2.SYSに簡単な修正を加えることで、CDSD.SYSをバイパスできるのではないか、と思ったのだが謎が発生。CDISC2.SYSは、どうやらふたつのリダイレクションをやってる。
なんでMSCDEX.EXEからの呼び出しに対応する必要があるのかよくわからん。単に故障した内蔵CDドライブの代わりに外付けSCSIドライブを使いたいだけだったら、INT 93H, AL=0C0Hのリダイレクトだけでいいはずだ。TOWNS_CDドライバがMSCDEX.EXEの呼び出しを受けて INT 93H, AL=0C0H を発行するはずだから。
結構高い可能性としてCDSD.SYSが内部でかなりいろいろキャッシュしてる影響で、CDISC2.SYSは機能単体の呼び出しができなかったのではないか?だとすれば、もっと小さなドライバでリダイレクトできるようになりそうな気がする。反面CDISC2.SYS、CDSD.SYSは参考にするとしてゼロからドライバ書かなきゃなんないな。
CDISC2.SYSを修正してSplatter House、飛翔鮫の外付けSCSIドライブからの起動の成功は結構自分としては希望が出る成果だった。本当はここにじゃんじゃん起動用フロッピーディスクイメージをアップロードしたいのだが、CDSD.SYSがバイナリ再配布禁止というのがそれを阻んでいる。作者に連絡を取る手段もわからない。SNSでも使えば見つかるのかね。僕はFaceBookアカウントはあるけど見てないし、Twitterはアカウントすら持ってないし。
そうではなく、CDISC2.SYSを改造してCDSD.SYSに依存せずにSCSIドライブにアクセスできるようにできないかを検討している。CDSD.SYSはソースは公開されているので、それと同じ機能を実現すればいい。調べたところ、FM-R/TOWNSではSCSI BIOS、INT 93Hの隠し機能 AH=0FDH を使っている。なぜか手元に、
インターブログ編, 富士通FMRシリーズ徹底解析マニュアル 増補改訂版, BNN, 1989
が奇跡的に残っているのだが、その中のINT 93Hの説明にはAH=0FDHは載っていない。FM TOWNSテクニカルガイドブックにも載っていない。CDSD.SYSのソースのコメントには "Direct Command Function" とある。さらにソースから推測できたのは、
INT 93H, 0FDH | |
AH | コマンドコード 0FDH |
DS:DI | 構造体へのポインタ |
CH | LUN (Unit number) SCSI IDとも違うらしい。ただ、CDISC2.SYSからは必ずゼロになってるような。 |
CL | 00H |
AL | 下位4ビット: SCSI ID 上位4ビット: 1011 (SCSI device) |
DS:DIの構造体 | |
Word Ptr [DS:DI] | 読み込みデータ格納先アドレス |
Word Ptr [DS:DI+2] | 読み込みデータ格納先セグメント |
[DS:DI+4~] | SCSIコマンド (最大12バイト) |
Return | |
CH | エラーコード 0:正常終了 2:パラメータエラー(?) 4:不明エラー |
AX | 値は返ってくるけど意味は不明 |
CDSD.SYSのソースでは、INT 93Hに続いて、
int 93h
xchg al,ah
or ah,ah
jz ExPhase2
stc
ExPhase2:
このようになっているので、INT 93H復帰後のALをエラーコードとしているが、実際使ってみたところCHがエラーコードである可能性が高い。どうやらここもFM-R/TOWNS版CDSD.SYSのバグ。
試しにエミュレータ上でハードディスクに対して INT 93H, AH=0FDHを実行したところ、未接続のSCSI IDに対してはCH=02、ハードディスクイメージが接続してあるSCSI IDに対してはCH=0が返ってきた。どちらの場合もAX=0。
また、どうやらINT 93H, AH=0FDHは読み込んだバイト数がわからないらしい。最初AXがそれではないかと思ったが、INQUIRYに対してAX=0で戻ってくるからそうではないようだ。読み込みデータ格納先アドレスの先頭にバイト数が書いてあったりしないか、とも思ったがハードディスクに対してINQUIRYの結果が最初2倍と0だったからそれも違った。
一応、UNZ.EXEのダンプからINT 93H, AH=0FDHに対応するBIOSの逆アセンブルを読んだところ、この機能はDMAを使ってSCSIデバイスからデータを読み込んでいるらしいことがわかった。DMA使ってるのに終わるまで待つ。また、SCSIコマンドを発行しているらしい箇所も特定できたのだが、どこからもその場所を呼んでいるように見えない。それが、これなんだけど:
567b: 8b 1e 1c 05 mov 0x51c,%bx 0x051c=Counter. 567f: 8b 14 mov (%si),%dx -> AH=0FDH, SCSI Disk -> DX=0x0C30 IO[0x0C30]=SCSI Data Register RedBook pp. 262 5681: 1e push %ds 5682: 8b 3e 12 05 mov 0x512,%di Pointer to SCSI command array 5686: 8e 1e 14 05 mov 0x514,%ds Pointer to SCSI command array 568a: 8a 01 mov (%bx,%di),%al 568c: ee out %al,(%dx) 568d: 1f pop %ds 568e: ff 06 1c 05 incw 0x51c 5692: c3 ret
SCSIデバイスからのData Requestに対するIRQハンドラとして呼ばれているのかと思ったけど、だとするとどこでそれを設定しているのかまで見つけられなかった。SCSIコマンドはコマンドによってバイト数が違うらしいから、どこで何バイト書き込むか決めてるのか特定したかったんだけど、まだそこまではできてない。
また、処理終了後、AX, CHになんらかの値を返すことがわかったのは逆アセンブルを読んだからなのだが、CHがエラーコードであることは間違いないが、AXの意味が今のところ不明。ただ、CDSD.SYSはAXを使ってないというか間違ってALを使ってるだけだからCDISC2.SYSから直接SCSI CDROMにコンタクトできるようにする目的には関係なさそう。
ちなみに、このSCSI BIOSの中で、タイマーBIOS (INT 97H) の未公開機能 AH=10H, AH=11Hを使ってるんだよね。インターバルタイマー機能っぽいんだけど。これもまだ未解明。
CDISC2.SYS修正版!
とりあえず、修正版のダウンロードはこのリンク!
内蔵CDドライブが死んだFM TOWNSが残っていたら、捨てるの待った!
起動用フロッピーディスクかハードディスク(エミュレータでも可)が必要だけど、ここにCDSD.SYSとCDISC2.SYSを使って外付けSCSI CDドライブを内蔵ドライブの代わりに使う方法を書いた。が、それでは動かないタイトルが続出だった。どうやら、その原因はCDISC2がCD自動停止時間の指定をエミュレートしていなかったかららしい。
AH=052H, CH=0FFHの場合で、エラーを返すもんだからSplatter Houseは以下の部分で無限ループに陥っていた。ちなみにDOSエミュレータを使えば元祖の386DEBUGである程度までTowns用.EXPをトレースできることが判明。グラフィックス使い始めたらもう終わりだけど。
15334B MOV AX,052C0H MOV CX,0FF00H INT 93H CMP AH,00 JNE 15334B
これに対するCDISC2.SYSの修正は簡単で、
CDStop proc near ; YS This function (CH==0FFH) does not exist in SCSI-2 command ; YS But, at least it shouldn't return an error. ; AH=52H, CH=0FFH means set sleep timer. cmp ch,0ffh jne @f mov ah, 0 ret @@:
この修正で、Splatterhouseと飛翔鮫が起動してゲーム開始まで可能なことを確認した。時間の都合でこのふたつしか試してないけど、この修正で他にも動作するタイトルが増えたのではないだろうか。なお、飛翔鮫は専用の起動フロッピーまたは起動パーティションが必要。うーん、起動フロッピーの作り方ってどこかに書いたっけ?早い話が、
というようなものを作ればいいんだけど。ハードディスクにブートパーティションを作って、それぞれのゲーム用のBIOSやデバイスドライバをディレクトリにわけてコピーしておいてCONFIG.SYSとAUTOEXEC.BATだけ書き換えてそのドライブにREIPLするという方法も使えるというか僕はそうやってる。
各ゲームの起動用ディスクイメージを作って公開したいんだけど、CDSD.SYSのライセンスの都合でできないんだが、作者さんに連絡を取って許可を得たいものの、連絡先がREADMEに書いてないからわからない。というかそもそも当時の連絡先がわかっても今はメールアドレスが変わってたりってことがほとんどだし。困ったな。
自分でTOWNSでSCSIいじくる方法を調べてCDISC2.SYSがCDSD.SYSに依存しないようにしてしまうって手もあるんだな。ちょっと考えてみよう。
早速成果が出たのでもう少しCDISC2.SYSで動作するタイトルを増やす作戦を続けてみよう。なんでかWing CommanderでBGMが鳴らないんだけど、I/O直接制御してないと思うんだけどな。
残念なことに、Splatter Houseと飛翔鮫は実機+CDSD.SYS+CDISC2.SYSでは起動しないが、それはCD-ROM BIOSの隠しコマンドを使っているからではないらしいことがわかった。隠しコマンドを使うと無限ループになるトラップを仕掛けたCDISC2.SYSを使ってエミュレータ上で起動してみたが、普通に起動できた。
ということはコピープロテクトのような目的でCD-ROM I/Oを直接使っているか、CDISC2.SYSのエミュレーションが不完全なため起動できていないかのどちらかということになる。
くっ、FM TOWNS用386DEBUGさえあれば。ヤフオクで僕に競り勝ったの誰かしらんけど、本当に使ってるんだろうなあ。ただのコレクションにしてるんだったら恨んでやる。
希望としてはCDISC2.SYSのエミュレーションの問題ならばエミュレーションを改善すればいい。せめてどういうコマンドがCD-ROM BIOSに送られているか調べることができれば。CDISC2.SYSのソースコードがあるんだから、CD-ROM BIOSコールのコマンド、パラメータなど普通にキャプチャできるが、問題はそれをどうやって外部に送り出すか。
はっ、RS232Cがあるじゃない。と、思ったが、CD-ROM BIOS内でさらにRS232C BIOSを呼び出していいもんだろうか?I/Oを直接制御した方が良さそうな気がする。FM TOWNSのRS232CのI/Oはどうなっているのか。。。。。
ただのIntel 8251じゃん。FM-7用RS232Cユーティリティ作るときさんざんいじくったよ。これならわかる。というわけで、MASMでRS232Cポートにデータを送信するプログラムを書いてみた。8251はわかったが、8251用クロックを生成するタイマーの使い方がわからなかったので、結局RS232C BIOSを解析してしまったが、Tera Termに19200bpsでデータを送信するコードをMASMで書いてみた。なんか最近アセンブリプログラミングのカンがどんどん戻ってきてる気がする。が、まったく本職に役に立たない。技術の持ち腐れだ。
FM TOWNS上で実行するとRS232Cポートに19200bps, 1 stop bit, No parity, 8-bit data, で "YS"と送信することに成功した。
次は、CD-ROM BIOSコールが入ってくるたびに内容をRS232Cポートに出力するように仕込んだCDISC2.SYSを作って実機上でSplatter Houseとか実行してどこで止まってるか調べればいいんだな。
FM TOWNS 2FのROMも抜き出してCD-ROM BIOSのコードを逆アセンブルしてみたら、なんとMXと同じだ、と、思ったらCD-ROM BIOSはTowns OSのIO.SYS側にあったのか!起動に必要だからCD-ROM BIOSはROMからメモリに展開されてそれからブートするもんだと思ってた。それだとTowns OSによってCD-ROM BIOSが違う可能性があるんだな。
これだとロジックアナライザの解析を始める前にブートROMを解析しておいた方がよさそうだな。最初のやりとりはデータだけとわかってるし、IO.SYSの先頭部分が読み込まれるはずだから、どういうバイトデータが飛び交うか取れそうな気がする。
この先どういう流れで進めようかな。とりあえずさしあたりはCDISC2.SYSとCDSD.SYSで起動できるタイトルを増やすというのが直近の目標だから、そのためには、動かないタイトルを各個撃破するかな。Demosplashで展開したとき現状で動くタイトルを調べてきたんだけど、
そういえば現役でFM TOWNS使ってた頃、Afterburner III買わなかった。あれ、普通にG-LOCって言って売ってくれてれば買ったと思うんだが、またAfterburnerのネームバリューを使ってソフトを売ろうなんて、そんなけしからんソフト買うか、と、思って買わなかった。後になってあれって実はG-LOCって聞いて買えば良かったと思ったんだけど。ヤフオクで安く売ってたから買ったけど。
あとWing Commanderも起動したけどBGMが鳴らない現象だった気がする。
他にも試すつもりが案外時間なかったな。I/Oを直接攻撃しているタイトルの場合はどうせ改造が必要だけどそうでない場合はCDISC2のエミュレーションのレベルを上げれば対応できると思うんだよな。とくに、コマンドの16Hを使ってCDDA鳴らそうとしてたりするのがあったら鳴らない。しかも、CD演奏が始まったことをCDが存在することの確認に使っていたりするなら、演奏開始に失敗した場合そのまま起動しない可能性は十分ある。
Demosplash 2019明けで重いFM TOWNS 2F, II MX、FM-7、FM77AV, AV40を会場まで運んで展開して撤収して収納してかなりの疲労状態。
FM TOWNS II MXのCD-ROM BIOSを解析していろいろおもしろいことがわかってきた。仕様として公表されているBIOSコマンドは、
と、これだけなのだが、CD-ROM BIOSの逆アセンブルを見たところ、コマンドFBH~16Hまでがデータ関連コマンド、4FH~62HまでがCDDA関係コマンドとして扱っているようだ。そして、各コマンドに対応するジャンプテーブルが、次の通り。
00005700 02 B5 02 80 FD FF 74 E1 C3 80 3E 29 05
80 75 0C ......t...>)..u.
00005710 C6 06 29 05 82 A0 28 05 B4 11 CD 97 C3 91
14 6B ..)...(........k
00005720 14 91 14 91 14 91 14 83 14 91 14 8A 14 B8 47
0D ..............G.
00005730 8E D8 BE 39 05 8B 54 02 EC A8 80 74 0D 24 70 C0
...9..T....t.$p.
00005740 E8 03 98 8B D8 2E FF 97 0D 15 CB B8 47 0D 8E D8
............G...
00005750 80 3E 29 05 80 75 05 C6 06 29 05 81 CB 00
00 00 .>)..u...)......
00005760
4E 1C 0E 16 0E 16 0E 16
11 16 45 16 8C 16 F4 16 N.........E.....
00005770 03 17 3F 17 0E 16 0E 16 0E 16 0E 16 0E 16 0E
16 ..?.............
00005780 71 17 0E 16 82
17 B8 17 1D 18 40 18 0E 16 0E 16 q.........@.....
00005790
FB 16 43 18
70 18 DE 18 1B 19 6E
19 F0 19 D1 1A ..C.p.....n.....
000057a0 0F 1B 46 1B A3 1B 0E 16 0E 16 0E 16 0E 16 0E
16 ..F.............
000057b0 0E 16 0E 16 C9
1B F9 1B 00 00 00 00 00 68 47 0D .............hG.
000057c0 1F 8A 46 03
BF 58 15 3C 16 7F 04 3C FB 7D 0F 2C ..F..X.<..<.}.,
000057d0 50 BF 84 15 3C
12 7F 04 3C FF 7D 02 33 FF B4 01 P...<..<.}.3...
000057e0 0B FF 74 14 98 8B
D8 8A 46 02 24 0F B4 80 B9 04 ..t.....F.$.....
000057f0 00 3C 01 73 03 E8 19
00 80 FC 80 75 03 89 4E 06 .<.s.......u..N.
-------------------------------------
ライトブルーのバイトとそれに続く黄色のバイトと青のバイトがコマンド00H~16Hに対応している。ピンクのバイトはFBH~FFH。青のバイトとグリーンのバイトが50H~62H。なお、黄色のバイトは4FHにも対応している。このうち、0E 16は機能が割り当てられていなくて単にエラーコードを返す。それ以外はなんらかの機能が割り当てられている。昨日コードFBHで呼び出すと00 00 (0000H)にジャンプすると無限ループになる。多分スタックオーバーフローでクラッシュする。FCHに対応するジャンプ先 4E 1C (1C4EH)は、機能コード00Hと同じ機能だが、ただしALの値をチェックしない。また、黄色のバイトと青のバイトにオーバーラップがあるため、機能コード16Hが50Hと同じ動作をするように見える。また、機能コード4FHが15Hと同じ動作をするようだ。また、57H, 58H, 60H, 61Hにも機能が割り当てられているようだ。
CDISC2.SYSのソースを見ると、公開されているコマンド以外はリダイレクトしていなかった。CDISC2.SYSで外付けディスクで動作しないタイトルは、内蔵CD-ROMのI/Oを直接操作しているか、あるいは、未公開機能を使っているだけかもしれない。未公開機能は実はコピープロテクションなどに関連する機能の可能性がある。あるいは、機能コード16Hまたは4FHを使っているとしてもおそらく動作しない。ひょっとするとCDISC2.SYSでこれら未公開機能に対応してやるだけで動作するタイトルが増えるかもしれない。
機械工学科でまったく権力が無くて広い机など割り当ててもらうことは到底できない僕は実機によるテストができる時間が限られてしまう。しかし、エミュレータのステートファイルに手を加えて、プログラムが未公開機能を使ったことを検出するぐらいはできそうだ。引き続き調査を続行する。
大体FM-7とFM77AVでやりたいことができるようになって、当面の動態保存の目途がついてきたので、フォーカスをFM TOWNSに移そうと思った。手元に一台のFM TOWNS II MXがある。が、その内蔵CDROMドライブは、半端に動作する状態。プレスしたCDからだとかろうじて起動するが、CD-Rはまったく読めない。レーザーが弱くなってるかフォーカスできなくなっているようだ。調べたところ、同様の問題は多発しているようでFM TOWNSの動態保存の障害になっているようだ。
実際、内蔵CDROMドライブを解析しようというプロジェクトも起ったようだが成功してはいないらしい。なぜかそれが海外で発生していて日本では見ないというのが、ゼロ戦を飛行状態に復元して飛ばしているのはなぜかアメリカという現状と重なって見えて、やや寂しい気がする。
手元には実家から回収してきた赤本がある。I/Oはわかる。実験のためにFM TOWNSエミュレータうんづ、MAMEがある程度活用できる。High-Cを使ってCDにコマンドを送るプログラムを書くことも可能だ。また、うんづは内部でコマンドをSCSIコマンドに変換することで内蔵CDROMドライブを再現しているらしい。だとすれば、同じことをハードウェアで実現すれば、実機の内蔵CDROMドライブコネクタに挟んでSCSI CDROMドライブを接続するコンバータを製作することが可能のはずだ。FPGAを使えば実現できるだろう。Arduinoでもなんとかなるかも。
問題は、I/Oポートの先。FM TOWNSの内蔵CDROMドライブは標準規格ができる前に作られたらしく、特殊な仕様となっている。しかも、コントロール用のワンチップマイコンがCPUとCDROMドライブの間に入っている。CPUからI/Oに書き込まれた内容はワンチップマイコンが適当に翻訳してCDROMドライブにコマンドを送り、データを受け取ってI/Oに出す。このワンチップマイコンが何をしているのか、そのコードはわからない。従ってI/Oに読み書きしているBIOSのコードを解析するだけではSCSIコネクタに出てくる信号にどう翻訳されるのかわからない。また、CDROMドライブのピン割り当てもわからない。それは調べなくてはならない。
GNDピンはマザーボードにテスタを当てて調べることができる。+5V, +12Vも多分テスタでわかる。また、制御しているワンチップマイコンを特定できれば、そのピンにつながっているピンがデータピンと見ることができる。
長期目標:
中期目標
短い期目標:
Comments are welcome. Send E-Mail to: |