LECTURE
H19/10/23記述
アセンブラの話
 予告では「プログラミングの道具」となっていましたが、その道具の中でも最も微に入り細に入りプログラムできる「アセンブリ言語」について語ってみようと思います。
 アセンブリ言語≒マシン語
 CPUがマシン語と呼ばれる数値の羅列しか認識できないという事実は昔も今も変わりません。最初のマイクロプロセッサといわれるi4004(ほとんど電卓専用ですが)も最新のCore 2 Duoも、実行できるのはマシン語だけなのです。i4004は外部バスが4ビットで1命令が8ビット、Core 2 Duoは外部バスが64ビットで1命令が8〜48ビットというように、スケールに差はあるものの基本的な仕組みは同じです。8ビット長の命令であれば、どちらも0〜255の数字に意味があり、それをCPUが命令として読み込むと、特定の動作をしてくれるのです。例えば、i4004が83H(Hは16進数を表す)という命令を読み込むと(バスが4ビットなので8Hを読んでから3Hを読む)、4ビットのアキュムレータ(演算用レジスタ)の内容に3番レジスタ(4ビット)の内容を足し、さらにキャリー(桁上がり情報)があれば1を足します。i4004のアセンブリ言語だと"ADD"と書きます。同様にCore 2 Duoが15Hという命令を読み込むと、32ビットのアキュムレータ(EAXレジスタ)に指定レジスタ(32ビット)の内容を足し、さらにキャリーがあれば1を足します。Core 2 Duoのアセンブリ言語では"ADC"となります。
 このようにアセンブリ言語は、この命令として動作する数値やデータ数値域などに文字を割り当てたものです。つまり、数値と文字が1対1に対応したものなのです。ということは、アセンブリ言語≒マシン語に他ならないのです。これがアセンブラを理解する上で最も大切なことのひとつと言えるでしょう。
 アセンブルの道具
 しかし、これだけだとアセンブラ(=アセンブリ言語をマシン語に変換するアプリケーション)は、単に文字列を数値に置き換えるだけになってしまいます。いえいえ、それだけでは事は済みません。実際には命令の中をビット単位で操作しなければなりませんし、演算式を計算したり、ジャンプ先までの距離を計算したりしなければなりません。特にジャンプ先の距離計算は厄介で、距離によって命令サイズが変化する場合があるので、アセンブラ設計者を悩ませていました。
 風雅システムでは、当時の標準アセンブラであったMASM(マイクロソフト社)ではなく、米SLR社OPTASMを使っていました。8086/286用のアセンブラとしては右に出る者がない高性能アセンブラで、アセンブリ速度が超高速、オブジェクトが小さい(無駄なNOPが全く入らない)という特徴がありました。残念なのはこのアセンブラがマイナーで、「知る人ぞ知る」アセンブラであったことです。私、杉之原名人が秋葉原のSLR社代理店で予約購入したとき、シリアルNo.が「6」だったことからも、そのマイナーぶりが伺えます。価格は約4万円とそれなりに高価でしたが、間違いなくその価値はありました。
 さて、アセンブラがあれば実行可能なアプリケーションが作れるかというと、そうではありません。アセンブラが出力するのは「リロケータブルバイナリ」と呼ばれるオブジェクトファイルなのです。実行可能なオブジェクトを作成するためには「リンカ」というアプリケーションが必要なのです。リンカはアセンブラが出力したオブジェクトファイルを読み込み、実行可能なオブジェクトファイルを出力してくれます。リンカの仕事は大したことではなく、OSの環境に合わせて実行可能なようにヘッダを付加したり、複数個のオブジェクトファイルを連結したりするだけです。CやPASCALコンパイラはリンカを内蔵していて直接実行可能なオブジェクトを出力してくれますから、そういったアセンブラがあってもよかったかもしれません。ちなみに風雅システムでは、リンカもSLR社製のOPTLINK/Compressという製品を使っていました。このリンカは高速なのは当たり前で、実行ファイル(.EXE)を圧縮して、よりサイズが小さくなるようにしてくれるという優れものでした(5万円以上しましたが(;。;))。これにはプログラムを解析しにくくするという利点もありました。
 CPUの仕組みを理解する
 アセンブリ言語に話を戻します。早い話、アセンブリ言語はマシン語そのものであることはご理解いただけたと思います。ということは、CPUごとにアセンブリ言語は異なるということになります。このあたりがC言語などと大きく異なるところです。C言語やJavaはひとつ文法を憶えれば事済みますが、アセンブリ言語はCPUの種類だけ文法があるといっても過言ではありません。でも、心配は要りません。CPUごとのレジスタ構成とアドレッシングモードを理解するだけで、アセンブリ言語はほぼマスターできるからです。【図・1】を見てください。i4004のレジスタ構成です。4ビットCPUなので、計算用のレジスタ(=CPU内部メモリ)は4ビット長しかありません。また、メモリ空間が4096バイトしかないので、プログラムカウンタ(=実行すべき命令のアドレスを保持)やスタック(=一時的にプログラムカウンタの値を保持)も12ビット長です。元来が電卓用のCPUなので、これで十分だったのです。そして、スクラッチパッドレジスタは汎用の変数と思えば良いでしょう。値の一時保持や、アドレス値の一時保持などに使います。外部メモリにアクセスすると時間がかかるので、CPU内部の高速メモリとしてスクラッチパッドレジスタがあるのです。聞き慣れない名称ですが、現在では「汎用レジスタ」と呼ばれることが多いです。
【図・1】 i4004CPUのレジスタ構成
 i4004のアセンブリ言語をマスターするためには、まず以上の点を理解します。そしてアドレッシングモードです。C言語などでは、どの変数も同じように演算ができますが、アセンブリ言語の変数にあたるレジスタは、ものによってできることとできないことがあるのです。これを総じて「アドレッシングモード」と理解して良いでしょう。i4004の場合、四則演算は足し算と引き算しかできません。これも憶えておかないと、プログラムの途中で「あれ?掛け算はどうするの?」ということになってしまいます。現実には足し算やシフト演算などで実現します。また、スクラッチパッドレジスタ同士で演算はできません。演算はAccレジスタを介してしか行えないのです。こういった制約もアセンブリ言語でプログラムする場合には憶えておかねばなりません。さらにi4004では、スタックポインタが無く、スタックレジスタが3本しかありませんから、サブルーチンの入れ子が3段までしかできません。こういったCPUの性質も理解する必要があります。アセンブリ言語の敷居が高いといわれるのは、プログラムの難しさではなく、こういったハードウェア的な性質を理解しなければならないことにあるのでしょう。でも、ひとつアセンブリ言語が使えるようになると、どんなCPUのアセンブリ言語でも、マニュアルを読めば一日でほぼマスターできるようになります。CPUの仕組みを理解していることが大切なのです。
 アセンブリ言語のプログラム例
 私が風雅システムでゲーム製作にあたって使ったアセンブリ言語は「Z-80」と「8086」と「80386フラットモデル」です。どれも、レジスタ構成もアドレッシングモードもかなり異なりますが、マスターするのに苦労はしていません。ちなみに、それまで使っていたアセンブリ言語はMC6809というCPUのものでした(FM−7を持っていたので(^◇^;))
サンプルプログラム1
0001
  LEA EDI,[EBX+EDX*8+200] ; EDI = EBX+EDX*8+200
0002
  MOV ECX,[ESI+3300] ; ECX = *(ESI+3300)
0003
  MOV EAX,[ECX+EDI*4] ; EAX = *(ECX+EDI*4)
0004
MOV EBP, EAX ; EBP = EAX
0005
MOV ECX, [EBP] ; ECX = *EBP
【リスト1】
 Z-80や8086は「直交性」が悪く、レジスタごとにできる演算が決まっていました。これが80386のFLATモデル(Windows95以降)になると一気に直交性が良くなり、どのレジスタもほぼ等価に使えるようになりました。【図・2】はPentium4(Core 2 Duoの32bitモードも同様)の汎用レジスタ群です。32ビット演算に限れば、IPを除いてどのレジスタもほぼ同じように使うことができます。【リスト1】を見てください。Pentium4用のサンプルプログラムです。右側のコメントはC言語での表記です。
【図・2】 Pentium4CPUの汎用レジスタ構成
 掛け算が入っていますが、掛ける数は2,4,8のいずれかになります。LEAは右辺の結果得られるアドレス値をそのまま代入する命令。MOV命令は右辺の結果得られるアドレスで示されるメモリの内容を代入します([]で括られている場合)。ちょっとわかりにくいかもしれませんが、リストをよく見て理解してみてください。C言語と大きく異なるのは、汎用レジスタ(変数)が一般変数にもポインタ変数にもなるという点です。どちらとして使うかはプログラマに完全に委ねられています。
サンプルプログラム2
0001
HELLOW_WORLD:
0002
  MOV EDI,VRAM_TOP
0003
  MOV EDX,MESSAGE_TOP
0004
MOV EBX,0
0005
@LOOP:
0006
  MOV AL,[EDX+EBX]
0007
  AND AL,AL
0008
  JZ @END
0009
  MOV [EDI+EBX],AL
0010
INC EBX
0011   JMP @LOOP
0012 @END:
0013   RET
0014 MESSAGE_TOP:
0015   DB 'Hellow! Assembler world.',0
【リスト2】
【リスト2】もPentium4用アセンブリ言語でのプログラム例です。画面に"Hellow! Assembler world."と表示させるものです。'JZ'(Jump if Zero)命令は直前の演算結果がゼロのときだけラベルに飛ぶ動作をします。INCは内容を+1する命令です。これだけ分かっていれば、あとはおよそ見当のつくプログラムだと思います。アセンブリ言語だと、C言語などでは一行で終わるものがこれだけになってしまいます。でも、やっていることは単純で決して難しくないことも理解してもらえると思います。
 このようにアセンブリ言語は非常に単純な命令を数多く組み合わせてプログラムするという特徴があります。しかし、CやJavaと同様にライブラリが充実していれば、思ったよりプログラムは面倒ではありません。Amaranthが100%アセンブリ言語だけで組んであるのに、コーディング期間がC言語を交えて組んであるAmaranth2とさほど変わらなかったのは、アセンブリ言語で組んだライブラリが充実していたからに他なりません。もちろん、そのライブラリの製作にはそれなりに時間がかかっていますが・・・(BeatViceの資産も使っていますし)
 アセンブリ言語の長所
 さて、どちらかというとアセンブリ言語のマイナス面ばかりが目立っていますが、プラス面もあります。まず、なんと言ってもオブジェクト(実行ファイル)速度でしょう。理論的に最も高速なオブジェクトを出力できるのはアセンブラです。最近のC言語は非常にオブジェクト効率が高くなっていますが、それでも熟練のアセンブリ言語プログラマにはかないません。特に、繰り返し回数の非常に多い小さなループでは威力を発揮します。8086系のCPUには、ある命令を指定回数、高速に繰り返す機能などもあるからです。
 また、SIMD命令の実装には、現在もアセンブリ言語が必要不可欠です。SIMD命令MMXSSEに代表される、ひとつの命令で複数個のデータを同時に処理できる仕組みをいいます。データの扱いが特殊なので、Cなどのコンパイラで使用しようと思うと、必ずアセンブリ言語で組んだライブラリを使用するしかありません。つまり、原則、アセンブリ言語でなければプログラムできないのです。(SIMD命令用の組み込み関数を使う方法もありますが、今のところ完全にコントロールするには至りません。)
 風雅システムの製品では、EVER BRUMEの表示ルーチンにMMXを利用しています。もちろん、この部分はCのプログラムの中にアセンブリ言語を組み入れて実装しています(インラインアセンブラ)。これによって戦闘フィールドのスクロール速度が数十パーセントアップしました。(今にして思えば2倍以上高速化できたと思いますがorz)
 おしまいに

 最近のパソコンは、はじめてパソコンが現れた30年前に比べると、ずば抜けた処理能力(おそらく1万倍以上)と大量の実メモリ(数万倍)を持っています。このため昔に比べるとアセンブリ言語の必要性はかなり小さくなりました。それでも、さきほど述べたSIMD命令の実装やシステムプログラムの記述には現在でも欠かすことができません。また、PIC(ピック)と呼ばれる組み込み用途のチップのプログラム制御などにも現役で使用されています(Cコンパイラも併用されていますが)。

 これは個人的な思いですが、アセンブリ言語でのプログラミングはとても面白いものです。他の言語にはない「不便さ」がまたイイのです。自由であり不自由でもあるアセンブリ言語はパズル的要素も含んでおり、一人で悦に入ることのできる楽しいものです。少しでも興味を持たれた方がいらっしゃったら、ぜひともチャレンジしてみてほしいと思います。

特別講義メニューページへ
ディン:「道具も大切な要因なのね」