ぐだぐだ低レベルプログラミング(235)x86(16bit)、FSAVE、全レジスタセーブ

Joseph Halfmoon

8087FPUの制御命令の説明に前回前々回と2回を費やしました。今回こそ命令を「動かしたい」と思います。しかし、その前に確認しておかないとならないんだな、16ビット実行環境であるFreeDOSから「みえる」FPUの命令がどのレベルのものなのか?「ピュアな」8087ではないけれども、雰囲気は出してる?なんだそれ。

※「ぐだぐだ低レベル プログラミング」投稿順indexはこちら

※実機動作確認(といってもエミュレータなんだけれども)には以下を使用させていただいております。

    •  Windows 11 PC (i5-1235U)
    •  Ubuntu 24.04 LTS on WSL2
    •  QEMU 8.2.2
    •  FreeDOS 1.3

今回は、8087系FPUの state (environmentと全レジスタスタック)をメモリ上にセーブする FSAVE 命令を動かしてその挙動を観察します。しかし第233回でやったとおり、FSAVE命令は「ピュア16ビット環境に適合した8087」と「32ビット仮想記憶対応の80387以降」ではそのセーブする内容(バイト数)が異なります。上の「※印」の註釈で書いている通りの屋上屋を重ねるような環境で、FreeDOS上の実行オブジェクトからはどんなFPUとして見えているのか? それが問題だ(今頃、それに気づいたお惚け老人はお間抜け。)

FreeDOS実行環境から見えるFPU

結論から言えば、FreeDOS(QEMU上のゲストOS)上で実行されるオブジェクトが「実行」できるFPUの命令セットは、

建前8087と互換動作(具体的にはFSAVE命令のメモリレイアウトは8087コンパチ)だけれども、80387以降で追加された命令や定義域も利用可能

という若干中途半端なものとなります。

FreeDOSには CPUSTAT というCPUの諸元を表示してくれるコマンドがあるので、まずそれを使って見てみます(以下はCPUSTAT出力の最初の部分。)CPUSTAT_0EC

CPUベンダ文字列としては、 AuthenticAMDとAMDを主張。そして FPU integratedなので、FPUをふくんでます。そして注目すべきは V86-modeです。ゲストOSであるFreeDOSは仮想86モードで動いているということだと思います。

またCPUSTATの続きを見てみると、CPUSTAT_1_EC

CPUは64ビットのサポートのあるx64に見えるけれども、QEMUの仮想CPUのバージョン2.5+だと。ちゃんとエミュレータであるQEMUが作り出したかりそめの存在だ、と認識されとるみたいです。

ここで80386以降x86の32ビット・プロテクテッドモードで使用可能なV86モードを想定し、FreeDOS上でのFPU命令の処理についてお惚け老人が推測するに、

    1. V86モードでは、直接FPU命令は実行できない(これは元々の仕様ね。)
    2. FPU命令は無効命令例外を引き起こす
    3. QEMUは無効命令例外を捕捉して、ゲストOS(FreeDOS)の実行を中断し、QEMU内部のコードで8087エミュレーションを行ってから制御を戻す
    4. その結果 FreeDOS上では、あたかも8087であるかのような挙動を示す

結局、命令は全てQEMU側で実行されとるのですが、FPUについては普通の整数命令とちょっと処理経路が違うみたい。そして今回環境では「なんちゃって8087(機能的には上よ)」としてFPU命令が実行できているように見えるっと。よってFSAVEでセーブされるバイト数は94バイト(8087式)となる筈。

今回実験のプログラム

今回は以下のような処理を行ってみます。

    1. π(3.14…)をレジスタスタックに8個詰め込む
    2. FSAVE命令で FPUステート(全レジスタ)をメモリに書き出す

なお以下は「強力なx86用アセンブラNASM」用のソースです(MSのMASMともインテルASM86とも微妙に異なるけど、まあ分かるっしょ。)

%use fp

segment code
..start:
    mov ax, data
    mov ds, ax
    mov ax, stack
    mov ss, ax
    mov sp, stacktop
test:
    fninit
    fldpi
    fldpi
    fldpi
    fldpi
    fldpi
    fldpi
    fldpi
    fldpi
    mov bx, dstAREA
    fsave	[bx]
    nop
fin:
    mov ax, 0x4c00
    int 0x21
    resb    2048

segment data    align=16
    resb    1024 * 63
dstAREA: 
    db 256 dup (0)

segment stack   class=STACK align=16
    resb    2048
stacktop:
    dw 1024 dup (0)
stackend:
アセンブルして実行

MS-DOS互換で機能強化されているフリーなFreeDOS上、以下のステップで上記のアセンブラソース fsave.asm から実行可能なオブジェクトファイルを生成して実行することができます(nasmとwatcom Cがインストール済であること。)

なお、FreeDOS付属の「debugx」デバッガは、8087のレジスタ内容を浮動小数で見せてくれます

nasm -f obj -l fsave.lst fsave.asm
wlink name fsave.exe format dos file fsave.obj
debugx fsave.exe

まず逆アセンブルリストから。unasmFsave

FSAVE直前のFPUレジスタの様子が以下に。regBefore

また、念のため、FSAVEのセーブエリアのメモリ領域を確認。memBefore

そしてFSAVE実行後のメモリの様子。memAFTER_EC

色付の枠がついている部分がFPUからセーブされたデータです。先頭からこんな感じ。

7F 03 <- CW
00 00 <- SW
00 00 <- TAG
64 01 <- IPLOW
C6 02 <- IPH + OPCODE
76 6C <- OPLOW
C6 02 <- OPH + 0

その後はスタックトップST(0)からボトムのST(7)まで、同じπを意味する浮動小数値が10バイトx8回くりかえされてます。そしてその合計が94バイトね。

なお、FSAVE後のFPUレジスタの様子が以下に。regAfter

FSAVEすると、レジスタどもは、もれなくFINIT実行後と同じ状態(デフォルト初期値)にクリアされてしまいます。

インテルのマニュアルみても、以下引用のように素っ気ない説明です。

FSAVE/FNSAVE initializes the 8087 as if FINIT/FNINIT has been executed.

ここまで「世話焼いて」くれるのはFPUのコンテキスト・スイッチにかかる時間を可能な限り短縮したかったってことかな?でもFINIT1個くらい大勢に影響ない感じもするんだが。。。

ぐだぐだ低レベルプログラミング(234)x86(16bit)、続、FPU制御命令 へ戻る

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です