ぐだぐだ低レベルプログラミング(188)x86(16bit)、セグメンテーションその1

Joseph Halfmoon

ことさらにMOVなど転送命令を避けて今回に至ります。流石にこの辺で悪名高い x86 のセグメンテーションについて説明しておく必要を認めました。そこで今回はリアルモードのセグメンテーションの図解といたします。まあセグメンテーションといいつつ、そのうちやるつもりのプロテクテッド・モードのそれに比べりゃ可愛いもんだけれども。

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

x86のセグメンテーション

「分かり難い」「とても使いにくい」などと悪名高いx86のセグメンテーションですが、16ビットの時代から現在の64ビットの時代まで「存在」しつづけています。しかしその立ち位置は大きく変わってきています。以下はお惚け老人の個人の感想っす。

    • 16ビットのx86の時代、セグメンテーションの理解なくしてはプログラムできなかった。
    • 32ビットのx86の時代、セグメンテーションを使ってはいけないわけではなかったけれど、多くのOSがセグメンテーションを意識せずに済む空間を用意してくれるのでプログラマは気にする必要はなかった。
    • 64ビットのx86の時代、セグメントを操作すること自体が「悪」というかペナルティを伴う行為になった。何か特段の理由ない限り触らないようにしよう。

さてこのところ練習しておりますのが、x86でも16ビットの時代のアセンブリ言語命令です。これまでは、セグメンテーションを見て見ぬ振りで演算系の命令にフォーカスしてきました。いよいよ転送系の命令に進出?します。そこでセグメンテーション抜きには自由自在なメモリアクセスはできませぬ。

ややこしいのが、16ビット動作モードのx86といっても「セグメンテーションには2種類」あることです。

    • リアルモードでのセグメンテーション。オリジナルの8086以来、現在にいたるまで根っこに存在する。だいたい8086では「リアルモード」などという言い方は無かった。
    • プロテクテッドモードでのセグメンテーション。80286以降(32ビット化した80386以降も含めて)に備わっているセグメンテーション。8086/8088、80186などは備えていない。プロテクテッドモードが登場したがためにそれ以前のモードをリアルモードと呼ぶようになった。

表にするとこんな感じ。

mode 8086/8088, 80186 80286~
real Yes Yes
protected No Yes

今のところ「8086として(リアルモード)」の演習をしているので、今回図解するのはリアルモードにおけるセグメンテーション動作です。「プロテクテッドモード」動作に比べたら単純ではあるのだけれども、コマケー規則がいろいろあって、悪名通りメンドイです。

リアルモードでのメモリアドレス計算規則

8086/8088、80186は、20ビットのアドレス線を持っており、リアルモードでは20ビット(1Mバイト)のメモリ空間を使うことができました。一方、8086/8088、80186、そして80286も16ビット・プロセッサであり、レジスタのビット幅は16ビットしかありません。16ビット幅のレジスタを使って20ビットのアドレスを生成するために、以下の図のような計算機構を備えてました。realModeSegmentation

メモリアドレスの計算は上記の図の黄色の部分の緑のレジスタどもの足し算から始まります。

    1. Effective Address (Offset)の計算。ベースレジスタ(16bit)の値に、インデックスレジスタ(16bit)の値とディスプレースメント(8bitまたは16bit)を加え、上位への桁上がりは無視して16bitの実効アドレスを計算する。なお、ディスプレースメントが不在の場合のアドレシングモードもあり、またディスプレースメントが8ビット値の場合は符号拡張して16ビット化する。また、インデックスレジスタが不在の場合もある。通常のメモリアクセスではベースレジスタが必要ではあるものの、アキュムレータへのロード/ストア操作では「ダイレクト・アドレシング」とて、16ビットのディスプレースメント相当の値のみで実効アドレスを指定できる場合もある。
    2. 物理アドレス20ビットの計算。セグメントレジスタ(16bit)の値を左4ビットシフトし、下の4ビットには0ずめして20ビットのセグメント・ベース・アドレス値を得る。そこに第1のステップで計算した実効アドレス16ビットを加えて物理アドレスを得る。
    3. ベースレジスタとして使えるのは汎用レジスタのうち、BX、BP、SI、DIの4本。インデックスレジスタはSIとDIの2本。
    4. コードフェッチの特例。IP(インストラクション・ポインタ)が実効アドレスを保持し、セグメントレジスタはCS(コード・セグメント)と必ず組み合わせられる。
    5. スタック操作の特例。PUSH、POPとCALL、RET等の命令でのメモリセーブ、メモリロードの場合、実効アドレスを保持するのはSP(スタック・ポインタ)である。SS(スタック・セグメント)と必ず組み合わせられる。

「簡単」だったでしょ?

4つのセグメント

さて、16ビットのセグメントレジスタは以下の4本あります。

    • CS、コードセグメント。命令フェッチ専用。
    • SS、スタックセグメント。スタック操作専用。
    • DS、データセグメント。データ(変数など)アクセス用。
    • ES、イクストラ・データセグメント。ブロック転送命令の行先などデータセグメントの補助的な用途。

それぞれ用途が決まっているので、イメージとしては以下の図のような感じです。1Mバイトの物理アドレス空間の中にそれぞれ64Kバイトサイズの4つのセグメントを置く感じになります。上記のアドレス計算ルールによりセグメント自体は16バイト・アライメントになります。重ねちゃいけないというルールはないので、4本のセグメントを全て重ねた使用方法(COMモデル)から全て別アドレスにする方法までいろいろあります。16ビット時代のコンパイラなどはそれらのセグメンテーションの指定のための各種スイッチを持つものがほとんどでした。realModeSegmentation2

うち、命令コードのフェッチはCSキメウチで変更不能です。CSを書き替えるためには、制御転送命令(ジャンプ系)の中でも「インターセグメント(あるいはロングジャンプ)」の命令群を使わないとなりません。短いジャンプ(相対ジャンプ)もありますがそいつらはCSに作用しません。

また、PUSH、POP、CALL、RETなどの命令のメモリ読み書きではSSキメウチです。8086/8088では上記の4つですが、80186以降ではSSに作用する命令が追加になってます。通常のMOV命令でもBPレジスタをベースレジスタとするアドレシングを使った場合、デフォルトでSSセグメントが使用されます。

上記以外のほとんどのメモリアクセスはDSがデフォルトです。ただし、x86にはこれまた悪名高い(昔は便利に使っていた)ブロック転送命令という一族がおり、そいつらはソース側がDS、デスティネーション側がESというデフォルト指定を持っています。

しかし、コードセグメントにしまってある定数をデータとして読みたい、とか、スタックセグメントの値をデータとして書き替えたいといった需要は確実にあります。そのようなご要望にお応えするため?に、x86はセグメント・オーバライド・プリフィックスというものを貴重なオペコードスペースのファーストバイトに持っています。こんな感じ。PrefixEC

こいつらは1バイトの命令コードですが、単独では命令コードとしては成り立たず、「メモリアクセス」を伴う命令の前に付加することでデフォルトではDSであるべきメモリアクセスをCS、ES、SSに向けることが可能です。また、デフォルトでSSへ向く、BPをベースレジスタとする命令に付加すれば、DS、CS、ESに付け替えることも可能です。なお、こいつらプリフィックスを複数個つけても簡単には例外になったりせず(命令長が長すぎると最後には例外になります)、最後の1個が有効です。メンドクセー奴らなんだこいつら。

ぐだぐだ低レベルプログラミング(187)x86(16bit)、DECIMAL ADJUST族 へ戻る

ぐだぐだ低レベルプログラミング(189)x86(16bit)、セグメントレジスタ操作 へ進む