アセンブリ言語、はじめの一歩

First: 1-Nov-2005
Last Modified:


まず例題プログラム

2つの数字を足すプログラムをみてみましょう.

以下のようになります。とても簡単に言い表せませんが、解説してみます.

  1. 最初に"."で始まる文字列はアセンブラ擬似命令と呼びます.アセンブラ(コンパイラ)に対する命令です.PowerPC(CPU)に対する命令(Instruction)ではありません.以下のプログラムでは.file、 .globl、 .extern、 .csect( .toc、 .tc、 .byte)が使われています.
  2. しかし"."で始まるものでも.mainやは擬似命令ではなく変数名(ここではグローバルな関数名)です.
  3. 8桁目からはじまる2~5文字の文字列がアセンブリ命令(Instruction)です.全部で命令セットは200個近くあります.mfsprとかstwとかです.似たようなものもありますが,一字でも違っていると機能が異なってきます.よりよいアセンブリプログラムを書く場合その命令をできるだけたくさん知っておく必要があります.
  4. 先頭から始まり":"で終わるところをタグと呼びます.プログラムのある個所(アドレス)にラベルを便宜的に付けます.start:とかreturn:です.プログラム上のこの場所(アドレス)に名前をつけます.
  5. "#"ではじまる文字列はコメントです。この#以下改行までプログラムは読み飛ばします。
  6. 英文ですが命令セットの資料を配布します。今後常にこの実習に持ってきてください。

addtwovals.s

        .file   "addtwovals.s"
        .globl  .main[pr]
        .extern printr31
        .csect  .main[pr]
        
start:  mfspr   0,8             # get LR for save
        stw     0,8(1)          # save LR to SP
        stwu    1,-64(1)        # to get new SP frame with size 64
        addi    3,0,50
        addi    4,0,-50
        add.    31,3,4
        bl      printr31
        lwz     0,72(1)         # get old LR for restore
        mtspr   8,0             # restore LR
        addi    1,1,64          # reload the old SP
return: bclr    20,0            # return

このサイトに掲げられたプログラムは原則として~fukunaga/awork/class/にあります.このプログラムを自分のディレクトリにコピーしろ,といわれたら以下のようにコピーコマンドをプロンプトの後に打ち込んで,エンターキーを押して実行させてください.

> cp ~fukunaga/awork/class/addtwovals.s ./

なおこのプログラムにはもうひとつasio.oというのを必要とします.これもコピーしてください.

> cp ~fukunaga/awork/class/asio.o ./

そしてコンパイル,これは独立したコンパイラもありますがccを使って

> cc addtwovals.s asio.o

実行は

> ./a.out

注意:asio.oのソースプログラムも同じところにあります.asio.sです.でもちょっと高級なアセンブリテクニックを使用しています.いずれそのプログラムも理解できるようになりますが,今はブラックボックスとして捉えていていいと思います.


擬似命令

PowePC機械語命令セットは実行時に解釈され実行されますが、擬似命令はさきほど書いたようにアセンブリ時にその命令が解釈され実行されます。ここでは上記プログラムに出てきたものを順に簡単に解説します。

擬似命令 シンタックス(Syntax) 意味
.file .file StringConstant ソースプログラムのファイル名指定。デバッガ、リンカープログラムがこの情報を利用する。
.globl .globl Name Nameで指定されるシンボルをリンカープログラムに知らせる。
.extern .extern Name Nameで指定されたシンボルは他のソースモジュールで定義されているものと設定。
.csect .csect QualName[,Number]

QualName=[Name][[StrorageMappingClass]]

プログラムコード、あるいはデータをひとまとめのグループとする。そのグループに名前、Storage Mapping Classを与える。範囲は次に.csectが出てくるまで。
.toc .toc ソースモジュールのTable Of Contents(目次)を定義。目次のエントリーは.tc擬似命令で指名する。
.tc .tc [Name][TC], Expression[, expression,...] .tocエントリーに必要な情報を集約する。

リンカープログラム

アセンブリプログラムは通常複数のソースコードモジュールから構成されています。アセンブラはひとつのソースコードモジュール(ファイル)をアセンブルします。そのモジュールに未定義シンボルがあるとエラーメッセージを出してアセンブルをやめます。しかしシンボルを.externとして宣言すると他のモジュールで定義されているはずだから未定義は未定義のままそのままアセンブルは続けろと命令します。その未定義シンボルは他のモジュールで.globl指定されているはずです。アセンブラが未解決のままのシンボルはリンカープログラムがプログラムを作るのに必要なモジュール、ライブラリをさがし.globl宣言されているシンボルをあたります。そんなシンボルが発見されればリンカーは定義済みとしてそのシンボルに最終アドレスを与えます。

リンカーはさらに各モジュールで(仮アドレス)定義されていたシンボルをすべて統合して全体としてそれらに最終アドレスを割り振ります。

.csect

.csect擬似命令の意味は上表で書いたとおりですが、QualNameとして名前を与えます。通常Cプログラムのそれと同様にmainプログラムには.main、関数プログラムにはその関数名を付けます。関数名にピリオドをつけるのはCプログラムでも共通に呼べるように、あるいはCプログラムもその存在を認識できるようにという便宜的な約束事です。

StorageMappingClass

これはいろいろ種類があるのですが、ここでは[rw](read/Write)、[ro](readonly)および[pr](program code)の3種類を覚えておけばいいでしょう。それ以外にもたくさん定義されていますが、ほとんど使う必要はありません。[rw]、[ro]はデータに対する.csectに与えますが、[pr]はプログラムの.csectに与えます。[RW]、[RO]あるいは[PR]と書いても問題ありません。大文字・小文字の区別なし。

.toc/.tcについて

この2つの命令の意味がもっとも抽象的です。しかし我々の実習では以下のような定型的な扱い方しかしないのでその使い方を覚えてください。定数宣言([ro])、変数宣言([rw])のようなものです。実際の変数・定数名は.tc行のラベル部分(先頭)で指定します。以下ではoutfmt、intfmt、constdata、bufferが定数・変数名を表しています。しかしその変数・定数が保つ値は実際にデータが定義されている場所のアドレスです。次に続く.tcでその.csectアドレスが指定されています。


.toc                                    # 定数・変数宣言開始部分(Table of Contents)
outfmt:   .tc  [tc], dataout[ro]        # [tc]は必ず書く。そして定数・変数の定義
                                                # してある.csectのアドレスを指定。出力書式設定
intfmt:   .tc  [tc], datain[ro]         # 入力書式文字列            
constdata:.tc  [tc], a[ro]              # 定数配列
buffer    .tc  [tc], resultbuf[rw]      # 配列変数指定
          .csect dataout[ro]            # このアドレスが実際のデータの定義アドレスになり
                                        # outfmtにはこのアドレス値が入る
          .byte  "Hello, World"
          .byte 0xa,0x00
          .csect datain[ro]
          .byte  "Input a number %d"
          .csect a[ro]
          .long 72,36,18.94.47,29,51,13,65,89

          .csect resultbuf[rw]          # 32long(2バイト)分スペースを確保
          .space 32

実際の命令部(プログラム部)

はじめてアセンブリ言語を学ぶ時,この部分の理解は省略してもかまいません.

この部分の説明にも多少前提知識が必要です。未完成ではありますが福永のまとめたAIX-Assembler(アセンブラ)というページを参照してください。

スタックポインタの操作(プログラムでは赤字で示されている)

スタックポインタに関してはAIX-Assembler(アセンブラ)の副関数リンクの取り決めを参照してください。

mfspr:move from special-purpose register

オペランド(命令に必要なパラメータ)はrDとSPRとなっています。rDは行き先(Destination)GPR番号、SPRは表8-9よりLR(=Link Register)のことです。したがって

mfspr 0,8

とはLRの値をGPR0に移動させろ、ということを意味します。次に

stw:store word

オペランドはrS,d(rA)となっています。この解説をひもとくとrAが0ならb=0、そうでないならbに(rA)、つまりrAのデータを代入してEA(Effectvie Address)はb+dして、そこにrSのデータを書き込め、ということです。word分(4バイト)です。したがって

stw 0,8(1)

とはrSはGPR0、rAはGPR1、dは8なのでGPR0の値をGPR1(スタックポインタ)+8のアドレスに書き込め、ということです。

そして

stwu:store word with update

このオペランドは先のstwと同じrS,d(rA)です。意味もほとんど同じですがupdateとあるのは最後にrAにEAの値を代入して更新しろ、という意味を持ちます。stwではrAの値は古い値を引きずりstwuではrAを新しく計算されたアドレス(rA+d)にするという意味です。

stwu 1,-64(1)

これはGPR1の値(スタックの先頭アドレス)-64という値(これを仮にEAとします)の示すアドレスに今のGPR1の値を代入,GPR1の値をついでにEAの値で更新(アップデータ)しろ,という意味を持ちます.

この3つをまとめて見ましょう。GPR1のスタックポインタはメモリ上のあるアドレスを示していると仮定してください。以下のような図にまとめられます。

命令部分の最後の3行目は

lwz 0,72(1)
mtspr 8,0
addi 1,1,64

となっています。

lwzはload word and zeroという命令です。オペランドはrD,d(rA)です。rA+dで示されるアドレスにあるデータをレジスタrDにロードせよ、ということです。rAはGPR1でdは72です。すると上の図の右の下のピンク部にあるLRの値がポイントされます。それをGPR0にロードしろということです。

mtsprはmove to special registerのことでオペランドはSPR,rSです。rSレジスタの値をSPRで示されるレジスタに移せということで表8-12からSPR=8ということはLRのことなので、GPR0→LRとします。

addiはadd immediateです。オペランドはrD,rA,SIMMとなります。SIMMは16ビット符号付整数を意味します。rA+SIMMの値をrDに入れます。SIMMには直接値を入れます(即値=immediate value)。すると

addi 1,1,64はGPR1の内容+64をGPR1に置き換えることになります。上の図の右のGPR1の示すアドレスがピンクの先頭に戻ったことになります。

これでスタックがメモリの中で拡張されていたのが縮小されもとに戻りました。そして

bclrはbranch condition to LR (link register)はreturn命令です。20とは0x14で表8-8からalwaysを意味します。0はCR(condition register)のビット0の値を条件にせよ、とのことですが、ここではalways設定なのであまり意味を持ちません。CRについてはおいおい触れていきます。

プログラム核心部

残りは

addi 3,0,50
addi 4,0,-50
add. 31,3,4
bl printr31
.....
bclr 20,0

のみです.ここがプログラム核心部です.あらかじめ2値の和を計算しているのですが,その値(50,-50)はプログラムに直接書き込んでいます.これを即値といいます.英語でimmediate value(イメディエート値)といいます.アセンブリ言語ではデータを即値としてもらってもあるいはどこかのアドレスに格納されていても,なにか計算したりするときにはそれらをレジスタをよばれるCPUのなかにあるメモリに書きこんで行います.そのために用意されている一般用途向レジスタが32個あります.GPR0からGPR31と呼ばれています.それらを自由に使う方が外部メモリを参照して計算するよりよほど早いです.上のコードでは50をGPR3に,-50をGPR4にaddi.命令を使って代入しています.addiは

addi rD,rA,SIMM

の形をとります.即値(SIMM)+rA(レジスタ1から31のどれかの保持している値)を計算して,レジスタrDに結果を格納しろ,との命令です.ところがrAの値が0のときは加算をするのではなく,即値をrDに示されたレジスタに代入するのみです.つまりrD=SIMMです.Instruction Setマニュアルの該当項を読んでみてください.なので最初の2行はr3=50,r4=-50となります.そして次のadd.はaddx rD,rA,rBを参照してください.ここで今の場合,xは.になっています.xの値で多少意味合いが異なります.これは全部レジスタでの計算でrD=rA+rBを行う命令です.今の場合,addi 31,3,4となっています.これはGPR3と4のもっているデータを足して結果をGPR31にいれる,となっています.GPR31にいれるのは次のprintr31というサブルーチンがGPR31のデータを標準出力にプリントするきまり(これは私がかってにそう決めた)だからです.blとはbxの項を参照してください.bはbranch(枝分かれ)です.lはリンクレジスタに帰り先のアドレスを記憶させる,という意味をもちます.

最後に

bclr 20,0はbclrxの項を参照してBO,BIの意味を考えます.といってもまだよくわからないと思いますが,とりあえずBIは忘れてよい.BOは今20です.これを2進数で表すと101000です.zビットはいつもクリア(0)とするとみなすと,表8-8の一番下,branch alwayに対応します.常に分岐せよ,と言う意味です.つまりこれで呼ばれた親プログラム(シェル)にもどれ,ということになります.


C.Fukunaga