はじめて読む8086 ノート
ニーモニックとアセンブラ
CPU はメモリからバイトコードを読み取り、対応する命令を解読して実行する。
この「対応する命令」単位でコーディングするのがアセンブリ言語。
DEBUG
で U サブコマンドを利用すると、バイトコードからアセンブリ言語を復元できる。
アセンブリ言語では対応する命令の記号を ニーモニック と呼ぶ。
ニーモニックは オペコード と オペランド で構成される。
高級言語でイメージすればオペコードは関数で、オペランドは引数。
たとえば、上の画像で行けばバイトコード「53」はニーモニック「 PUSH BX」に対応し、オペコード「PUSH」のオペランド「BX」である。
DEBUG コマンドでのアセンブリコーディング
コマンド | 意味 |
---|---|
A | 指定メモリアドレスからアセンブリ言語入力 |
U | メモリ上のバイトコードをアセンブリに復元(ディスアセンブル・アンアセンブルなどと呼ぶ) |
DEBUG
がなかったころは、まず紙にアセンブリコーディングして手動で対応表からバイトコードを作り出し、入力するという苦行をしていたらしい。
レジスタ
レジスタは CPU 内部のメモリ
ただしメモリと違いアドレスでは無く名前で区別される他、バイトやワードという単位ではなく「本数」でとらえる。
バスを介する必要が無いので高速に処理を実施できる。
アセンブリは基本的にこのレジスタを操作して以下の3つの処理をオペレートする。
- メモリ・I/O からレジスタにデータを転送する
- レジスタ内部のデータを用いて演算する
- レジスタからメモリ・I/O にデータを出力する
8086 では 16bit のレジスタが14本用意されている。
DEBUG
の「R」を利用するとマシンのレジスタを確認することができる。
AX から IP まではレジスタの値が表示されている部分。「NV UP EI ...」の箇所はフラグレジスタの状態が略号で表示されている。
最後の行は、次に実行される命令に関する情報が出ている。
この場合は 0100 にある 0F(ニーモニック DB 0F)を実行するということ。
レジスタ一覧
汎用レジスタ
汎用レジスタはデータを記憶しておいたり演算に使えるレジスタ。
汎用という名のとおり、全ては加減算・比較・論理演算に使うことができる。
ただし、それぞれは特有の機能をもっており、 それらと組み合わせて使う命令が決まっている。
また上位8ビット / 下位8ビットで分けて利用することも可能。
その場合、たとえば AX であれば AL が下位(Low)、AH が上位(High)に対応する。
レジスタ | 識別子 | 用途 |
---|---|---|
アキュムレータ | AX | 各種の演算(他のレジスタより高速)・剰余算 |
ベースレジスタ | BX | 特定のメモリを指し示すポインタ |
カウントレジスタ | CX | 転送や繰り返しの数を数えるポインタ |
データレジスタ | DX | データの一時記憶用・AX と組み合わせた 32bit の剰余算 |
特殊レジスタ・インデックスレジスタ
IP レジスタ以外は汎用レジスタと同等の機能を持つ。
ただし、汎用レジスタがデータの演算や記憶を目的とすることに対して、特殊レジスタ・インデックスレジスタはメモリのアドレスを指定することが目的。
SI レジスタが示すアドレスの内容を AX レジスタに転送する、といった用途で用いることになる。
SP, IP レジスタはプログラムでその値を直接利用することはできない。
実行中に間接的に利用している感じになる。
レジスタ名 | 識別子 |
---|---|
ベースポインタ | BP |
スタックポインタ | SP |
インストラクションポインタ | IP |
ソースインデックス | SI |
デスティネーションインデックス | DI |
セグメントレジスタ
メモリアドレスを指定するために使われるが、特殊レジスタ・インデックスレジスタとは趣旨が異なる。
セグメントとよぶメモリ管理のために利用される。
レジスタ名 | 識別子 |
---|---|
コードセグメント | CS |
データセグメント | DS |
エクストラセグメント | ES |
スタックセグメント | SS |
フラグレジスタ
プログラムの実行によって変化する CPU の状態を表すためのレジスタ。
他のレジスタのようにデータの保存・演算に利用することはできない。
たとえば演算結果が正か負か、に応じてフラグが立ったり折れたりする。
これを利用してプログラムの条件分岐を実現できる。
15 | 14 | 13 | 12 | 11 | 10 | 09 | 08 | 07 | 06 | 05 | 04 | 03 | 02 | 01 | 00 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0F | DF | IF | TF | SF | ZF | AF | PF | CF |
スタック
たとえば AX の値を一度待避させておいて、ある計算を行った後に待避しておいた値を使いたい時がある。
このように一時的にデータを保存しておくためには、スタック領域を利用する。
プログラムからはスタック領域のアドレスを意識する必要が無い。
スタックの出し入れを行うと「スタックポインタ」が自動で更新されるから。
スタックポインタはスタック領域のトップを指すレジスタ。
スタックにデータを積むたびに低いアドレスにシフトしていく。
シフト幅は2であるが、これはスタックのデータの出し入れが1ワード=2バイトであるから。
セグメント
セグメントアドレスとオフセットアドレス
20bit 全てを使って表現するメモリアドレスを「物理アドレス」と呼ぶ。
これに対して「セグメントアドレス(8bit)」と「オフセットアドレス(8bit)」にわけてメモリアドレスを表現する方法がセグメント方式。
マシン語プログラミングでは、物理アドレスを直接指定することはできず、必ずセグメント方式でアドレスを指定する。
32AE:0100 // セグメントアドレス:オフセットアドレス
セグメントアドレスはセグメントベースという物理アドレスの基準点をきめるために利用される。
具体的にはセグメントアドレスの最後に 0 を付与して 16バイトおきにセグメントベースをきめていく。
セグメントアドレス | セグメントベース |
---|---|
0000 | 00000 |
0001 | 00010 |
0002 | 00020 |
... | ... |
FFFF | FFFF0 |
オフセットアドレスは、セグメントベースを基準にしたアドレス空間を用いるためのもので 4bit で表現される(つまり最大 64KB)
セグメントベースにオフセットアドレスを加算すれば実際の物理アドレスがわかる。
セグメントの大きさはプログラムやデータのサイズに合わせてきめるので、物理メモリ上には様々なサイズのセグメントが並ぶことになる。
32AE0 // セグメントベース
0100 // オフセットアドレス
-----
32BE0 // 物理アドレス
セグメントレジスタ
アドレスの指定には 16bit のレジスタを2本使う(セグメントアドレスとオフセットアドレス)
セグメントアドレスを指定するのに使うのがセグメントレジスタ。
MS-DOS ではプログラム起動時に自動でメモリ空間にセグメントを割り当てて、セグメントレジスタにセットしてくれる。
レジスタ | 名称 | 機能 | 用途 |
---|---|---|---|
CS | コードセグメント | マシン語プログラムが格納されているセグメントアドレス | CPU がマシン語命令を読み込む時に自動で利用 |
DS | データセグメント | データを格納するセグメントアドレス | メモリとレジスタでデータ転送するとき自動で利用 |
ES | エクストラセグメント | DS にくわえてデータ格納が必要な時に利用 | 同上 |
SS | スタックセグメント | スタック専用のセグメントアドレス | スタック操作時に自動で利用 |
以後は、全てのセグメントレジスタが同一のセグメントアドレスを指すケースのみを考慮して学習を進める。
セグメント方式の利点と欠点
68k CPU 等では1つのレジスタで物理アドレスを直接指定できるが、なぜ 8086 では2つのレジスタを利用するセグメント方式なのか?
それは多くのプログラムで 64K バイト以上のデータにアクセスしたり、64K バイト以上のプログラムが必要になることがないから。
セグメント方式はセグメントが設定された後はオフセットアドレスのみ取り扱えば良い。
そのため、物理アドレス方式に比べると CPU が高速にアドレス計算を行えるというメリットがある。
ただし、デメリットは完全にこれの裏返し。
つまり 64K バイトの制限に引っかかると、セグメントアドレスの切り替えが必要になりパフォーマンスが落ちてしまう。
プログラム実行のメカニズム
現在実行されているコンピュータの多くはフォン・ノイマンの提案した動作原理に基づいている。
即ち「プログラムはメモリに格納され、CPU が命令を順次読み出して実行する」スタイル。ストアドプログラムシーケンシャルコントロール方式。
この方式ではデータ領域をマシン語として解釈してしまうとプログラムは暴走してしまうため、気をつけなければいけない。
プログラムのトレース
コメントでご指摘頂いていますが下記では
CMP AL, [DI]
とすべきところをCMP AL, [D1]
と間違えていますので参考にしないでください
オフセットアドレス 201 に 1A が存在するか調べるプログラムをアセンブリで入力してみる。
アセンブリ入力 | 逆アセンブル |
---|---|
|
| {} と [] を見間違えたけど指摘してくれた(当たり前か)| この場合オペコードが最初の1バイトに対応してて残りがオペランド |
これの動作をみるために、以下コマンドをうって 201 に 1A を書き込んでおく。
D 200 201 // メモリ内容確認してから(このときは 4D E2 だった)
E 200 31,1A // 200 に 31, 201 に 1A を書き込む
D 200 201 // 再び確認
トレースオン!
R コマンドを打って「セグメントレジスタ(DS, ES, SS, CS)のアドレスが同一」であることを確認しておく。
また、次に実行される命令が打ち込んだプログラムの最初のニーモニックであることも確認できる。
それがすんだら、T コマンドを利用してプログラム実行を開始する。
T コマンドは「プログラムを1ステップずつ実行する」効果をもつ。
表示される内容は R コマンドの結果と同じだが、実行する毎にレジスタの値や次に実行されるニーモニックが変化することがわかる。
MOV CX,0100 によって CX レジスタに 0100 が格納されたのがわかる。
プログラムの中身を追うと以下のような動作になる。
マシン語ではバイトオーダがひっくり返る点に注意。
(NOTE) Kobit だとうまくいくんだけど qiita にあげると table タグがうまくレンダリングされないので徐々になおす・・・
マシン語 | ニーモニック | レジスタ | 動作 |
---|---|---|---|
B0 1A | MOV AL,1A | AL ← 1A |
|
B9 00 01 | MOV CX,0100 | CX ← 0100 |
|
BF 00 02 | MOV DI,0200 | DI ← 0200 |
|
E8 02 00 | CALL 010D | SP ← SP - 2 IP ← 010D |
|
3A 05 | CMP AL,[DI] | NC ← CY (キャリーフラグ) |
|
74 03 | JZ 0114 |
|
|
47 | INC DI | DI ← DI + 1 |
|
E2 F9 | LOOP 010D | CX ← CX - 1 IP ← 010D |
|
3A 05 | CMP AL,[DI] | NR ← NZ (ゼロフラグ) NC ← CY (キャリーフラグ) |
|
74 03 | JZ 0114 |
|
|
03 | RET | IP ← 010B SP ← SP + 2 |
|
CD 20 | INT 20 |
|
重要なのは IP(インストラクションポインタ)レジスタ。
CPU は次に実行する命令を CS(コードセグメント)レジスタと IP レジスタを基にしてメモリから読み込む。
IP は要するにオフセットアドレスを指定するレジスタ。
命令を読み込むと自動で IP が進み、命令がパラメータを必要とする物であればそれも読み込んで IP がさらに進む。
条件分岐、サブルーチンの動作は、この IP を能動的に書き換えることで実現されている。