ぐだぐだ低レベルプログラミング(192)x86(16bit)、PUSH/POPにも先祖の痕跡

Joseph Halfmoon

前回はブロック転送命令にまつわるコマケー話。今回はPUSH/POP命令にまつわるコマケー話です。x86の場合スタックへのデータの退避や復帰にはPUSH、POP命令を使います。PUSH、POP自体は分かり易い命令であるのでコマケー話など無い感じ。しかし、歴史を知らないと何でそんな命令があるの?という御供の命令もあり。

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

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

    •  Windows 11 PC (i5-1235U)
    •  Ubuntu 24.04 LTS on WSL2
    •  QEMU 8.2.2
    •  FreeDOS 1.3
PUSH、POP命令

今回使用してみるのは以下の命令どもです。

    1. PUSH/POP 汎用レジスタ
    2. PUSH/POP r/m
    3. PUSHF
    4. POPF
    5. SAHF
    6. LAHF

上記以外にもセグメントレジスタのPUSH/POP命令が存在しますが、今回はパスです。

PUSH、POPは16ビットモードのx86の場合、かならず16ビット幅の操作となります。バイト幅の操作はありません。スタックへのPUSHは、SP(スタックポインタ)を先に2減じてからSS:SPで指されるメモリに対象データを格納します。スタックからのPOPは、SS:SPで指されるメモリからデータを復帰先に読み出してから、SPに2加えます。

ファーストバイトオペコードマップ(部分)上の該当命令どもの位置を赤枠で示します。PUSHPOPOPCODEMAP

まず、「汎用レジスタ」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ビットにストアする命令です。なぜこのような命令が必要になったかというと以下のような経緯です。知らんけど。

    1. 最初のx86である8086は、御先祖の8ビット、8080のプログラムをアセンブリ言語レベルで移植可能、という建前のもと作られた
    2. 8080で条件フラグは、スタック操作時にPSWと呼ばれる「16ビットレジスタ」の上位8ビットに配置されていた。なおPSWの下位8ビットにはアキュムレータであるAレジスタが鎮座していた。
    3. 8080でPSWをスタックに退避、復帰するためにPUSH PSW、POP PSWという命令が存在した。スタックに退避した後でメモリ上でPSWイメージを操作するようなソフトウエアは当然のように存在した。
    4. これと辻褄あわせるため、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

まずは起動直後のディスアセンブル。step00

最初は、BXレジスタに0x1234をロードして、スタックへPUSH。SPの値が‐2されていることが分かると思います。step01

続いて変数v0のオフセット・アドレスをBXにロードし、DS:[BX]で指される変数v0の値0x5678をスタックへPUSH。CISCらしい荒業っす。step02

STCはキャリーフラグを立てる命令です。実行後、NC表示がCY表示になります。この状態でLAHFを行うとAH(AXの上位8ビット)にFlagsの下位8ビットイメージが転送されます。これをPUSH AX。step03

続いて、Flagsの内容を変更するために、ことさらにCLC(CYをNCに変える)を行ってPUSHFします。今度は8086の16ビットのFlagsレジスタ全体がスタックに退避されます。退避後STCしてFlagsを改変してます。step04

さて、今度はPOPFで、先ほどスタックにPUSHしたFlagsの値を復帰してます。POPFの後、CYがNCに戻ることで確かめられます。step05

つづいて、8080PSWイメージでの復帰を行います。まずPOP AXでスタックから読み出しておいて、SAHFします。するとFlagsの下8ビットにも戻ると。step06

その後は、スタック上に退避した変数v0を読み出す操作です。ただし、元の変数に戻すのではなくて、お隣のv1なる変数に読み込んでます。step07

v1へのストアが成功しているので、0番地(v0)と2番地(v1)に同じ値が入ってます。例によってx86はリトルエンディアンなので、0x5678は、メモリ上では小さいアドレスから0x78、0x56の順です。memoryPUSHPOP

最後に退避してあったBXレジスアの値をPOPします。step08

0x1234に戻ったね。

ぐだぐだ低レベルプログラミング(191)x86(16bit)、MOVS、所謂ブロック転送命令 へ戻る

ぐだぐだ低レベルプログラミング(193)x86(16bit)、86のNOPはXCHGの別名? へ進む