ぐだぐだ低レベルプログラミング(183)x86(16bit)、MULのオペランドは1個?

Joseph Halfmoon

前回はADD、SUBなどの算術演算命令、AND、ORなどの論理演算命令8種が「ほぼほぼ」以下同文ということを確認しました。でも算術演算といえば加減乗除というくらいで、乗除はどうなってんの?そこで今回は乗算命令MULを見ていきます。「何かと何かを掛ける」命令のハズなのに、アセンブラのオペランドは1個だけ。なして?

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

※実機動作確認には以下を使用させていただいております。

    •  Windows 11 PC (i5-1235U)
    •  Ubuntu 24.04 LTS on WSL2
    •  QEMU 8.2.2
    •  FreeDOS 1.3
MUL、IMUL、DIV、IDIV

8086/8088レベルでは、4種の整数乗除算が定義されてます。

    • MUL、符号無乗算、バイト、ワード
    • IMUL、符号付乗算、バイト、ワード
    • DIV、符号無除算、バイト、ワード
    • IDIVI、符号付除算、バイト、ワード

なお毎度書いておりますが、x86の場合ワードといったら16ビットです。

乗算の場合、被乗数に乗数をかけてビット幅が倍になった積を得ます。除算の場合は、被除数を除数でわって、ビット幅は同じですが、商と余りの2つの結果を得ます。

前回やったADDやSUBなら、「自由に」オペランドの片割れ、デスティネーション側を指定可能だったのですが、MUL系の命令には選択の余地はありません。

    • バイトxバイトの時は、ALに積のロー側8ビット、AHに積のハイ側8ビット
    • ワードxワードの時は、AXに積のロー側16ビット、DXに積のハイ側16ビット
    • バイト÷バイトの時は、ALに商8ビット、AHに余り8ビット
    • ワード÷ワードの時は、AXに商16ビット、DXに余り16ビット

ということであります。そのため、オペランドのうち、乗数や除数の方のみ指定して、デスティネーション側は「暗黙」の指定です。アセンブラの形式的には、「シングル・オペランド」命令に見えます。

割り当てられている機械語コードのファースト・バイトが以下に。MULopcodeMAP

MUL、IMUL、DIV、IDIVは狭いところに押し込まられていることが分かります。また、NEG、NOT、TESTなどの「シングル・オペランド」系の論理演算命令と同居。ADD、SUBなどと比べると随分と虐げられてる感じです。即値もないし。

でもま、8086/8088がアセンブリ言語レベルの互換性を追求した古の8ビット8080には乗除算など無かったので「とってつけた」感じになるのも致し方ない?あるだけましってか?

MUL命令の命令エンコーディング

4命令の代表ということでMUL命令のエンコーディングを以下に掲げます。MULencoding

バイトかワードかの判別はファーストバイトの最下位ビットで決まるのは例によってです。MULなのかIMULなのか、あるいはDIVかIDIVか(もっと言えばNEGなどか)を決めるのは2バイト目のmodRMバイトのビット5、4、3の3ビット、赤く塗られた部分です。

modRMバイトのエンコーディングにより、乗数、除数が格納されているレジスタやメモリを指定します。メモリアドレシングによってはディスプレースメント(前回はオフセットと記述してしまいましたが、インテル式にディスプレースメントとしました)がバイトもしくはワードで引き続くので、命令長は2バイトから4バイトということになります。毎度のCISCだね。

動作確認用のアセンブリ言語ソース

MUL命令の動作を観察するためのソースが以下に。「FreeDOS推し」のNASMアセンブラ用のソースです。「だいたい」インテル式のソースと似てます(インテル式だと byte でなく byte ptr となったりするけど。)

    org 100h
section .text
start:
    mov sp, stacktop
    mov bx, workb
    mov si, workw
    mov bp, sworkw
    mov word [bp], 0100h
    mov ax, 1234h
    mov cx, 0002h
    mov dx, 5555h
test:
    mul cx
    mov ax, 1234h
    mul     cl
    mov ax, 1234h
    mul byte [bx]
    mov ax, 1234h
    mul word [si]
    mov ax, 1234h
    mul word [bp]
fin:
    mov ax, 0x4c00
    int 0x21
section .data   align=16
workw:  dw  0080h
workb:  db  4

section .bss    align=16
sworkw: resw    4
    resb    2048
stacktop:ど
動作確認

FreeDOS上のdebug(御本家マイクロソフトのdebugの超強化版)で動作確認したものが以下です(実際にはQEMU上でのエミュレーションだけれども。)

まずは、上記ソースのラベル test のところから。レジスタ間のMUL。赤線引いたところが確認すべきところっす。MULreg

最初はワード幅の掛け算、MUL CXです。AXとCXを掛けて、結果のロウをAXにハイをDXに格納。ハイ側は0になるので、DXの古い値(0x5555)が破壊されているのが分かりますな。

下側がバイト幅の掛け算、MUL CLです。バイト幅の場合、ALとCLを掛けて、倍幅の結果をAXに格納。平和だな。

続いてメモリとアキュムレータ(AL、AX)のMULMULmem

最初は、BXが指す先のデータセグメント中のバイトメモリの内容との掛け算。ディスプレースメントなしなので2バイト命令。

2つ目が、SIが指す先のデータセグメント中のワードメモリの内容との掛け算。同じくディスプレースメントなしなので2バイト命令。

最後3つ目が、BPが指す先のスタックセグメント中のワードメモリの内容との掛け算。BPをメモリアドレスを指すベースレジスタに指定するとデフォルトはスタックセグメントになります。また、メモリアドレシングのエンコードにも「特例」が適用されて、ディスプレースメントなしでアセンブリ言語命令を記述しても、「もれなく」バイト幅ディスプレースメント(符号拡張つき)が適用されます。お楽なような束縛がキツイような。。。

ぐだぐだ低レベルプログラミング(182)x86(16bit)、ADD以下同文?微妙に非対称? へ戻る

ぐだぐだ低レベルプログラミング(184)x86(16bit)、シフト、ローテイト(86のね) へ進む