doc drawn up: 2004-12-05 .. 2022-01-26
最近、組立言語(≒機械言語)に興味があって、いろいろと勉強してみた。僕にとっては、8bit パソコン時代から、憧れだった機械言語だが、中学生時代に一度挑戦してみたものの、なんとも訳がわからずに挫折した経験がある。その後、独学で結果的に初めて自分が本格的に使いこなせるようになった言語である Perl を習得し、Perl プログラミングを通じて、コンピュータのプログラミング・アルゴリズムに対する知識が少しずつ深まってきた。
知人が基本情報技術者試験を受験しようとしているので、組立言語を教える必要が生じた。基本情報技術者試験には、プログラミング言語に関する問題が出題されるが、プログラミング言語としては、C、COBOL、Java、アセンブリの 4 種類のうちのどれかを選択するようになっている。過去問に目を通してみたところ、特にプログラミング言語初修者にとっては、明らかに組立言語が他の 3 言語に比して問題が単純で有利(註 1)なので、組立言語を選ばない手はないという結論に達した。
さらに、自分自身の動機としても、愛用する Perl 言語の次世代版である Perl 6 のコンパイラのターゲットとなる、仮想マシンである Parrot が、組立言語なら直接動かすことができるという事実を知ったからだ。Perl 6 コンパイラが完成するのはまだ当分先の話のように思えるので、組立言語さえ使えたら、Parrot を今すぐにでも使うことができるというので、急にやる気が出てきた。
以上 2 点の理由で、組立言語が個人的に“旬”となっていた。そんな折、近所の市立図書館で購入されたばかりの情報系の参考図書、西久保靖彦『よくわかる CPU の基本と仕組み』(東京都、秀和システム、2004-09-13)というものを手にし、昨日(2004-12-04)読み終えたばかりである。この本は、機械言語の解説が目的ではないが、機械言語の理解のために非常に役に立つ本だと思った。
今まで、機械言語の理解を、機械言語の解説本だけで行おうとしていたから、理解に苦しんだのだということがわかった。機械言語というものは、CPU という物理的なアーキテクチャを直接的に反映して定義されている。つまり物理的なアーキテクチャさえ知れば、機械言語が「なぜそのような(例えば、二進数だとかの)意味不明・七面倒臭い手法を採るのか」が一目瞭然なのだ。それを、物理的なアーキテクチャというコンテクスト(文脈)なしにいきなり「機械言語はこうこうこうなってますよ」と解説されたところで、わかりっこないのだ。日本語を母国語とする者にとっては、文字はその音声言語を視覚的な記号である文字に転写するだけの作業であるのと同様に、マシン・アーキテクチャを知っている者にとっては、機械言語はそのマシン・アーキテクチャに基づいた CPU の挙動を命令語のデータとして書き連ねる作業に過ぎないのである。
このような物理的アーキテクチャの理解を背景とした機械言語レベルの知識があると、C 言語その他の高級言語の理解は比較的容易になるように思える。C 言語その他の高級言語は、元々組立言語しかなかった時代にその組立言語をベースにしてより高度に抽象化した記述方法を生み出していった結果の所産なのだ。ある高級言語を習得した人が次に別の高級言語に乗り換えようとしたらゼロからの再出発同然となってかなり大変な努力を要するが、組立言語の知識がある人なら、一旦、機械言語レベルに立ち返って、そこから新しく習得しようとする高級言語の体系を眺めながら学習してみれば、比較的すんなりと習得できるに違いない。つまり、組立言語は、コンピュータ言語の多言語習得における軸足とすることのできる言語である(註 2)。僕のように、組立言語を迂回して先に高級言語を習得してしまった人には、是非一度、触れてみておいて欲しい。
組立言語(≒機械言語)というのは、極論すれば、CPU 内部に物理的に実装された「レジスタ」と呼ばれる特殊な変数記憶領域を操作するコンピュータ言語だと言えます。高級言語においては変数というものは論理的な存在のデータとして扱われます――だから、ユーザが好きなだけ変数を作成して使うことが可能です――が、組立言語が扱うレジスタ変数は、あくまでも CPU というハードウェアに物理的な電気回路としての実体を持ち、それによって必然的に生じる制約が、通常のデータ変数とは大きく異なった特徴へとつながっています。
CPU という機械は、電気的な算盤を回路によって実現したものです。つまり、電気的に回路として実現可能なものである必要があるため、例えば人間にとって扱いやすい十進数ではなくて二進数を基本としていたりというような、コンピュータ技術特有の妙ちくりんな幾多の手法が使われているのです。これらの(二進数だとかの)コンピュータ技術の基本的な知識は、情報工学関係の学生さんであれば必ず教科書で一通り目にすることでしょう。基本情報技術者試験等の資格試験において必須の知識です。しかし、「何のために?」二進数だとかの手段を「わざわざ用いるのか?」という理由までは、基本的な教科書の知識を憶えただけでは見えてきません。それはすでに情報工学の範囲を超えて、電気工学の話をある程度知る必要があります。
電気工学的な話については、参考図書(註 3)等に譲りますが、物理的な特性から、CPU は、二進算盤(註 4)が幾つか集まったセットで構成されたものとみなせます。この「二進算盤のセット」という認識を前提にすれば、組立言語の特徴というものを理解するのが早くなると思います。二進算盤がすなわちレジスタであり、そのレジスタの状態を操作するのが、組立言語(≒機械言語)というわけです。
CPU はレジスタの状態によってどのような種類の操作を行えばいいのかを決定し、操作を行います。一方その操作の結果によって、レジスタの状態が変更されます。つまりレジスタは操作に影響を与え、操作はレジスタに影響を与えます。そのような循環によって CPU の処理が進んでいきます。
例えば、後の操作によって失いたくない計算結果などの情報は、レジスタから CPU 外部のメモリに複写しておくことで対処します。このレジスタからメモリへの情報のコピー操作もまた、特定のレジスタの状態によって指示します。レジスタが複数あるのは、そのように、操作の種類を伝えるために使うべきレジスタと、計算結果などを保持しているレジスタというように、同時にいくつかのレジスタが併存している必要があるからです。
「レジスタ変数にどのような値をセットすれば、どのような操作が行われるのか?」という取り決めは、CPU 毎に規格が定まっています。例えば、Pentium® シリーズのような x86 系 CPU の場合は、インテル社が規格を設計し、公開情報として入手することが出来ます(註 5)。
ちなみに、「CPU 内部でレジスタ変数をいじるだけで、どうやって画像を表示したりするようなことができるようになるの?」と疑問に思われるかもしれません。プログラミング言語を覚えて、例えばゲームのようなソフトウェアとして具体的なプログラムを作るつもりでいる人には、「レジスタ変数の操作」なんて話は抽象的で意味が薄いように感じられるかもしれません。画像を表示したりするようなことは、「入出力」――いわゆる「I/O(註 6)」――の話になります。メモリ空間の特定の場所に、例えばビットマップデータを書き込むことによって、画面にビットマップに従った映像が描画されます。つまり、CPU 内部においてレジスタ上でデータを適切に加工するなりして、そのデータを特定の場所のメモリ空間にコピーすることによって、好きな映像を画面上に表示することができるのです。このメモリ空間は、あくまでも論理的なもので、実際のメインメモリ(DRAM)であるとは限りません。ある特定の番地に割り付けられた論理的なメモリ空間は、実はビデオカード上のビデオメモリ(VRAM)であることもあります。しかし、我々プログラマが意識するのは論理的なメモリ空間だけでよく、メインメモリかビデオメモリかという違いは、OS やドライバの方で面倒を見てくれるので気にしなくて済みます。
現実問題としては、そういった I/O に関するプログラミングは大変な労力を要するので、一般のプログラマが組立言語でソフトウェアを作成することはあまりありません。ビデオカードなどのハードウェアベンダ側で、「ドライバ」という形で組立言語レベルのプログラムを一手に引き受けて行い、我々はそのドライバを通じて、高級言語からライブラリ関数や API を利用して、直接的な組立言語レベルのプログラミングを行う手間を省くことができます。しかし我々一般のプログラマにとっては必要ないにせよ、ドライバそのものは、まさしく組立言語、すなわち「レジスタ変数の操作」というレベルのプログラムによって実現されているのだということは、理解して知っておいて損はないでしょう。
高級言語は、あくまでもこのような組立言語レベルの「レジスタ変数の操作」の組合せからなる操作を、高度に抽象化した記述方法によって行う言語体系だと考えることができます。
前置きはそのくらいにしておいて、では早速、組立言語を使ったプログラミングを実際に始めてみましょう! この章においては、特に特殊な開発環境を用意せずとも、普通の Windows 環境において組立作業によってちゃんと動く実行ファイルを作成できてしまうのだという事実を、主にデバッガの操作の中で説明します。組立言語そのものについては、次の章以降でしますので、この章においてはデバッガを使った作業の流れについて、大雑把な感じだけでもいいので、実感としてつかんでもらえればと思います。
Microsoft 社が DOS 時代から 32bit 時代までの Windows® に標準で組み込んでいた debug コマンドを使って組み立てる(註 8)ことができます。コマンド・プロンプトから、debug
と打ち込んでデバッガ(註 09)を起動してみて下さい(註 10)。
C:\>debug -
‘-
’(ハイフン)の隣に点滅するカーソルが表示された状態でコマンド待ちになったと思います。ここで a
コマンドを入力するとアセンブルモードになります。
-a 2CD1:0100
2CD1:0100
というアドレスが表示されます。前半 4 桁はセグメント・アドレスと言って、必ずしも今回のように 2CD1 となると決まってはいません。後半 4 桁がオフセット・アドレスと言って、セグメント内の最初の位置(すなわち 0000)から後ろにどの程度離れた地点なのかを示すものです(註 11)。
とりあえず、「お気楽組立言語」としては、後半 4 桁のオフセット・アドレスだけを意識することにして、前半 4 桁のセグメント・アドレスは気にしないことにしましょう。すなわち、適当な一個のセグメントの中に収まる「ちびっ子」プログラムを作る分には、セグメント・アドレスの存在は無視することができます。
MS-DOS / Windows の最も単純なプログラム(COM スタイルのプログラム)では、ユーザプログラムを 0100 番地から開始します(註 12)。以下の図にならって一行ずつプログラム文を打ち込んで下さい。010B 番地でプログラム本文は終わりなので、010D 番地では何も打ち込まずにリターンすると、アセンブルモードが終了して、再び‘-’が表示されると思います(この組立・プログラムの内容解説については、次章で行います)。
-a 2CD1:0100 mov ah, 9 2CD1:0102 mov dx, 10d 2CD1:0105 int 21 2CD1:0107 mov ah, 4c 2CD1:0109 mov al, 0 2CD1:010B int 21 2CD1:010D -
次に e
コマンドを使って、エディットモードでデータを入力します。エディットモードはアセンブルせずに直接データ内容そのものを指定した番地から書き込みます。
-e 10d 'Happy!', d, a, '$' -
ここではプログラム本文の末尾の次の番地である 010D 番地から Happy!
という文字データの並びを書き込んでいます。データが十六進の数値ではなく文字データであることを示すために、'
(シングル・クォーテーション)で囲む必要があります。,
(カンマ)で区切って d
と a
を入力しているのは、改行コード(註 13)に相当する文字コードを使うためです。最後の文字データ $
は、文字列の終わりを示す意味で使わるトークン(目印)です。
次に、確認のために、d
コマンドを使って、ここまで入力したメモリの内容をダンプ(註 14)します。
-d 2CD1:0100 B4 09 BA 0D 01 CD 21 B4-4C B0 00 CD 21 48 61 70 ......!.L...!Hap 2CD1:0110 70 79 21 0D 0A 24 00 00-00 00 00 00 34 00 C0 2C py!..$......4.., 2CD1:0120 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 2CD1:0130 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 2CD1:0140 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 2CD1:0150 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 2CD1:0160 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 2CD1:0170 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ -
010D 番地から 0112 番地に相当する部分に、文字列 Happy!
が格納されているのが確認できると思います。そしてそれに続けて、2 文字の表示不能文字 0D と 0A(改行コードに相当)を挟んで文字列の終わりを示す $
が 0115 番地に格納されています。
このメモリダンプ表示の見方についてわからない人のために解説しておきます。左側がセグメントとオフセットの組合せからなるアドレスを示していることはアセンブルモードの表示と同じなのでわかると思います。中央の部分がメモリの内容を十六進数 2 桁の数値で示しています。各行の左端のアドレスを基準として、左から右に向かって順番に +0, +1, +2, ... +9, +A, +B, ... +F したアドレスに格納されている 1 バイト(00 - FF)のデータがそれぞれ示されています。つまり、右端の基準アドレスに対して、その 1 桁目の数値を 0, 1, 2, ... 9, A, B, ... F に置き換えたものがそのデータのアドレスと考えればいいのです。
+0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F
2CD1:0100 B4 09 BA 0D 01 CD 21 B4-4C B0 00 CD 21 48 61 70 ......!.L...!Hap
2CD1:0110 70 79 21 0D 0A 24 00 00-00 00 00 00 34 00 C0 2C py!..$......4..,
さらに、右端の部分の表示は、中央の部分の数値データがそれぞれ「ASCII 文字データであったとしたならば」という前提で置き換えて表示したものです。文字データでない、機械言語の操作命令のための数値データに関しては、でたらめに表示されることになります。また、表示不能の文字は、‘.
’で示されます。
ちなみに、このダンプリストでは、011C から 011F 番地の辺りに 00 ではない何かの値が表示されていますが、別の何らかの操作の際に残ったデータなので、気にする必要はありません。関係のあるデータは、プログラム本文を打ち込んだ 0100 - 010C と、データを格納した 010D - 0115 からなる、0100 - 0115 の範囲(22 バイトに相当)のアドレスに存在するものです。
さて、そこでその 0100 - 0115 の 22 バイトの範囲のプログラムデータを、実行ファイルとして書き出して保存することにしましょう。
書き出し(write)の方法は、w
コマンドを使うことになります。ただし、w コマンドを実際に使う前に、「書き出すデータのサイズ」と、「書き出すファイル名」をあらかじめ指定しなければなりません。
サイズは BX レジスタの値でセグメント値を、CX レジスタの値でオフセット値を指定します。このプログラムはセグメントをまたがるような大きさではありませんから、BX レジスタの値は 0 となり、0100 番地から始まって最後が 0115 番地ですから、115 - 100 + 1 = 16(これは十六進表記の場合で十進では 22 に相当)が CX レジスタの値であることがわかります。レジスタの値を変更するには、r
コマンドを使います。次の図を参考にして、BX と CX レジスタにそれぞれ値を入力して下さい。
-r bx BX 0000 :0 -r cx CX 0000 :16 -
次に n
コマンドを使って、出力ファイル名を指定します。次の図を参考にして、ファイル名を指定して下さい。
-n happy.com -
これで準備が整いました。w
コマンドを入力して、書き出しを行います(註 15)。
-w Writing 0016 bytes -
ファイルサイズ 22 バイト(十進表記)の HAPPY.COM が作成されたはずです。
q
コマンドでデバッガを終了し、HAPPY.COM を(コマンド・プロンプトの中で)実行してみて下さい。
-q C:\>happy Happy! C:\>
「Happy!」というメッセージが表示されましたね。