ぐだぐだ低レベルプログラミング(181)x86(16bit)、ADDオペランド何でもありすぎ

Joseph Halfmoon

前回から元祖x86、16bitCPU、8086/8088の命令セットの復習を始めました。うん十年ぶり。忘れてます。しかしArmとかRISC-Vとかの後に改めて8086の命令をみると確かに複雑、CISCだからあたりまえか。今回は整数足し算、ADD命令を練習してみます。命令にとれるオペランドがいろいろありすぎ、何でもあり?

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

※実機動作確認には以下を使用しております。前回はRaspberry Pi 4(Armコア)機上でQEMUでCPUエミュレーションしていましたが、今回からはx86_64のPC上で動かしてます。どうせQEMU上のFreeDOSなのでどこで動かそうと同じじゃと(実はいろいろあるけれど。)

    •  Windows 11 PC (i5-1235U)
    •  Ubuntu 24.04 LTS on WSL2
    •  QEMU 8.2.2
    •  FreeDOS 1.3
整数ADD命令

前回はINC/DEC命令で控えめに始めましたが、今回はADD命令でまさに整数演算命令の本丸に踏み込みます。ArmやRISC-Vみてから8086のADD命令みると驚愕するのが以下であります。

    1. オペランドにレジスタでも、メモリでも、即値データでも何でもとれる
    2. 2オペランド、左にデスティネーション兼ソース、右にソースを置く

Armにせよ、RISC-Vにせよ、RISCでは演算はレジスタ間、メモリへのアクセスはロード/ストア命令と峻別されているので、演算命令であるADDのオペランドにメモリをとれるというのはビックリです。まあ、歴史的に言えば8086のCISCのスタイルが先だったのだけれども。

また、2オペランドスタイルで演算結果がソースの片方を上書きしてしまうというのも「そこそこビックリ」かと。RISCは3オペランドで両方のソースを保持することもできるからね。でもコマケーことを言うとArmのメモリ節約用Thumb命令は2オペランドスタイルで8086同様に片方上書きしてしまうタイプです。

ADD命令がとれるオペランドを要約すると以下のようです。

Destination/Source1 Source2
register register
register memory
memory register
register immediate
memory immediate
accumulator immediate

唯一の「救い」はADD命令に関してオペランドの両方がメモリという組み合わせが許されてないことですな。ただバリバリのCISC、8086には「その手の」命令も無くはないのですが、また今度ね。

8086/8088レベルのADD命令テストプログラム

NASM(NASMについてはコチラの回で)用、メモリモデルはTINY、DOS上での実行形式は .COM 形式となる予定のアセンブリ言語プログラムが以下に。

    org	100h
section	.text
start:
    mov	sp, stacktop
    mov	bx, workb
    mov	si, workw
    mov	bp, sworkw
    mov	word [bp], 4000h
    mov	ax, 1234h
    mov	cx, 3000h
    mov	dx, 5555h
test:
    add	ax, 2000h
    add	al, 0Ah
    add	cx, dx
    add	al, [bx]
    add	cx, [si]
    add	[bp], dx
    add	byte [bx], 33h
fin:
    mov	ax, 0x4c00
    int	0x21

section	.data	align=16
workw:	dw	1000h
workb:	db	7

section .bss	align=16
sworkw:	resw	4
    resb	2048
stacktop:

startは「伝統の」0100h(インテル式、0x100)、ラベル test: から fin: の間が今回練習してみたADD命令です。

freeDOS上でのNASMによるアセンブルは以下で。

nasm add.asm -fbin -o add.com

生成された add.com をfreeDOS付属のdebugで開いたところが以下に(debugはMS-DOS付属のデバッガと同じファイル名で機能も「互換」ではありますが、何気にアチコチ強化されているfreeDOSのプログラムです。)

ADDinitU

上記の赤線部分は、初期化後のレジスタの値と、これから操作するメモリの初期値です。

まずはレジスタのADD操作。ADDregs

赤線はAXレジスタへの即値 0x2000の足し込み、緑線は AXの下8ビット分であるALへの即値 0x0Aへの足し込みです。結果をみるとADDされていることが分かります。ここでAX、ALは「アキュムレータとして優遇」されていて、他のレジスタに対してよりも「短い命令」にアセンブルされてます。ここは前回も触れたさらにご先祖の8080の命令セットからの移植の配慮ね。

ピンク色ではフツーのレジスタ間ADD。

つづいては、オペランドの片方にメモリをとるもの。ADDmem

最初のピンク線は、バイト幅の計算で、BXレジスタの指す先のバイトをもってきてALレジスタと加えて、ALレジスタに書き出すもの。

次の赤線は、ワード幅(x86では16ビット)で、SIレジスタの指す先のワードをもってきてCXレジスタの値に足し込むもの。

その次の黄色線は、ワード幅で[BP+0]番地から読んだ内容とDXレジスタの内容を加算して[BP+0]番地に書き戻すもの。なお前回もやりましたが、アドレシングのためのベースレジスタにBPをとるとスタックセグメントSSを指すことになります。ただし例によってで、.COM形式なので全てのセグメントは重なってます。

最後の緑線は[BX]番地のバイトに0x33を足し込むもの。メモリから読んだ値に即値を足して、メモリに書き戻す命令です。この命令の場合、[BX], 33 などとオペランド書いてしまうと「ワードだか、バイトだか判断できん」ということになります。そこで荘厳にも

BYTE PTR

と修飾されてます(インテル式。NASMでは素っ気なく BYTEとだけ)

オペコードは1個なんだけれども、いろいろ出来すぎ8086。

ぐだぐだ低レベルプログラミング(180)x86(16bit)、INC/DECにCISCを見? へ戻る

ぐだぐだ低レベルプログラミング(182)x86(16bit)、ADD以下同文?微妙に非対称? へ進む