TSG 部報 第 200 号・夏休み号


初心者のための 8086 講座<第3回>

 どうも高野商店です。前回はなんか薄い内容だったので、それじゃあつまらんと言うことで突然ですがgasやります。

gasとはなんぞや?

 gasとはGNU assemblerのことで、その名の通りGNUが作ったアセンブラーです。gasはgcc(GNU C compiler)が吐いたアセンブラーコードを処理するアセンブラーであり、gccをインストールすると漏れなくついてきます。:-)gasにはいろいろなプロセッサ用がありますが、ここでは386用のgasについて述べます。gasはMASM(Microsoft macro assembler)などに比べて次のような利点があります。

  1. プロテクトモード対応である。
  2. それ故、メモリの制限が無い。(つまり1MBの壁を考える必要はない)
  3. もちろん、386、486、の独自命令をサポート。
 まあ1)が言えれば2)3)は当たり前なのですが、、、(^^;;

 欠点としては

  1. マクロが使えない。
  2. リアルモード用の16ビットアドレスをサポートしていない。
ぐらいです。1)はgasがあくまでもアセンブラーに徹しているという性格上仕方のないものです。しかし、gasはgccと抱き合わせてインラインアセンブラーとして使えるのであまりマクロの必要性は感じられません。

gasの記述方

  1. ニーモニックはほぼintel社の表記法と同じで、オペランドがバイト/ワード/ロングなどを示す b/w/lを後に付けます。
  2. 前回ではニーモニックのあとにつけたオペランドの順番はディスティネーション、ソースの順番でしたが、gasの場合これが逆になります。
  3. レジスタの名前には頭に%をつける。
  4. immediateオペランド(つまり直接値)には頭に$をつける。
  5. 絶対番地オペランドには頭に*をつける。
  6. メモリ参照は となります。
 さて、以上のようなことをふまえて前回書いたアセンブラーをgasっぽく書き換えてみましょう。

となります。

 このようにほとんどintel社の命令群と大差がないのですが、infoを読むとごく一部の命令に違いがあることがわかります。その差は後々になってからお話しします。

gas

実際にgccにgasのコードを吐かせてみましょう。

 これはC言語でお馴染みのHello.cです。(注 ここで使っているのは8086のアセンブラなので情報棟のgccでやっても動きません。305のXaでやってみて下さい。)
ちなみに.alignとかはコンストラクタと呼ばれ、i386/387の命令を発生する以外の直接gasに対して命令を送るものです。また、レジスタの頭にeのついているもの(%eaxなど)は32ビットレジスタを意味します。

解説

 まず代表的なコンストラクタを説明します。

  1. .text 命令部に入れることを定義します。
  2. .globl ラベルを他のモジュールから参照可能にする。
  3. .align .align nで次の文を 2n の倍数の番地に置く。
  4. .ascii label: .ascii "文字列" の形式で文字列を定義する。
 上でとりあえず使っているのは以上の3つだけです。ところで3)はどんな意味があるのでしょうか?上の例では.align 4となっていますが、gccによって出力されたアセンブラーのリストはすべての関数の頭にこれがついてます。これは4バイトごとにメモリにデータが割り当ててあると4バイトのデータ(long型のデータ)や8バイトのデータ(double型のデータ)を高速にアクセスできるようになるわけです。その分メモリの空きが多くなるのでメモリをバカ食いします。(^^;

 さてここで初めて出てきたcallとjmpについて説明します。

ジャンプ文

 プログラムは常にメモリアドレスが低いところから高い方に向かって格納されていますが、途中でデータの結果によって処理の仕方を変えなければならないことが起きてきます。ジャンプ命令には無条件ジャンプと条件ジャンプとがあります。ジャンプ命令を実行すると、いままで通り次のメモリに入っている命令を実行するのではなくジャンプ命令で指定したアドレスへ飛んでいきます。無条件ジャンプはどんな場合でも指定されたアドレスにジャンプします。条件ジャンプはある条件が満たされたときのみジャンプします。条件が合わなかった場合、普通に次の命令を実行します。ところでその条件とはいったいなんでしょうか?以前、説明したフラグレジスタを条件材料として条件ジャンプを実行するわけです。これによってC言語でよく用いられるif文等の条件分岐が可能になります。

 まずは無条件ジャンプです。gasでは

の様に書きます。labelは上の例ではL1:のように「ラベル名:」で指定します。
これを実行すると、無条件にL1:以下の命令を実行します。
cmp 0x02,%alを実行したあと

となります。上に挙げた例は2つのを値をそれぞれ符号なしの1バイトデータとしてみなした場合です。符号ありの場合は最上位ビットある無しで負の数かそうでないかを決定します。だからたとえば0xffは符号無しの場合は255を表し、符号ありの場合は-1を表します。下の命令は符号ありの場合の条件ジャンプです。

符号あり無しの話をもう少し詳しくしてみます。
符号無しの場合、16進法と10進法との対応関係を考えてみると

これが符号ありの場合になると...

まったく違うことがわかります。だからもし

としたあと、jbとjlは全く違った意味を持つことになるわけです。

コール文

 さて次はコール文です。プログラムは普通、低いところから高いところのアドレスに向かって実行されていきますが、その中で同じことを何度も書かなくてはいけなくなるなるときがあります。C言語ではこれを一つの関数としてまとめ、必要なところでその関数を呼び出します。このようにプログラム本体とは別の場所に作られ、プログラム本体から呼び出されるためのあるまとまった処理を行うプログラムのことをサブルーチンと呼びます。プログラム本体のことをメインルーチンと言います。上の例では__mainと_printfがサブルーチンとして呼び出されています。call _printfと呼び出されている部分がそれです。ここには_printfサブルーチンは書いてありませんが、それはどこにあるのでしょうか?_printfや___mainサブルーチンはライブラリーの中に納められていて、C言語ではリンクする段階で一緒に結合されます。だから、アセンブラーの中に書いて無くても大丈夫な訳です。
 サブルーチンが必要なもう一つの理由としてはプログラムを見やすくするためです。たとえ一度しか実行しない処理であっても、ある程度まとまったものならサブルーチンにしてしまった方が、プログラムの構造がわかりやすくなり、デバッグ等の作業も楽になります。
 CPUの命令では、メインルーチンからサブルーチンを呼び出すcall(コール)命令と、サブルーチンからメインルーチンへ戻るret(リターン)命令が用意されています。この二つは常に対になって用いられます。簡単例を出してみると...

むう、なんかめちゃくちゃつまらないプログラムですが(^^;これをアセンブラに翻訳すると...

 そのままgccに吐かせたコードではなく少しいじくってあります。
 上で見てもらってもわかるようにcallで呼び出されて、retで再びもとに戻ってきていることがよくわかります。もちろん_printfなども実際にはライブラリにret命令が記述してあるわけです。
 さて、関数funcには引数として2が渡されているわけですが、この引数はアセンブラー上ではどのように実現されているのかを見てみます。うえのメインルーチンで怪しそうなところを見ていくとpushl $2という命令がcall _funcの前にあるのがわかります。push命令は、前回スタックの操作のところでやりましたが、gccでは引数を渡すのにこのスタックの操作をすることによって実現しているわけです。サブルーチン_funcを見てみるとpop命令を使ってではなく、sp(スタックポインタ)を参照してmov命令を使って引数を参照する手段を取っています。ここでbp(ベースポインタ)を使ってメモリを参照していますが、セグメントレジスタを指定していません。一般にbpを指定した場合、セグメントレジスタは自動的にss(スタックセグメント)になります。

スタックについての復習

 スタックは元来アドレスを気にせずに簡単にデータの出し入れができるようになっている仕組みのことです。スタックにデータを積むと、最初に取り出せるのは最後に積んだデータです。スタックにデータを積む命令がpush命令で、スタックからデータを取り出す命令をpop命令と言いました。spはこのスタックの一番上に積まれているデータのアドレス(オフセット)を指すようになっています。たとえばaxレジスタをスタックに出し入れする命令は

です。またフラグレジスタに関しては特別に

というフラグ命令が用意されています。

 さて、次回は実際にgasをインラインアセンブラとして使う方法と、アセンブラ命令の続きをやります。


[目次へ]

g541119@komaba.ecc.u-tokyo.ac.jp