ぐだぐだ低レベルプログラミング(186)x86(16bit)、ASCII ADJUST一族

Joseph Halfmoon

x86命令には「BCD補正命令」なる一族あり、その中でもパックドBCD数を扱うDAA命令は御先祖の8ビット機(8080/8085)でも存在した伝統の命令です。しかしx86ではアンパックドBCD数を扱うための頭文字にAを頂く命令どもが追加されております。強力?でも今となっては盲腸みたいなもん?どうなん?

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

※実機動作確認には以下を使用させていただいております。

    •  Windows 11 PC (i5-1235U)
    •  Ubuntu 24.04 LTS on WSL2
    •  QEMU 8.2.2
    •  FreeDOS 1.3
BCD(Binary Coded Decimal)

通常2進数であれば、4ビット幅を一杯につかって10進数の0から15までの数字を表すことが可能です。しかしこれを10進数の0から9を表現する表記のみを有効とすれば、2進4ビット=10進1桁として表現することができます。BCDでありますな。これが効いてくるのは、ピタリ10進で計算したい用途です。なにせ2進と10進は分かり合えない部分あり、0.1は10進じゃピタリ賞ですが、2進では正確に表すこと不可能。よって0.1%の利率の複利計算などすると、何もテクを使わなければ2進と10進の結果は一致しませぬ。

遥かな古代、電卓というものが登場したときにもこの2進、10進の問題があり、基本電卓というものは10進で計算すべしという縛りがかかったみたいです。思い起こせば86系はその先祖である8ビット機、そしてそのまた先祖である4ビット機にルーツを持っており、まさに4ビット機は「電卓」のために作られたものでありました。よって86系は累代、BCD演算というものに貴重なファーストオペコード空間の一部を割り当ててきたわけであります。今となってはどれどほどの意味があるのかは問いませぬが。

さてBCDにも以下のお作法あり。

    1. パックド、4ビット幅を10進1桁に割り当てる。8ビット(バイト)に10進2桁を記憶させる
    2. アンパックド、1バイト(8ビット)の下4ビットに10進1桁を記憶させる。上の4ビットは0詰めで残しておく。

今回練習してみますのは、2のアンパックドBCD数を扱うのに「便利」なものどもです。御先祖の8ビット機にはなく、8086に至って登場した命令群です。

AAA一族

さて、今回練習する命令は以下の4命令です。

    • AAA 、ASCII ADJUST FOR ADDITION
    • AAD、ASCII ADJUST FOR DIVISION
    • AAM、ASCII ADJUST FOR MULTIPLY
    • AAS、ASCII ADJUST FOR SUBTRACTION

加減乗除、ひととおりそろってます。基本全ての命令はALレジスタにアンパックドの10進数が入っている前提で、ALレジスタとの通常の(2進の)バイト演算命令を行ったときに結果がアンパックドな10進数になるようにサポートする命令です。また、AHレジスタには10進の上位桁(割り算の場合のみ余り)が入ることになってます。

ただしその使い方はちょっと凸凹ありです。

    • AAA、2進加算命令を実行した後でアンパックド10進になるように補正
    • AAD、2進除算命令の結果がアンパックド10進で得られるように除算前に先回りで「補正」しておく
    • AAM、2進乗算命令を実行した後でアンパックド10進になるように補正
    • AAS、2進減算命令を実行した後でアンパックド10進になるように補正

AADだけ順序が違うのね。また、AAAとAASは1バイト命令ですが、AADとAAMは2バイト命令です。そしてAADとAAMの2バイト目は即値の「10」だと思います。10進補正するときの10という数値が命令の中に含まれておると(8086世代の場合、当然そこを書き替えると異なる動作をするので演算命令として使えないこともない。。。)

今回動かしてみるアセンブリ言語ソース

NASMアセンブラ用のソース、FreeDOS上でCOM形式のオブジェクトにして実行するつもりのものが以下に。

    org 100h
section .text
start:
    mov sp, stacktop
test:
    mov ax, 0105h
    mov dl, 08h
    add al, dl
    aaa
    mov ax, 0101h
    add al, dl
    aaa
    nop
    mov ax, 0402h
    aad
    div dl
    nop
    mov ax, 0009h
    mul dl
    aam
    nop
    mov ax, 0203h
    sub al, dl
    aas
    mov ax, 0209h
    sub al, dl
    aas
fin:
    mov ax, 0x4c00
    int 0x21

section .data   align=16
workw:  dw  5678h

section .bss    align=16
    resb    2048
stacktop:
動作確認

上記のソース (aaa.asm)から、以下のコマンドライン一発で実行可能なオブジェクトが得られます。

nasm aaa.asm -fbin -o aaa.com

動作確認は、FreeDOS上の debug コマンド(御本家 マイクロソフト社のdebugと「上位互換」のデバッガ)を使って行っております。

まずはAAA(加算後の補正)命令からAAA_0

AX=0105のところに赤線ありますが、これはAH、ALの2つを合わせて10進2桁「15」だ、ということであります。ここにDLレジスタの「8」を加えるわけです。だたし2進加算命令 ADD AL, DLではALレジスタの内容が0x0D(つまり十進で13)になるだけでBCD表現からはハズレてます。そこでAAA命令を発するとあら不思議、AH=02、AL=03あわせて「23」ということで10進加算されております。

ここで注目したいのはフラグのACとCYです。とくに8086のACフラグ(auxiliary carry)は条件分岐に使えるわけでもなく、何のためのフラグ?と思われているかもしれませんが、BCD補正では、ほれこのように大活躍。

上記は、AAAの効果あり、のケースでしたが、計算によってはAAAなど不要の場合もあります。こんな感じ。AAA_1

上記は10進で 1 + 8 = 9 と2進でみてもOKな範囲なので何も起こりませぬ。

続くのは割り算です。AAD

最初AXに0402が入ってますが、これは10進解釈で「42」です。これを事前のAADをすると、AXの内容が002Aになってます。0x2A=42なので、AAD命令により10進2桁が2進数になったことになります。ここで DIV DLしてます。

42 ÷ 8 = 5 あまり2

ということで最終的にはAHにあまりの2、ALに商の5が入っておると。BCDで割り算できたことになりますな。

続くのは掛け算です。AAM

ALにDLをかけると 9 x 8 = 72 となります。2進数では0x48です。この2進の結果に対してAAM命令を適用すると、AHに7、ALに2とアンパックド10進に変換されとります。

最後は引き算です。赤線引いてないすけど、足し算と以下同文です。最初は補正が必要な場合。AAS_0

次はAASの効果不要の場合。AAS_1

計算はできるけど、説明がメンドイ。それに今となっては使い途もビミョ~。

ぐだぐだ低レベルプログラミング(185)x86(16bit)、シフト、ローテイト練習(86) へ戻る