doc drawn up: 2004-12-05 .. 2004-12-27

組立言語(≒機械言語)

はじめに

最近、組立アセンブリ言語(≒機械マシン言語)に興味があって、いろいろと勉強してみた。僕にとっては、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)。僕のように、組立言語を迂回して先に高級言語を習得してしまった人には、是非一度、触れてみておいて欲しい。

Chapter 1: 組立言語のイメージ

組立言語(≒機械言語)というのは、極論すれば、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 を利用して、直接的な組立言語レベルのプログラミングを行う手間を省くことができます。しかし我々一般のプログラマにとっては必要ないにせよ、ドライバそのものは、まさしく組立言語、すなわち「レジスタ変数の操作」というレベルのプログラムによって実現されているのだということは、理解して知っておいて損はないでしょう。

高級言語は、あくまでもこのような組立言語レベルの「レジスタ変数の操作」の組合せからなる操作を、高度に抽象化した記述方法によって行う言語体系だと考えることができます。

Chapter 2: 早速プログラミング!

前置きはそのくらいにしておいて、では早速、組立言語を使ったプログラミングを実際に始めてみましょう! この章においては、特に特殊な開発環境を用意せずとも、普通の Windows 環境において組立アセンブル作業によってちゃんと動く実行ファイルを作成できてしまうのだという事実を、主にデバッガの操作の中で説明します。組立言語そのものについては、次の章以降でしますので、この章においてはデバッガを使った作業の流れについて、大雑把な感じだけでもいいので、実感としてつかんでもらえればと思います。

用意するもの

Microsoft 社が Windows® に標準で組み込んでいる debug コマンドを使って組み立てるアセンブルする註 8)ことができます。コマンド・プロンプトから、debug と打ち込んでデバッガ(註 09)を起動してみて下さい(註 10)。

Microsoft (R) KKCFUNC バージョン 1.10
Copyright (C) Microsoft Corp. 1991,1993. All rights reserved.

KKCFUNC が組み込まれました.

マイクロソフトかな漢字変換 バージョン 2.51
(C)Copyright Microsoft Corp. 1992-1993
-

-’(ハイフン)の隣に点滅するカーソルが表示された状態でコマンド待ちになったと思います。ここで 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! という文字データの並びを書き込んでいます。データが十六進の数値ではなく文字データであることを示すために、'(シングル・クォーテーション)で囲む必要があります。,(カンマ)で区切って da を入力しているのは、改行コード(註 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
00016 バイト書き込み中.
-

ファイルサイズ 22 バイト(十進表記)の HAPPY.COM が作成されたはずです。

q コマンドでデバッガを終了し、HAPPY.COM を(コマンド・プロンプトの中で)実行してみて下さい。

-q

C:\asm>happy.com
Happy!

C:\asm>

「Happy!」というメッセージが表示されましたね。

註記

  1. 他の 3 言語に関しては、与えられた具体的な事例から抽象的なアルゴリズムを導出し、さらにその抽象的なアルゴリズムに対してコーディングを行うという、2 段階の思考作業を要求される。一方、組立言語に関しては、具体的な事例から抽象的なアルゴリズムを導き出すという段階が存在せず、いきなりアルゴリズムを実現するためのコーディング段階に関する問題を解くだけである。その上、試験で使われる組立言語(CASL II)は実在するものではなく基本情報技術者試験用に用意されたあくまでも架空の言語体系であり、問題文にその仕様書が添付されているので、命令語を憶える必要は一切ない。
  2. 基本情報技術者試験を受験するような若い学生には、「組立言語なんて時代遅れで非実用的だし、そもそも CASL II なんて架空の言語体系を憶えても実際に将来仕事で役に立つことはないから無駄」とバカにして切り捨てようとする意見も少なくないみたいだが、『朝三暮四』だと思う。所詮、基本情報技術者試験レベルの問題で問われる程度の水準は、実際に言語を習得して使っていくレベルからすれば「ほんのさわり」に過ぎない。むしろ学習過程であるからこそ、組立言語に触れてみて、歴史の追体験による技術体系コンテクストを多少なりとも知っておくことこそ、重要で後々役に立つ勉強になる内容だと思う。
  3. 例えば、西久保靖彦『よくわかる CPU の基本と仕組み』(東京都、秀和システム、2004-09-13)という本が、電気回路的に CPU の動作をいかに実現しているかということを知るのに参考になります。
  4. 二進算盤というのは単なる概念的な例え話というよりも、物理的な挙動を理解するための具体的なイメージとして思い描いて下さい。電気回路内のあるゲートにおける電圧の高・低によって、論理的なデータビットの「1 か 0 か」の状態が表現されています。これはまさしく算盤の珠が上にある(= 1)か、下にある(= 0)か、ということと同じです。さらに、算盤では桁上がりが生じると隣の珠を動かします。同様に、CPU 回路においては、隣のビットを表現しているゲートに伝播してそこを 1 にします。人間用の十進算盤と電気回路用の二進算盤の違う点は、十進算盤では一つの珠が 10 段階で上下し 10 段階を超えると隣の珠に影響するのに対し、二進算盤では一つの珠(ゲート)が 2 段階で上下し、2 段階を超えると隣の珠に影響するという点だけが違っています。人間が算盤を使う場合にせよ、電気回路が算盤を使う場合にせよ、決められたルール通りに珠を動かして行って、最後に計算が終わった段階で、珠の状態を見て、答えを知ります。二進数の演算で、シフト演算するとそれが自動的に 2 のべき乗になるという仕組みになっているのも、、人間が算盤を使って何も考えずにルール通りに珠を動かして計算することができるのと同じです。
  5. Intel Architectureインテル・アーキテクチャ の 32 ビット版という意味で「IA-32」と名付けられており、Software Developer's Manualソフトウェア開発者マニュアル日本語)として入手できます。
  6. 「I/O」とは「Input/Output」の略で、そのまま「入力/出力(=入出力)」と訳せる言葉です。例えば、C 言語でお馴染みの stdio というライブラリは、STanDard Input/Output(標準入出力)を略した名称です。
  7. ちなみに筆者の環境は CPU: Pentium III で OS: Windows 2000 です。Windows XP の場合は基本的に何も問題はないでしょう。Windows 9x や MS-DOS の場合もおそらく大差はないと思いますが、確認は取っていませんので、あらかじめご了承下さい。
  8. アセンブリ言語を使って機械言語を生成することを「アセンブルする」と言います。英語の動詞「assembleアセンブル」を語源として、「アセンブルする言語」という意味で「assemblyアセンブリ」、「アセンブルする者(=プログラム)」という意味で「assemblerアセンブラ」が派生しています。
  9. debuggerデバッガ は、「デバッグする者(=プログラム)」という意味の言葉です。そして、debugデバッグ とは「バグを解消するde」という意味の動詞です。bugバグ はプログラムの欠陥のある箇所を指す言葉であることは、皆さんご存じでしょう。
  10. Microsoft の標準デバッガは必要最小限の機能しか付いていませんが、様々な機能を持った強力な専用デバッガというものも存在します。例えば、OllyDbg日本語化パッチ)というデバッガはフリーかつ大変人気があるソフトです。もし、本格的な組立言語プログラミングをするのであれば、試してみると良いでしょう。
  11. アドレスをこのようにセグメントとセグメント内のオフセットの組み合わせで表現する(あまりスマートとは言い難い)面倒なやり方は x86 系 CPU 特有の方式で、他の CPU では単に一つの実アドレスを示すだけで済むものがあり、あまり評判の良くないものですが、歴史的な経緯(古い CPU との互換性を優先しながら拡張してきた)によってこのような仕様となったようです。
  12. 0000 - 00FF までの部分は、プログラムにコマンドライン・オプションの内容を渡すためにそのデータを格納する領域として使われる、というのがその理由のようです。デバッガで a コマンドを入力すると、オフセット・アドレス 0100 が最初に表示されたのはそのためです。
  13. わりと有名な話ですが、MS-DOS / Windows では改行コードを ASCII コード番号 0D(キャリッジリターン)と 0A(ラインフィード)の 2 文字を組合せて表現します。
  14. dumpダンプ という言葉は、「吐き出す」とか荷物をドサッと「積み降ろす」というようなニュアンスの言葉です。
  15. w コマンドはデフォルト(何も指定しない場合)ではプログラムの開始位置を 0100 とみなしています。w コマンドに続けてアドレス値を指定した場合は、指定したアドレスから、BX:CX の値で示されたサイズを書き出し対象とします。

<coding>