前回、x86の16ビット命令セットからADD命令を練習。実はADDが分かれば、ADC、AND、XOR、OR、SBB、SUB、CMPの残り7命令も「ほぼほぼ以下同文」てことで理解できます。でも「ほぼほぼ」って何だ?そこで、この辺でCISCらしいx86のオペコードエンコーディングを復習。ほんとメンドクセーんだが、これ。
※「ぐだぐだ低レベル プログラミング」投稿順indexはこちら
ADD、ADC、AND、XOR、OR、SBB、SUB、CMP
表題にならべました8個の命令は、8086/8088において一族的に扱われております。「ほぼほぼ」同じ機械語命令コードのエンコーディングです。アセンブラ大好きお兄さんお姉さん方には説明の必要もないかとは思いますが機能を列挙します。
-
- ADD、整数加算
- ADC、キャリー付整数加算
- AND、論理積
- XOR、排他的論理和
- OR、論理和
- SBB、ボロー付き整数減算
- SUB、整数減算
- CMP、比較
バイト可変長の8086/8088の機械語において、上記の8命令は、256個しかないファーストバイト(一等地)の51区画を占めています。こんな感じ。以下のHはオペコードバイトの上4ビットのニブル。Lは下4ビットのニブルを意味します。
まずはADD、ADC、AND、XORの核心部分。
つづいてOR、SBB、SUB、CMPの核心部分。
さて、今回は命令の実機演習(QEMU使っているのでシミュレータ上だけれども)をお休みして。これらのCISCらしい命令エンコーディングを味わっていきたいと思います。
ADC命令のエンコードを例に
前回ADDだったので、今回はADCです。ADDとADCは計算のときにキャリー(CYフラグの値)を含めるか否かの違いだけで、エンコーディングは以下同文です。バイト可変長な様子が分かる代表例を作ってみましたぞ。
上記では、オペコードADCがバイト可変長のエンコーディングにより変化する様子を描いてみました。実際にはこれ以外の組み合わせもあるのですが、まあ代表的な様子ということで。左に番号を振ったので、番号順に説明していきます。
1番目のエンコード
1番目は一番ありがちなレジスタ間のADC演算です。演算幅がバイトであるのかワード(x86では16ビット)であるのかはファーストバイトの最下位のwとかかれたビットで判別します。このビットは紫色にしたので、全形式共通であります。
第2バイトの最上位の2ビットはmodフィールドと呼ばれ、第2バイトで指示されるオペランドの形式を示してます。この例のmod=11は、レジスタ間であるという指定です。ビット5,4,3がrrr、ビット2,1,0がRRRとありますがその3ビット2つでレジスタ2つを指定してます。
また、ファーストバイトのビット1はディレクションを示しており、RRRからrrrへなのか、rrrからRRRへなのかを判別してます。RISCのように演算は皆レジスタ間であればこのディレクションビットなどは無用の長物なのですが、x86においてはオペランドの片方がメモリということがあるので、方向指定が役に立ちます。ちなみにmod=00とすると、レジスタ間ではなく、レジスタとレジスタの指すメモリ間の演算に化けます。
ここで、ファーストバイトのビット5,4,3に010というコードが埋め込まれているのを覚えておいてくだされ。このパターンは後のエンコードでも出てくる8種命令の中からADCを識別するためのコードです。
なお、上記の図を見ると直ぐに分かりますが、x86は1バイトのコードを上位から2ビット、3ビット、3ビットの3つのフィールドに分けてデコードするスタイルです。これさえ知っていたらお楽。ホントか?
2番目のエンコード
2番目はレジスタとメモリ間です。演算幅はwビットで、方向はdビットで制御されるのは同じです。違いはmod=01となっているところです。これは演算の片割れがメモリであることを示すとともに、実効アドレス(イフェクティブ・アドレス、EA)の計算にオフセットが含まれることを示してます。オフセットは1バイトであり、符号拡張された上でメモリをポイントしているレジスタ(ベースレジスタ)値に加算されているので、レジスタ前後の+127からー128までのアドレスを指せることになります。
3番目のエンコード
3番目もレジスタとメモリ間です。2番目との違いはオフセットが16ビットワード長のフル幅指定になっていることです。
4番目のエンコード
4番目のエンコードはアキュムレータ(ALまたはAX)優遇の例です。8ビットの御先祖様8080との互換性を大切にした形。ALレジスタに8ビットの即値を演算する専用命令です。これにより後に述べる「一般的」な即値命令より1バイトコードが短くなります。
5番目のエンコード
5番目のエンコードもアキュムレータ優遇です。こちらはAXレジスタに16ビットの即値を演算する専用命令です。
6番目のエンコード
6番目からは「一般的な」即値演算です。まずはバイトレジスタへの8ビット即値の演算。ここで注意すべきなのは、1番目から5番目までのエンコードでファーストバイトに鎮座していた赤の010のパターンが第2バイトに「移っている」ことです。それでも010はADCだと。
7番目のエンコード
7番目は6番目のwビットを1に変更したものです。ワードレジスタへの16ビット即値の演算となります。
8番目のエンコード
8番目はバイト幅のメモリの値と8ビット幅の即値の演算となります。その際メモリアドレスにオフセットを加えるのですが、エンコードされたオフセットは8ビット幅で、それを16ビット幅に符合拡張した上でベースレジスタに加えることになります。即値8ビットそのものはその後に続きます。
9番目のエンコード
9番目もバイト幅のメモリの値に8ビット幅の即値の演算ですが、その際のオフセットは16ビットのフル幅指定となります。
10番目のエンコード
10番目はワード幅のメモリの値にワード幅の即値の演算です。ここでは、オフセットは16ビットのフル幅指定となっています。
まあ、こんな感じで2バイト命令から6バイト命令まで、エンコードされることが分かりました。また、機能的には4番目の形式と同じことを6番目の形式でもエンコードできるなど、ケッコー冗長?なエンコードです。その代わり「ツボにハマればより短い機械語命令」をエンコード可能なように工夫されています。アセンブラのプログラマはその辺分かって命令選べよ、って感じっす。CISCだね~。ただし、BP使ったメモリアクセスのときのオフセット幅の例外とかもあるので、ハンドアセンブルするのはかなりメンドイっす。やらんけど。アセンブラはアセンブラでその辺頑張っているのよ。
「ほぼほぼ」以下同文な理由
最初に、8命令のエンコードは「ほぼほぼ以下同文」といいました。「ほぼほぼ」な理由の部分を再度掲げます。
即値命令のところです。符号拡張しない即値演算命令の方には、8命令が全てそろっていますが、符号拡張する即値演算命令には、AND、OR、XORの論理演算3命令が抜けてます(8086のマニュアル上では。)論理演算に即値の符号拡張などねえべよ、という思し召しだと思います。また、0x82のところは、即値の符号拡張とバイト演算というこれまたナシな組み合わせになるので放置されているもの見えます。8086/8088では一等地のファーストバイトに空き地がぽつぽつとあります。