前回はADD、SUBなどの算術演算命令、AND、ORなどの論理演算命令8種が「ほぼほぼ」以下同文ということを確認しました。でも算術演算といえば加減乗除というくらいで、乗除はどうなってんの?そこで今回は乗算命令MULを見ていきます。「何かと何かを掛ける」命令のハズなのに、アセンブラのオペランドは1個だけ。なして?
※「ぐだぐだ低レベル プログラミング」投稿順indexはこちら
※実機動作確認には以下を使用させていただいております。
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ビット
ということであります。そのため、オペランドのうち、乗数や除数の方のみ指定して、デスティネーション側は「暗黙」の指定です。アセンブラの形式的には、「シングル・オペランド」命令に見えます。
割り当てられている機械語コードのファースト・バイトが以下に。
MUL、IMUL、DIV、IDIVは狭いところに押し込まられていることが分かります。また、NEG、NOT、TESTなどの「シングル・オペランド」系の論理演算命令と同居。ADD、SUBなどと比べると随分と虐げられてる感じです。即値もないし。
でもま、8086/8088がアセンブリ言語レベルの互換性を追求した古の8ビット8080には乗除算など無かったので「とってつけた」感じになるのも致し方ない?あるだけましってか?
MUL命令の命令エンコーディング
4命令の代表ということでMUL命令のエンコーディングを以下に掲げます。
バイトかワードかの判別はファーストバイトの最下位ビットで決まるのは例によってです。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。赤線引いたところが確認すべきところっす。
最初はワード幅の掛け算、MUL CXです。AXとCXを掛けて、結果のロウをAXにハイをDXに格納。ハイ側は0になるので、DXの古い値(0x5555)が破壊されているのが分かりますな。
下側がバイト幅の掛け算、MUL CLです。バイト幅の場合、ALとCLを掛けて、倍幅の結果をAXに格納。平和だな。
最初は、BXが指す先のデータセグメント中のバイトメモリの内容との掛け算。ディスプレースメントなしなので2バイト命令。
2つ目が、SIが指す先のデータセグメント中のワードメモリの内容との掛け算。同じくディスプレースメントなしなので2バイト命令。
最後3つ目が、BPが指す先のスタックセグメント中のワードメモリの内容との掛け算。BPをメモリアドレスを指すベースレジスタに指定するとデフォルトはスタックセグメントになります。また、メモリアドレシングのエンコードにも「特例」が適用されて、ディスプレースメントなしでアセンブリ言語命令を記述しても、「もれなく」バイト幅ディスプレースメント(符号拡張つき)が適用されます。お楽なような束縛がキツイような。。。