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

Joseph Halfmoon

前回8086/88, 80186のセグメンテーション(ほぼほぼ80286.386以降のリアルモード相当)についておさらいしたので、今回は「メンドクセー」奴らセグメントレジスタを実際に操作してみます。といっても「読み・書き」するだけなんだけれども。そんな簡単な操作でも一筋縄でいかない感じがほの見えるのであります。

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

※実機動作確認(といってもエミュレータなんだけれども)には以下を使用させていただいております。

    •  Windows 11 PC (i5-1235U)
    •  Ubuntu 24.04 LTS on WSL2
    •  QEMU 8.2.2
    •  FreeDOS 1.3
FreeDOS上での.EXEファイルの生成

さて、ここまでの8086レベルの16ビット命令の動作確認は、.COM形式(tinyメモリ・モデル)で行ってきました。この形式はセグメントレジスタの設定はOSにお任せ、64Kバイト上限、自プログラムの内部ではセグメントレジスタを操作したりしませぬ。しかし、セグメントレジスタを操作することとなりました今回からは、メモリモデルを切り替えざるを得ませぬ。.EXE形式ね。

さて使用させていただいとりますFreeDOS(MS-DOS互換のフリーなOSです)上で nasmアセンブラを使って .EXE ファイルを生成する場合は以下のようなステップとなります。

    1. リンカ wlink を使えるようにwatcom Cの環境セットアップバッチ(owsetenv.bat)を実行してパスを通しておく
    2. nasm で obj形式ファイルを生成
      • nasm -f obj seg.asm
    3. できたobjファイルをwlinkでリンク
      • wlink name seg.exe format dos file seg.obj
    4. debugでの実行制御はいままでと変わらず
      • debug seg.exe
今回練習の命令

以下の部分オペコードマップ上、今回練習の命令は、MOV(転送)命令(オレンジ色背景)のうち赤枠のセグメントレジスタに関する2命令です。
x86MOVopcode
上記から分かるとおり「汎用」(8086の場合クセ強な「汎用」だけれども)レジスタと異なり、セグメントレジスタへは即値ロード命令が存在しませぬ。よって、初期値の設定などは他の汎用レジスタやメモリからとなります。

セグメントレジスタ操作のアセンブリ言語ソース

多少 nasm アセンブラのクセありなソースが以下に。

segment code

..start:
    mov ax, data
    mov ds, ax
    mov ax, stack
    mov ss, ax
    mov sp, stacktop
    mov ax, edata
    mov es, ax
test:
    mov [save0], ds
    mov [save1], es
    mov ss:[save0s], ds
    mov ss:[save1s], es
    mov es:[save0e], ds
    mov es:[save1e], es
fin:
    mov ax, 0x4c00
    int 0x21
    resb    2048

segment data    align=16
save0:  dw  0
save1:  dw  0
    resb    1024 * 63

segment edata   align=16
save0e: dw  0
save1e: dw  0
    resb    1024 * 63

segment stack   class=STACK
save0s: dw  0
save1s: dw  0
    resb    2048
stacktop:

過去回では、section ということで記述してましたが、今回からは segment です。ただし、アセンブラ的にはそれほどな差もないみたい。

EXEモデルでは、COMモデルのように全セグメントレジスタが同一値で100hスタートといった前提がありません。セグメントの使用はそれぞれ個別にセグメントレジスタをロードして開始することになります。ただし、CS(コードセグメント)とSS(スタックセグメント)は事前に準備しておかないといろいろトラブルぶりそーなためか以下のようなルールになっているみたい。

    • “..start:” という開始番地のラベルを手がかりにCS:IPを設定
    • ”class=STACK” という属性?の指定されたセグメントにSS:SPを設定

上記ソースでは、DS、SS、ESの各セグメントレジスタに初期値を設定後、DS、ESの値を各セグメントのメモリに書き込んでいるのですが、ちょいとメンドクセー話もありーの。

実行結果

上にある.EXEファイルの生成手順により生成したオブジェクトファイルを、FreeDOSのdebug(御本家マイクロソフトのdebugの上位互換、魔改造?デバッガ)で観察した様子が以下に。

まず頭の方のセグメントレジスタへの値のロード部分。SSseg

上記の先頭部の2命令など、セグメントレジスタへの値の書き込みは

    1. AX(汎用レジスタ)への即値ロード(即値はメモリ上のセグメントのベースアドレスを指す)
    2. AXからセグメントレジスタへ書き込み

という2ステップで行っています。

しかし挙動が「妙」なのは、下の方のSSセグメントレジスタへのAXからの転送命令のところです。debugのtコマンド(トレース)を使ってシングルステップ実行しているのに、2命令が一気に実行されているように見えます。

朧げな記憶によると、これは

SSレジスタに書き込んだ直後はいかなる割り込みも受け付けられず(トレースも)、次の命令まで実行される

というお約束のためです。SSロード直後にSPへのロード命令がおいてあるのを見れば分かると思いますが、SSロード直後、SPへのロードとの間で割り込みが発生してしまうと、SS:SPでポイントされるスタックがどこか素性の知らぬ空間に漂ってしまいます。すると割り込みそのものがクラッシュしてしまいます。このご禁制はその防止だと思います。かならずSS:SPは同時に立てられないとなりませぬ。

なお、ここで設定しようとしているSS:SPの値は、ソースのスタックセグメントを見ると「一目瞭然」な値です。実際には該当コードを走らせるまえにOS側で設定されてからプログラムに入ってきます。アセンブラでの設定は、実は冗長なコードであったです。

つづいて、DS、ESに設定されているセグメント値をセーブするところが以下に。まずはデータセグメントへのセーブ。DSsave

DS=0x091Fです。DSの値が DS:0000、ESの値がDS:0002へセーブされているのが分かると思います。この「ダイレクトアドレシング」はデフォルトのセグメントがDSになっているので、何も指定しなくとも行先はDSとなります。

同じことをSSに対して行った様子です。DSのセーブ命令が上の末尾の1命令、ESのセーブが以下の先頭にあります。SSsave

「ダイレクトアドレシング」はデフォルトDSであるので、スタックセグメントへ向かわせるためには、セグメント・オーバライド・プリフィックス SS: が必要です。メンドクセー。

同じことをESに対して行った様子は以下同文であります。ESsave

やはりセグメント・オーバライド・プリフィックス ES: が必要です。x86(16ビット)のアセンブラを書くときにはこの手のプリフィックスのルールを呼吸するように処理できねばなりませぬ。本当か?

単なるMOV(転送)命令と思ったらアカン?

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

ぐだぐだ低レベルプログラミング(190)x86(16bit)、即値のMOV操作 へ進む