前回はブロック転送命令にまつわるコマケー話。今回はPUSH/POP命令にまつわるコマケー話です。x86の場合スタックへのデータの退避や復帰にはPUSH、POP命令を使います。PUSH、POP自体は分かり易い命令であるのでコマケー話など無い感じ。しかし、歴史を知らないと何でそんな命令があるの?という御供の命令もあり。
※「ぐだぐだ低レベル プログラミング」投稿順indexはこちら
※実機動作確認(といってもエミュレータなんだけれども)には以下を使用させていただいております。
PUSH、POP命令
今回使用してみるのは以下の命令どもです。
-
- PUSH/POP 汎用レジスタ
- PUSH/POP r/m
- PUSHF
- POPF
- SAHF
- LAHF
上記以外にもセグメントレジスタのPUSH/POP命令が存在しますが、今回はパスです。
PUSH、POPは16ビットモードのx86の場合、かならず16ビット幅の操作となります。バイト幅の操作はありません。スタックへのPUSHは、SP(スタックポインタ)を先に2減じてからSS:SPで指されるメモリに対象データを格納します。スタックからのPOPは、SS:SPで指されるメモリからデータを復帰先に読み出してから、SPに2加えます。
ファーストバイトオペコードマップ(部分)上の該当命令どもの位置を赤枠で示します。
まず、「汎用レジスタ」8本のPUSH、POPは、目抜き通りのいいところ1行まるまる使って優遇されてます。一方0x8FにもPOP、0xFFにもPUSHがありますが、これはメモリ(データセグメントなど)上におかれた変数をスタックにPUSH、POPするために主に使われるコードです。スタックもメモリ上にあるので、これらはメモリーメモリ間の転送命令ということになります。CISCだね。
真ん中の0x9C付近から4個並んでいるのがFlags関係のPUSH/POPに使われる命令どもです。16ビットモードのx86の場合フラグを集めた Flags は16ビット幅のレジスタです。その16ビットレジスタを素のままPUSH/POPするのが以下の2命令です。ここは何の不思議もありませぬ。
-
- PUSHF
- POPF
しかしその後にならぶ
-
- SAHF
- LAHF
命令については、その「歴史的価値」を知らないとなんだこりゃ、な命令でしょう。LAHFは、AH(AXレジスタの上位8ビット)にFlagsレジスタの下位8ビットをロードする命令です。SAHFは、その逆でAHの内容をFlagsの下位8ビットにストアする命令です。なぜこのような命令が必要になったかというと以下のような経緯です。知らんけど。
-
- 最初のx86である8086は、御先祖の8ビット、8080のプログラムをアセンブリ言語レベルで移植可能、という建前のもと作られた
- 8080で条件フラグは、スタック操作時にPSWと呼ばれる「16ビットレジスタ」の上位8ビットに配置されていた。なおPSWの下位8ビットにはアキュムレータであるAレジスタが鎮座していた。
- 8080でPSWをスタックに退避、復帰するためにPUSH PSW、POP PSWという命令が存在した。スタックに退避した後でメモリ上でPSWイメージを操作するようなソフトウエアは当然のように存在した。
- これと辻褄あわせるため、8086の条件フラグのビット配置は8080の下位8ビットに合わせてあった。そして、LAHF命令により8080コンパチのPSWイメージをAXレジスタ上に生成できた(ここでは8086のALレジスタが8080のAレジスタに相当する。)これをPUSH AXすれば、8080のPUSH PSWと同じイメージをスタック上に作れる。逆の操作はPOP AXしてSAHFとなる。
PUSH/POP命令のアセンブリ言語ソース
上記で列挙したPUSH/POP命令を1づつ触ってみるだけのソースです。強力なx86用アセンブラNASM用のソースです。
segment code ..start: mov ax, data mov ds, ax mov ax, stack mov ss, ax mov sp, stacktop mov ax, edata mov es, ax test: mov bx, 0x1234 push bx mov bx, v0 push word [bx] mov ax, 0x98AB stc lahf push ax mov ax, 0xCDEF clc pushf stc popf clc pop ax sahf pop word [bx + 2] pop bx fin: mov ax, 0x4c00 int 0x21 resb 2048 segment data align=16 v0: dw 0x5678 v1: dw 0 resb 1024 * 63 segment edata align=16 resb 1024 * 63 segment stack class=STACK resb 2048 work: dw 8 dup (0) stacktop:
PUSHしてPOPしているだけのコードですが、ちゃんと読み書きできている確認のため、PUSHした後、PUSH元を「破壊」してからPOPするような形にしてあります。
アセンブルして実行
MS-DOS互換で機能強化されたFreeDOS上、以下のステップで上記アセンブラソース push.asm から実行可能なオブジェクトファイルを得て実行することができます(nasmとwatcom Cがインストール済であること。)
nasm -f obj push.asm wlink name push.exe format dos file push.obj debug push.exe
最初は、BXレジスタに0x1234をロードして、スタックへPUSH。SPの値が‐2されていることが分かると思います。
続いて変数v0のオフセット・アドレスをBXにロードし、DS:[BX]で指される変数v0の値0x5678をスタックへPUSH。CISCらしい荒業っす。
STCはキャリーフラグを立てる命令です。実行後、NC表示がCY表示になります。この状態でLAHFを行うとAH(AXの上位8ビット)にFlagsの下位8ビットイメージが転送されます。これをPUSH AX。
続いて、Flagsの内容を変更するために、ことさらにCLC(CYをNCに変える)を行ってPUSHFします。今度は8086の16ビットのFlagsレジスタ全体がスタックに退避されます。退避後STCしてFlagsを改変してます。
さて、今度はPOPFで、先ほどスタックにPUSHしたFlagsの値を復帰してます。POPFの後、CYがNCに戻ることで確かめられます。
つづいて、8080PSWイメージでの復帰を行います。まずPOP AXでスタックから読み出しておいて、SAHFします。するとFlagsの下8ビットにも戻ると。
その後は、スタック上に退避した変数v0を読み出す操作です。ただし、元の変数に戻すのではなくて、お隣のv1なる変数に読み込んでます。
v1へのストアが成功しているので、0番地(v0)と2番地(v1)に同じ値が入ってます。例によってx86はリトルエンディアンなので、0x5678は、メモリ上では小さいアドレスから0x78、0x56の順です。
0x1234に戻ったね。