さて前回よりx86の最初のステップ、16ビットモードに入りました。いままでRISC-V、ArmとRISC系(Armはそれにしちゃ命令多過ぎだが)を練習してきましたが、今回からは「バリバリの」CISCデス。それほど意識することはないけれども、違いは確実にあるので気づいたところから見ていきます。
※「ぐだぐだ低レベル プログラミング」投稿順indexはこちら
※実機動作確認には以下を使用しております。
整数INC/DEC命令
さてx86(16bit)のアセンブリ言語命令の練習の最初に選んだのはINCとDECです。「対象」を+1するのがインクリメントでINC、ー1するのがデクリメントでDECと分かり易いデス。ただx86(16bit)の場合、その対象というのが以下の2つ(詳しく言うと4つ)にわかれます。
-
- 整数レジスタの値(バイト幅またはワード幅)
- メモリの値(バイト幅またはワード幅)
なお、x86世界ではワードは16ビットです。これは元祖の8086が16ビット機だったためだと思います。ArmやRISC-Vの回ではワードは32だったのでご注意を。
さてRISCでもINC/DEC命令はありましたが、操作対象はレジスタだけでした。ここにメモリも入ってくるのがCISCです。RISCで同じことをしようとすれば、メモリ上の値をロード命令でレジスタに読み、レジスタ上でINC/DECして、ストア命令でメモリに書きもどすという3命令を要します。CISCの1命令はこれを1命令でやってしまうのであります。複雑(コンプレックス。)
さて、命令の機能そのものは分かり易いので以下のような8086レベルのアセンブリ言語プログラム(NASM、NASMについてはコチラで)を書いてみました。
org 100h section .text start: mov sp, stacktop mov bx, workb mov bp, workw mov cx, 1000h mov dx, 1234h test: inc cx dec cx inc dh dec dh inc byte [bx] dec byte [bx] inc word [bp] dec word [bp] fin: mov ax, 0x4c00 int 0x21 section .data align=16 workw: dw 100h workb: db 7 section .bss align=16 resb 1024 stacktop:
スタイルとしては、MS-DOSの.COM形式(TINYメモリモデル)のプログラムです。メモリモデルについては別シリーズのコチラで。
ラベル test: 以下が inc、dec を練習してみている部分です。まず16ビットレジスタcxをinc して dec。+1してー1なので元に戻ります。x86の整数レジスタについては前回で。つづいて8ビットレジスタ dh をincしてdec。さらに今度は bxレジスタが指しているメモリ番地をincしてdecします。ここで byte と書いてあるのはbxレジスタの指す先、メモリは [bx] と記します、がバイト幅であるという意味です。
-
- 本来のインテル式アセンブラ表記では byte ptr
- NASMでは byte
インテル式の表記は荘厳ですが、目的はバイトなんだかワードなんだか判別できればいいので、NASM式で十分っす。
メモリのinc/decは、bxで指す先のバイトと、bpで指す先のワードの2種類を練習してますが、ここにはバイトとワード以外の違いもあるのです。それは出てきたところで。
アセンブルして動作を観察
freeDOS上にインストール済のNASMを使って、上記のソースから .COM形式の実行ファイルを作るには以下です。
nasm incdec.asm -fbin -o incdec.com
freeDOSには、MS-DOSのdebugに上位互換(魔改造?されている)のdebugが付属しているので、それを使って動作を観察してみます。
ラベル test: の位置で「止めた」ところからシングルステップで見ていきます。
緑のところで、INC CXしてDEC CXしているのが分かると思います。CXレジスタを見てみると CX=1000(16進なので 0x1000の意味。インテル式表記だと 1000h)から1001となって1000に戻っているのが観察できます。注目すべきはINC CXとDEC CXのオペコードです。黄色で下線引いてますが、1バイトです。とっても短いです。ここでもCISCの特徴が現れました。
x86=CISC=バイト可変長オペコード
です。基本的な命令は短いバイト数、複雑な命令は長いバイト数にエンコードされる原則(方針?)です。ワードレジスタをinc/decする命令は優遇されてます。8086のオペコードマップを見るとこんな感じ。
全部で256個しかない、1バイトの空間のうち16バイトをワードレジスタのinc/decに費やしてます。なお、よゐこはレジスタ名が、AX、CX、DX、BXとABC順になってないことにも気づくでしょう。その話は前回ね。
一方、赤線のところでは、バイトレジスタ DH を INC、DECしています。DHはDXの上位1バイトなので、これが独立して+1、ー1されているのは予定どおり。しかし、オレンジ色の下線を引いたオペコードをみると2バイトっす。バイトレジスタのinc/decは不遇ね。ワードはポインタの操作やカウンタ操作で頻繁に使うけれども、バイトのinc/decは少ない、って判断があったのでしょう。知らんけど。
さてレジスタ操作に続くのが、お楽しみのメモリ操作です。こんな感じ。
まず d 0130 013Fのところで、inc / dec の対象となっているメモリの内容をダンプして確認してます。黄色の方がワードで BP レジスタが指す先、赤の方がバイトで BX レジスタが指す先です。
黄色の箱の中は 00 01 と書かれてますが、初期値は 0x0100 です。
x86はリトルエンディアン
なので、メモリにおくと、0x0100の下位側の00が下のアドレスに来て、上位側の01が上のアドレスに来ます。
さて、FreeDOSのdebugは、荘厳な BYTE PTR、WORD PTR表現で、各命令をディスアセンブルしてくれる上、命令実行直前の操作対象のメモリ内容を右端に表示してくれます。便利ね。
それをみると
-
- INC BYTE PTR [BX]は、DS:0132=07
- INC WORD PTR [BP]は、SS:0130=0100
と表示されてます。DSとかSSって何よと問えば
セグメント・レジスタ名
です。実はBXが指す先とBPが指す先は「異なる」セグメントなのでした。BXはデータセグメントDSを指し、BPはスタックセグメントSSを指します。しかし、ここでは全てのセグメントが同じ番地から始まるTINYモデル(.COM形式)を使っているので、メモリ上はお隣同士だと。
また、よゐこは INC [BX]と INC [BP]のオペコードの長さが異なることにも気づいたでしょう。86命令には結構非対称なところがあるのよ。詳しいことはおいおいと。それは何時だよ?