
前回、32ビット世界突入とほぼほぼ同時に、中の人は手術モード突入。愛してやまない、などと言いながらアセンブラなど吹き飛んでしまいました。ようやく体調戻りつつあり。さっさと練習しないと冗談抜きで「死ぬまでに」x86舐め終わることは難しいっす。でも64ビットのSIMDまで行くとなるととんでもない命令数っす。気が遠くなるよ。
※「ぐだぐだ低レベル プログラミング」投稿順indexはこちら
※実機動作確認には以下を使用させていただいております。
-
- Windows 11 PC (i5-1235U)
- Microsoft (R) Macro Assembler Version 14.33.31630.0
- WinDbg 1.2511.21001.0
「見えない」プリフィックス、OPSとADS
x86にプリフィックスと呼ぶ前置バイトあり。命令のオペコードに先立ってプリフィクスを置くことで動作がいろいろと変わります。皆さまよ~く御存じのところでは、ストリング系命令に付加することで繰り返しを制御するREP系プリフィックス、使用するセグメントを変更するためのセグメント系プリフィックス、バスサイクルにLOCKをかけるためのLOCKプリフィックスなどあるかと。
今回とりあげさせていただくのは、
-
- オペランド・サイズ・プリフィックス、OPS
- アドレス・サイズ・プリフィックス、ADS
の2種のプリフィックスであります。ただし上記2つのプリフィックスをプログラマが明示的に記述することは、まず滅多にない、と申し上げておきます。x86の32ビット世界では頻繁に使われているプリフィックスなのですが、アセンブラの方で勝手に出し入れしてくれているので、意識する必要なし。多分、知らなくてもプログラム書くのに問題もなし。
フツーの32ビットプロセッサ
まずは、x86でない、フツーの32ビット・マイクロプロセッサでの汎用レジスタとそこにバイト長8ビット、ハーフワード長16ビット、ワード長32ビットの数値を格納するときのレイアウトをみてみましょう。
相手がRISCでもCISCでもかまいませぬ。汎用レジスタは8本、16本、32本くらいのどれかであるのがフツー。そしてその中の1本の汎用レジスタの使い方をみるとこんな感じ。
ワード長(32ビット)は、汎用レジスタ(32ビット)の全体を使い、ハーフワード(16ビット)はレジスタのLSB側の16ビットを使い、バイト(8ビット)はレジスタのLSB側の8ビットを使うっと。
単純明快、一目瞭然であります。
x86の「ひねくれた」配置
さて肝心のx86のバイト、ワード(x86では16ビットをワードと呼びます)、ダブルワード(x86では32ビットをダブルワードと呼びます)の格納の仕方が以下に。
x86の場合、「汎用レジスタ」は8本ということになってます。汎用というわりにはレジスタの使用方法にクセがあるけれども、ここではそれは言いますまい。16ビット長のレジスタ名が基本であります。
AX、CX、DX、BX、SP、BP、SI、DI
お気づきの通り、A、B、C、Dではなく、A、C、D、Bと並べるのがx86のタシナミというもんです。16ビット8本ね。これが32ビットレジスタとしては
EAX、ECX、EDX、EBX、ESP、EBP、ESI、EDI
頭にEをつければよろしいと。例えば32ビットレジスタEAXの下16ビットがAX、32ビットレジスタESIの下16ビットがSIです。
ここまでは他の32ビットマシンどもと同等。しかし8ビットとなるとこの関係がガラガラと崩れます。
AL、DL、CL、BL、AH、DH、CH、BH
8ビットとしても8本のレジスタがあるのですが、ALはEAXの下8ビット、これは良いとして、AHはEAXのビット8からビット15です。ALの上ね。AHとALを合わせるとAXになるっと。一方、SP、BP、SI、DIにはバイトアクセスは提供されてません。
このような「ひねくれた」配置になるのは御先祖の8ビットマシンとの互換性を切に考えた初代の8086の御威光であります。
x86の機械語エンコードもひねくれとる
この初代8086の呪いというか、既定方針はその後のx86に暗い影?を投げかけてます。基本x86はオペランドの幅を切り替えるのに1ビットしか使えないエンコード構造だったからです。8086から80286までは16ビット機だったので、8ビットと16ビットを切り替えられればOK。しかし32ビットの80386にいたって、どうやって32ビット幅をエンコードするのか問題が出てしまいました。
そこで登場したのがオペランド・サイズ・プリフィックスです。これをつけたら32ビット、いえいえ、そんな単純な方法をとるx86ではありません。そんなことすると一律に32ビットの命令コードがプリフィックスだらけになって水ブクレ。そこで考えられたのは
プリフィックスでデフォルト設定をヒックリ返す
という御約束です。デフォルト設定は2種。
-
- オペランドは32ビットと8ビット、アドレス(オフセット部分)は32ビット
- オペランドは16ビットと8ビット、アドレス(オフセット部分)は16ビット
つまりデフォルト設定が、1の状態で16ビットのオペランドを扱いたかったとき、オペランド・サイズ・プリフィックスをつけると16ビット処理ができる。一方2の状態で32ビットのオペランドを扱いたかったときは、オペランド・サイズ・プリフィックスをつけることで32ビット処理ができると。同じ機械語オペコードがあっても、そのコンテキストで意味が違うという荒業であります。
さてそのデフォルト設定がどこにあるのか、というとカレントのコード・セグメント・デスクリプタの属性の中のDビットであります。この値が1なら32ビット、0なら16ビットです。
というわけで今でもx86は底の奥底でセグメンテーション機構の呪縛に囚われておるのです。まあ、知らなくてもコードは書けるケド。
なお、オペランド・サイズ・プリフィックスは整数データの幅によって今でも頻繁に使われてます(アセンブラが勝手に生成してくれる。)しかし、アドレス・サイズ・プリフィックスの方はまず使われないのではないかと。これは286式の16ビット、64Kサイズのセグメントを使いたいときに使うものだからです。そんなセグメント使わんずら。
オペランド・サイズ・プリフィックスの説明で1回終わってしまった。先は遠い。
