※この記事はRoger Ferrer IbáñezさんのブログARM assembler in Raspberry Pi – Chapter 1の翻訳です。
私としては特定のアーキテクチャのアセンブリを学ぶより高級言語を学ぶほうが有益だと思います。しかし、386を知る身として(※訳注 i386つまり32ビット版x86アーキテクチャ。かなりややこしいらしいです。)、楽しんでArmアセンブリを学べるのではないかと思いました。マスターすることよりその下で何が起きているかを詳しく理解することを目指していきます。
ARMの紹介
役に立つことだけさらっと。
ARMは、柔軟性を目標にした32ビットアーキテクチャーです。ハードウェア設計の自由度が高いためことはシステムインテグレーターには良いことですが、システム開発者としてはハードウェアの違いに対処するのは面倒です。したがってこの記事では動作環境をRaspberry Pi OS/Raspberry PiモデルBとします(※訳注 訳者はRaspberry Pi 3モデルB+で試し結果が異なればそれを記します)。
いくつかの解説はARM共通ですが、ラズベリーパイにのみ当てはまるのもあります。特に明示しません。ARM 公式サイトにはたくさんの資料がありますのでそれを見てください!
アセンブリ言語の記述
アセンブリ言語は、バイナリコードの上にある単純な構文層です。
バイナリコードはコンピューターで直接実行できるものです。それはバイナリの表現にエンコードされた命令で構成されています(エンコード方法はARMのマニュアルに書いてあります)。バイナリコードを直接書くこともできますが骨が折れます(Linux自体に関係する技術を扱うならそうすることも必要かもしれません)。
そこで、(ARM)アセンブリ言語を記述します。コンピューターはアセンブリ言語を直接実行できないためアセンブリ言語からバイナリコードに変換してやる必要があります。アセンブリコードをバイナリコードにアセンブルするためにはアセンブラが必要です。
アセンブラをasと呼ぶこともあります。同じ理由でGNUプロジェクトのアセンブラであるGNUアセンブラはgasと呼ぶこともあります。この記事ではgasをアセンブルに使っていきます。
vimやnano、emacsなどエディターを開いてください。アセンブリ言語ファイル(ソースファイルとも言う)には慣習で拡張子.sが付きます。なぜ.sなのかはわかりません。
最初のプログラム
とりあえずエラーコードを返すだけのとてつもなくシンプルなプログラムから始めましょう。
/* -- first.s */
/* This is a comment */
.global main /* 'main' is our entry point and must be global */
main: /* This is main */
mov r0, #2 /* Put a 2 inside the register r0 */
bx lr /* Return from main */
first.sというファイルを作成し上記の内容を書き込み保存します。
ファイルをアセンブルするには、次のコマンドを入力します($の後に続くものを書いてください)。
$ as -o first.o first.s
first.oが作られます。実行ファイルを得るためにこのファイルをリンクします。
$ gcc -o first first.o
すべて問題なく進めば、firstファイルができます。最初のプログラムができました。動かしてみましょう。
$ ./first
何も表示されないはずです。実際には何かをしているのにそれだと少々残念です。
というわけでエラーコードを表示してみましょう。
$ ./first ; echo $?
2
うまくいきました。エラーコードが2であることは偶然ではなく、アセンブリコード中の#2によるものです。
アセンブラを実行して、リンカを実行するという流れはじきに飽きるので、以下の`Makefile'ファイルまたは同様のファイルを作成することをおすすめします。
# Makefile
all: first
first: first.o
gcc -o $@ $+
first.o : first.s
as -o $@ $<
clean:
rm -vf first *.o
※訳注
gccは優秀なので以下の方法でも.sファイルを実行ファイルにできます。
Makefileの使い方を知らない人はこの方が楽です。
理解できればいいのは本質的には「オブジェクトコードを作ってからリンクが行われる」ということかなと思います。
$ gcc -o first first.s
解説
物事を少し簡単にするために少しごまかしたところがあります。return 2;をするだけのアセンブリコードにC言語のmain関数を書きました。C言語ランタイムがプログラムの初期化と終了を処理するため、このようにプログラムが簡単になります。今後は常にこの手法を使っていきます。
さきほどつくったfirst.sのすべての行を確認しましょう。
/* -- first.s */
/* This is a comment */
これらはコメントです。コメントは/*と*/で囲まれています。コメントは無視されるためアセンブリコードに説明文を付け加えるのに使えます。うまく機能しなくなるので/*の内側に/*と*/を入れ子にしないでください。(※訳注 C言語と同じく一行コメント//も使えます。)
.global main /* 'main' is our entry point and must be global */
これはGNUアセンブラへのディレクティブ(疑似命令)です。ディレクティブはGNUアセンブラに特別なことをするように指示をします。ディレクティブはピリオド(.)で始まり、その後に命令の名前と引数が続きます。この場合ではmainがグローバルであるという意味です。これは、C言語ランタイムがmainを呼び出すために必須です。グローバルにしないと、C言語ランタイムは呼び出すことができずリンクは失敗します。
main: /* This is main */
GNUアセンブリコードにおいてディレクティブではない行はすべて、ラベル: 命令の形をとります。ラベル:または命令もしくはその両方は省略できます(空の行や空白文字のみの行は無視されます)。ラベル:のみの行は、そのラベルを次の行に適用します(この方法で同じものを参照する複数のラベルを作ることができます)。命令の部分はARMアセンブリ言語そのものとなります。この場合ではmainを命令なしで定義しています。
mov r0, #2 /* Put a 2 inside the register r0 */
行の先頭の空白は無視されますが、インデントは、この命令がmain関数に属していることを視覚化するために使用しています。
これはムーブを表すmov命令です。値の2をr0レジスタに移動します。レジスタについては次章で詳しく説明するので、今は心配しないでください。見ての通り、移動先が左にあるため扱いにくい構文です。ARM構文では常に左となり、文の意味は「r0レジスタに即値2を移す」です。ARMにおける即値の意味は次章で扱いますのでこれも心配ご無用です。
要するに、この命令は2をr0レジスタに入れるということを意味します(その時点でr0レジスタが持つ値は事実上上書きされます)。
bx lr /* Return from main */
このbx命令は、「分岐と交換」を意味します。今は、交換部分については考えないようにします。分岐とは、命令実行の流れを買えることを意味します。ARMプロセッサーは命令を順番に次に次にと実行していくため、前行のmovの次にbxが実行されます(この順次実行はARMに特有のものではなく、ほぼすべてのアーキテクチャで共通です)。分岐命令は、この暗黙の順次実行を変更するために使用されます。この場合、lrレジスタの示す値に分岐します。今はlrレジスタがもつ値は気にしないでおきましょう。この命令がmain関数を終わらせて事実上プログラムが終了することが理解できれば十分です。
エラーコードの話はどうなったのでしょう。実は、main関数のリザルトはプログラムのエラーコードであり、関数を終了するときにリザルトはr0レジスタに格納する必要があります。つまり、main関数によって実行されるmov命令が実際にはエラーコードを2に設定しているのです。
今日はここまで。