ぐだぐだ低レベルプログラミング(191)x86(16bit)、MOVS、所謂ブロック転送命令

Joseph Halfmoon

前回、DIレジスタをベースに使うメモリアクセスではDSがデフォルトになる件練習。しかしDIレジスタは一部命令でESと「不可分に結びついている」のです。x86式には「ストリング転送命令」、一般には「ブロック転送命令」においてです。今回はその代表 MOVS 命令について見ていきたいと思います。コマケー話の宝庫なのよ。

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

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

    •  Windows 11 PC (i5-1235U)
    •  Ubuntu 24.04 LTS on WSL2
    •  QEMU 8.2.2
    •  FreeDOS 1.3
ストリング命令群

x86には、ストリング命令と称する命令群があります。8086の前の時代、インテル8080、8085にはブロック転送命令が無く、一方ザイログZ80にはあったです。Z80が「受けた」理由の一つじゃないかと思います。そこでインテルも8086では同様な命令を導入したのだと思うけれども同じような名前にするのが嫌だったのか?今となってはこういう命令は百害あって一利なしとか言われそうなんだが。まあ、80286くらいまではDMA転送の代わりにディスク・ブロックをまとめて読み込むとかに重宝されていた遠い記憶。

    • MOVS
    • CMPS
    • STOS
    • LODS
    • INS
    • OUTS

確かにストリング(文字列)を操作するのにも使えますが、ぶっちゃけ「ブロック転送」系の命令群です。あるメモリの塊をまとめて移動したりできるもの。今回は、その代表としてMOVS命令についてその動作を復習してみたいと思います。

今回、関連するオペコードに赤枠をつけてみたものが以下に。MOVSopcode

オペコード 0xA4、0xA5を占めているのがMOVS命令です。2個あるのは0xA4がバイト幅の操作、0xA5がワード幅の操作ってこってす。

しかし、下の方をみると、REP、CLD、STDなどという命令にも赤枠があります。こいつらはブロック転送やるときに無くてはならないものどもなのです。

    1. MOVSは単体命令では転送1回のみ
    2. REPプリフィックスをMOVSに冠すると複数回の転送が起こる
    3. 転送に使うポインタSI、DIの更新方向はDF(ディレクション・フラグ)で制御される。DFを操作するのがCLDとSTD

MOVSはストリング命令といいつつも、単体では転送一回です。転送はデフォルトのデータ・セグメントのSIで指されるアドレスから、ES:DIで指されるアドレスに対してです。そして転送実行後、DFが0の場合、SIとDIの値は転送がバイトなら+1、ワード(x86では16ビットです)なら+2加算されます。DFが1の場合は反対方向に1もしくは2減じられます。よって転送のアドレス順序が問題になる場合(転送元と転送先の領域に重なりがある場合)は事前にCLDもしくはSTD命令でポインタの更新方向を決定しておく必要があります。

なお、SIの指すメモリはデフォルトセグメントがDSですが、オーバライド・プリフィックスにより上書き可能です。一方、DIの方はES固定でオーバライドは不可です。しかし、後に述べる理由でオーバライドプリフィックスを使うのはあまりお勧めできないデス。

さてMOVSの頭にREPプリフィックスをつけると複数回の転送が起こりますが、これは、「CXレジスタが0でない間、MOVS命令を繰り返し実行し、毎回の実行後にCXレジスタを1減じる」という操作により実現されます。ここでもx86(16ビット)の汎用じゃないレジスタの使用を見ることになります。CXは「カウンタ」レジスタなのよね。

CXは16ビットなので最大値は65535です。ここから容易に分かるのが

REP MOVSの実行時間、とんでもなく長くなる可能性あり

ということです。通常、割り込みは命令の末尾で受け付けるものです。しかしREP MOVSのような命令の最中ずっと割り込みを受け付けないと割り込み使うシステムではわけわからなくなるので、REP MOVSは途中で割り込みを受付可能となってます。また、目出度く割り込み処理を完了したのち、中断されているREP MOVSをレジュームしてやらないとこんどはブロック転送の辻褄があわなくなります。REP MOVSは、このレジューム処理にも対応しとります。しかし、復旧には条件があるみたいです。セグメント・オーバライド・プリフィックスやLOCKプリフィックスなど複数個のプリフィックスをREP MOVSにつけていても、複数個のプリフィックス全部を復旧できるわけじゃない、という制限です。まあこういう命令にLOCKつけるとか言語道断な気もしますが(バスサイクルを長期占有してしまうので。)素直に素のREP MOVS として使っている方が安全ね。

即値MOV命令のアセンブリ言語ソース

MOVS命令1個を動かしてみてみるだけのソースです。ワード幅転送で8ワードのこじんまりした転送例です。

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 si, srcblk
    mov di, dstblk
    mov cx, 8
    cld
    rep movsw
fin:
    mov ax, 0x4c00
    int 0x21
    resb    2048

segment data    align=16
srcblk: dw  8 dup (0x1234)
    resb    1024 * 63

segment edata   align=16
dstblk: dw  8 dup (0)
    resb    1024 * 63

segment stack   class=STACK
    resb    2048
stacktop:

転送元の DS:srcblkには、0x1234という値が詰めてあり、転送先のES:dstblkは0詰めです。

アセンブルして実行

さて、FreeDOS上、以下のステップで上記アセンブラソース movs.asm から実行可能なオブジェクトファイルを得ることができます(nasmとwatcom Cがインストール済であること。)

nasm -f obj movs.asm
wlink name movs.exe format dos file movs.obj

実行は例によって御本家 MS-DOSのdebugとクリソツな debugです。

debug movs.exe

debug起動直後のディスアセンブル表示が以下に。MOVS

上記赤矢印のREP MOVSW(ワード幅指定)の直前のレジスタ状態が以下に。beforeMOVS

赤線が注目すべきところです。

また、実行前のメモリの様子が以下に。beforeMOVS_memory

そして、1命令 REP MOVS を実行した直後のレジスタ状態が以下に。afterMOVS

CXは0になり、SI、DIは0x10までオートインクリメントされとります。

実行後のメモリは以下のとおり。afterMOVS_memory

ブロック転送されとるなあ。あたりまえか。

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

ぐだぐだ低レベルプログラミング(192)x86(16bit)、PUSH/POPにも先祖の痕跡 へ進む