1 はじめに
つい先日、こんな記事がバズっているのを見ました。
C言語を理解したいのならアセンブラを学べ(ただしINT 21Hは出てこない) - Qiita
21世紀も1/5が終わろうとし、平成もまさに終わらんとするときに「INT 21H」なんて用語を見るとは夢想だにしませんでした。
あまりにも懐かしすぎたので、勢いで8086の開発環境を作って、適当なプログラムを動かしてみました。
本稿のゴールは下記の通りです。
- FreeDOS(オープンソースのMS-DOS互換OS)の環境を構築し、JWasm(マクロアセンブラ互換)及び関連ツールを動かせるようにする
- 簡単なプログラムを書いてアセンブルし、実行できるようにする
- プログラムの中でINT 21Hを使う(※超重要)
- DEBUG.COM(デバッガ)を実行して、CPUの動作を1ステップずつ追う
※本稿は元記事インスパイア・リスペクト・オマージュ及びその他の何かです。
2 アセンブラを使うにはどうしたらいいのか
IBM PC/AT互換機用のOSとして最も普及しているMS-DOSの5.0若しくは6.2を入手し、MASM 6.0を購入するのがベストです。MS-DOS 3.1には標準でMASMが付いてくるので、お金のない学生は中古でMS-DOS 3.1を購入するのも手かもしれません。
20世紀の終わりぐらいまではこんなこと言ってられましたが、さすがに2018年の現在ではもう購入できませんね。
OSは、FreeDOSという素晴らしいオープンソースソフトウェアがあるので、これを利用することとします。
FreeDOS | The FreeDOS Project
アセンブラには、MASM互換のJWasmを利用します。
ibiblio.org FreeDOS Package -- JWasm (Development)
実行環境は皆様おなじみのOracle VirtualBoxを利用します。
ちなみに、単にプリミティブなCPUの動きを学びたいのであれば、CASL IIという手も無くは無いです。
基本情報技術者試験のプログラミング問題で選択できるCPUですね。
CASL IIシミュレータもありますので、実際にプログラムを書いて動かすことが可能です。
3 開発環境を作ってみよう
※環境構築部分は長くなったので、下記記事に分離しました。
構築手順はこれを参照してください。
FreeDOSの仮想マシンを構築してTCP/IPスタック・8086開発環境を整備する - Qiita
4 早速アセンブラに触れてみよう
まず、作業用ディレクトリを作りましょう。C:\WORKとします。以降の作業はこのディレクトリの中で行います。
C:\> MKDIR WORK
基本のHello World
まずは基本のHello Worldです。以下のコードをエディタで打ち込みます。
当たり前ですが、QiitaのハイライトにMASMはありませんでした。
C:\WORK> VI HELLO.ASM
.model tiny
.code
ORG 100H
START:
MOV DX, OFFSET HELLOWORLD_STR
MOV AH, 09H
INT 21H
MOV AX, 4C00H
INT 21H
HELLOWORLD_STR DB "Hello world with MASM!", 0DH, 0AH, "$"
これをJWasmでアセンブルします。ちなみに、JWasmはJWASMD.EXEとJWASMR.EXEという2つの実行ファイルがあります。基本的にはJWASMR.EXEで問題ありません。(※理由は長くなるので割愛)
C:\WORK> JWASMR -bin -Fo HELLO.COM HELLO.ASM
アセンブルが完了したら、ファイルが実行ファイル(HELLO.COM)が生成されたことを確認します。
確認出来たら、これを実行してみましょう
C:\WORK> HELLO
Hello world with MASM!
無事に「Hello world」が出力されましたね。
デバッガを起動してみる
とりあえずデバッガを起動してみましょう。コマンド名は「DEBUG.COM」です。
C:\WORK> DEBUG HELLO.COM
-
プロンプトが「-」だけになりました。
「?」を入力してEnterを押すことで、ヘルプを表示できます。
最低限抑えておく必要のあるコマンドは以下あたりでしょうか。
- D [range] … 指定したアドレスのメモリ内容を表示する。
- R … レジスタの値を表示する。
- T … コードを1ステップずつ実行する。引数に数字を指定することで、そのステップ数分だけ命令を実行する。
「quit」でデバッガを抜けることができます。
-quit
C:\WORK>
5 解説
ここからは、8086プログラミングに必須の概念・要素を解説します。
アセンブラで扱う数値は16進数で表記した方が分かりやすいので、基本的は16進数で表記します。
また記法は、末尾に「H」を付与することとします。
具体的には、255であればFFHとなります(0xffではなく)。
オペコード・オペランド
Wikipediaの解説がまとまっているので、こちらを参照した方が良いでしょう。
レジスタ
8086には以下のレジスタがあります。
下記記事に解説がまとまっているので、こちらが参考になるでしょう。
ポイントだけ、本稿で解説していきます。
- 汎用レジスタ
- AX
- BX
- CX
- DX
- インデックスレジスタ
- SI
- DI
- スタックポインタ等
- SP
- BP
- インストラクションポインタ(プログラムカウンタ)
- IP
- フラグレジスタ(下記の主要なフラグに他の幾つかを加えた、全部で16bitのレジスタ)
- CF(Carry Flag)
- PF(Parity Flag)
- ZF(Zero Flag)
- SF(Sign Flag)
- OF(Overflow Flag)
- セグメントレジスタ
- DS
- ES
- SS
IP:インストラクションポインタ(プログラムカウンタ)
まず重要なのはIPです。これは、次に実行する命令が格納されているメモリ番地を保持しています。命令が実行されるごとに加算されていきます。
なお、Internet Protocolとは略称が一緒ですが、全く関係がありません。
デバッガで先ほど作ったプログラムを実行し、レジスタの内容を表示させてみましょう
「IP=0100」と表示されていることが分かります。
細かい説明は省きますが、現在、0100H番地の命令を実行しようとしているところです。
また、レジスタの内容の後に、実行するべき命令が表示されています。
次は、DXレジスタに010CHを代入する命令です。
Tコマンドで1ステップ実行してみましょう。
「IP=0103」となりました。また、DXレジスタの値が010CHに変わったことが確認できます。
さらにTコマンドで実行していきましょう。先ほど書いたコードが実行されいてくことが確認できます。
OFFSET疑似命令
先ほど1ステップずつデバッガで実行しましたが、最初の命令は「MOV DX,010CH」でした。
ただ、アセンブラのソースコードは「MOV DX, OFFSET HELLOWORLD_STR」でした。
これはどういうことでしょうか。
まず、もう一度デバッガを実行して、010CH番地を見てみます。
ソースコード上で、「HELLOWORLD_STR」として定義した文字列が格納されています。
先ほどのソースコードに戻りましょう。
プログラムの最後に、「HELLOWORLD_STR DB ~」といった行が定義されています。
これは実行プログラム中に、指定のデータ列を定義する疑似命令となります。
最後の「INT 21H」が格納された番地の後に、「Hello world with MASM!」というデータを格納することを意味しています。
最終的に実行される機械語命令においては、対象のデータが格納されているメモリ番地の値が必要ですが、プログラミング中には分かりません。
そこで、対象のデータ列に対して「HELLOWORLD_STR」というラベルを付与し、これをOFFSET疑似命令を利用することで、抽象的に参照できるようになります。
.model tiny
.code
ORG 100H
START:
MOV DX, OFFSET HELLOWORLD_STR
MOV AH, 09H
INT 21H
MOV AX, 4C00H
INT 21H
HELLOWORLD_STR DB "Hello world with MASM!", 0DH, 0AH, "$" ;←文字列データを定義している。
8086のメモリセグメント方式
8086は16bit CPUとして設計され、アドレスバスは20bitあります。したがって全部で1,048,576bytesのメモリ領域を使うことができます。
ただしこれをリニアに(≒途切れなくシームレスに)扱うことができません。
プログラムからリニアに扱えるのは16bit(=65,536bytes)までです。
これは以下のような、間接的なアドレッシング方式を採用しているからです。
- 物理的なメモリ番地(実効アドレス)を指定するためには、「セグメントアドレス」と「オフセットアドレス」を組み合わせる
- セグメントレジスタは16bit
- セグメントレジスタの内容を左に4bitシフトさせる
- セグメントレジスタを4bitシフトさせた値に、オフセットアドレスを加算する
実行する命令の格納番地は、CS:IP(コードセグメント:インストラクションポインタ)の組み合わせで指定されます。
CS=0E1CH、IP=0100Hとなっているのが見えます。
対象の命令が格納されている、物理的なメモリ番地は下記のように計算します。
- 0E1CHを4bit左にシフトする→E1C0H
- E1C0Hに0100Hを加算する→E2C0H
先ほど見た命令「MOV DX, 010CH」が格納されている物理メモリは、最終的にE2C0Hとなります。
先ほどのプログラムでおさらいしてみましょう。
Dコマンドで、メモリの内容を表示させることができます。
「0E1C:0100」を指定した場合と、「0E1B:0100」を指定した場合を比べてみましょう。
ちょうど4bitシフト分(=16bytes)だけずれて指定されたことが確認できます。
メモリセグメント方式の目的
何故、8086はメモリ空間をリニアに指定できないのでしょうか。これにはいくつかの理由があります。
- 8bit OSのプログラム資産を生かしたかった
- プログラムの再配置が容易
1. 8bit OSのプログラム資産を生かしたかった
16bitマシンが普及する前は、当然ですが8bitマシンが普及しておりました。CPUは8080をベースにしたZ80というものが多く使われておりました。
8080は16bitのアドレスバスを持っており、最大で扱えるメモリ領域は65,536bytesまででした。
8086のメモリアドレッシング方式は、8bitマシン用のプログラムに大きく手を加えることなく、簡単な修正と再アセンブルすることでそのまま動かせることを狙いとしたものだったようです。
私は8bitマシンをそもそも使ったことが無いので、上記の狙いが当たったかどうかは分かりません。
しかし少なくとも、当時のIntelはそう判断したようです。
2. プログラムの再配置が容易
アセンブラのプログラムは最終的に、機械語に変換されて実行プログラムとして保存されます。
実行ファイル中では、処理対象のメモリ番地の具体的な値が保持されています。
もし仮に、アドレスを指定する方法として、00000HからFFFFFHまでの値を直接指定する方法しかなければ、
OSはプログラムをロードする度にメモリ番地を書き換えなければなりません。
メモリセグメント方式を採用することで、OSはアセンブルした実行プログラムの内容に手を加えることなく、
そのままメモリ上にロードすることができます。
ソフトウェア割り込みとハードウェア割り込み
さてここから本題です。たびたび見かける「INT 21H」、これは何ものでしょうか?
まず「INT」とは、「Integer」ではなく、「Interrupt」の略となります。
これはソフトウェア割り込みと呼ばれる命令を実行するものとなります。
割り込みとは一般的に、キーボードやマウスなどの外部機器が操作された際に発生するもので、
それまで実行していたプログラムをいったん中断し、割り込み処理用のプログラムに制御を移すことを言います。
割り込み処理用のプログラムの実行が完了したら、またもとのプログラムに戻し、処理を継続します。
基本的には外部のハードウェアの動作をトリガーとし、既定の処理を行う仕組みとなりますが、
これをプログラムからも発生させることができます。
これをソフトウェア割りこみと言います。
MS-DOSは、OSとしてのソフトウェアルーチンを提供するため、このソフトウェア割り込みを利用しています。
割り込みベクタ
8086には「割り込みベクタ」という概念があり、割り込み処理の管理がしやすいようになっています。
8086はINT 00HからINT FFHまで、合計256種類の割り込みを利用可能で、
物理メモリアドレスの0000Hから03FFHまでの領域を「割り込みベクタテーブル」として割り当てています。
# セグメント・オフセット方式で表すと、0000:0000H~0000:03FFHまでの領域となります。
このテーブルには、割り込み処理用プログラムが格納されている番地が格納されます。
1つの割り込みにつき4bytes(セグメントアドレス:オフセットアドレスの組み合わせ)必要ですので、100H×4bytes分の領域が必要となります。
通常、INT 00HからINT 1FHまではハードウェア割り込みやBIOS用に、INT 20HからINT FFHまではソフトウェアルーチン用に用います。
MS-DOSでは、OSとしての処理を提供するルーチンを、割り込み番号21Hに割り当てています。
アプリケーションプログラムは、各レジスタに必要な値をセットしてINT 21Hを実行することで、MS-DOSの様々な機能を利用することができます。
MS-DOSシステムコールの各機能にどのようなものがあるのか、ググれば見つかります。
このあたりが分かりやすいでしょうか。
スタック
※作成中
おわりに
結局のところ、8086固有の仕様の説明に終始し、あまりCPUの動きの説明になりませんでした。
それと、どの概念から説明するか悩んだ挙句、かなり端折ったので、初心者には分かりにくいかと思います。
ちゃんとやるのであれば、やっぱり、CASL IIで学んだ方が良いかもしれません。
色々と説明不足感がありますが、詳しい記事を書いても需要なさそうなんでどうしよう…
いやいっそのこと、AUTOEXEC.BATとCONFIG.SYS自慢大会でもやるか…?21世紀も1/5を過ぎるというのに?
インターネット老人会案件だ、これ!
参考文献
8086及び32bitの386/486を学ぶには、下記の書籍を順に読んでいくのがお勧めです。
はじめて読む486にいきなり手を出してもかなり苦しいんじゃないかと思います。