7. アセンブラ GNU as の基礎知識

Linux Zaurus 上でアセンブラ (GNU as または gas と呼ぶ) でプログラミング する場合の基本的な事柄を解説します。

簡単なソースのアセンブル、リンク、実行から始めて、GNUアセンブラの文法の 重要な部分を見ていきます。

Windowsマシンで使用しているインテルのi386系のCPUでは、GNU as と MASM, TASM, NASM等のアセンブラと文法が異なるため、GNU asを使った プログラミングでは特にレジスタの指定で混乱します。ARM用の GNU as は ARM用の他のアセンブラとほぼ同じなので安心して GNU as を使いましょう。


アセンブリソースの例

ARMのアセンブリプログラムのソースの例です。5行の「hello, world」を 表示するだけの簡単なプログラムなので、もっと短くすることもできますが、 大きなプログラムでも使えるようにARM用のGNUアセンブラの重要事項を詰め込んで みました。

#-------------------------------------
# プログラム例 list01.s
#-------------------------------------

#-------------------------------------
# text セクション
#-------------------------------------
.text
       .align  2
       .global _start           @ 必須

/* 定数に名前をつける */

sys_write = 0x900004            @ write システムコール
sys_exit  = 0x900001            @ exit システムコール

/* ここからプログラムを記述 */

_start: /* 常にこのラベルから実行開始 */


        mov     r3, #5          @ 5回繰り返す
    1:  ldr     r1, msg         @ 文字列先頭アドレス
        mov     r0, #1          @ 標準出力(fd=1)を指定
        ldr     r2, msg_sz      @ 出力データの長さ(バイト)
        swi     #sys_write
        subs    r3, r3, #1      @ カウンタを更新
        bne     1b              @ 後方のローカルラベルへ

        /* プログラム終了時に実行する */

        mov     r0, #0          @ 終了コードを0
        swi     #sys_exit       @ プログラム終了

        /* 書き換え不要な定数は text セクションに置く */

msg:    .long   msg0            @ 文字列格納アドレス
msg_sz: .long   msg_sz0         @ 文字列の長さ格納アドレス

#-------------------------------------
# data セクション
#-------------------------------------
.data
        .align  2
        /* ここは初期化が必要なデータを置く *
         * プログラムサイズに影響する       */

msg0:   .asciz  "hello, world\n"

        .align  2               @ 4バイト境界に設定
msg_sz0 = . - msg0              @ 文字列の長さを計算

data1:  .hword  12345

#-------------------------------------
# bss セクション
#-------------------------------------
.bss
        .align  2
        /* ここは初期化が不要なデータを置く *
         * プログラムサイズには影響しない   */

data:   .long   0               @ この例では使っていない
#-------------------------------------

ソースファイルの最終には改行が必要です。ファイルの最後が改行で 終わっていない場合はエラーとなります。 アセンブリのソースファイルの拡張子には「s」を使います。大文字の「S」の 場合は C のプリプロセッサ(cpp)で前処理をする場合に使われます。最近の GNU as はマクロも使えるので C のプリプロセッサを使わなくても困ることはありません。


アセンブルとリンクと実行

「アセンブリ言語のソースをアセンブラでアセンブルする」には as コマンドを使います。

~$ as -o list01.o list01.s

オブジェクトファイルはリンカで実行可能な形式に変換(リンク)しなければ なりません。リンクには ld コマンドを使います。「-o」 の後ろに指定した ファイル名が実行可能なコマンドの名前になります。

~$ ld -o list01 list01.o

実行してみます。アセンブル、リンクして生成された実行可能ファイルの 場所はコマンド検索パス(echo $PATH で確認)に入っていない場合が普通なので、 前に「./」を付けて実行します。

~$ ./list01
hello, world
hello, world
hello, world
hello, world
hello, world
~$

ソースファイルをアセンブルしてリンクする場合のコマンドが長くて面倒です。 次の内容のシェルを作成しておくと便利です。ファイル名を asld として 保存してください。

#!/bin/sh
as -o $1.o $1.s
ld -o $1 $1.o

「chmod +x asld」を実行して実行可能にします。 検索パスの通ったディレクトリにおきます。「su」を実行した後に、例えば 「cp asld /usr/bin」とします。次のように簡単にアセンブルとリンク できるようになります。

~$ asld list01
~$ ./list01
hello, world
hello, world
hello, world
hello, world
hello, world
~$

list01のサイズを見てみましょう。

~$ ls -l
-rwxr-xr-x    1 zaurus   qpe          1118 Sep 13 17:33 list01
-rw-r--r--    1 zaurus   qpe           792 Sep 13 17:33 list01.o
-rwxr-xr-x    1 zaurus   qpe          1817 Sep 13 17:23 list01.s

1118バイトでも小さいプログラムですが、strip コマンドで実行に不要な情報を 除くことで、さらに小さくすることが可能です。

~$ strip list01
~$ ls -l
-rwxr-xr-x    1 zaurus   qpe           456 Sep 13 18:01 list01

456バイトのコマンドが作成できました。


コメント

プログラム中に埋め込むコメント(注釈)の形式には3種類あります。

  1. C言語と同じ 「/*」と「*/」に囲まれたコメントで複数行に渡ってコメントを記述できます。
  2. 行の先頭の「#」から行末までがコメントとなります。
  3. 「@」から行末までをコメントとする形式。

コメントはアセンブル時に1つの空白に置換されます。ARMでは「@」の形式のコメントをよく使います。


セクション

セクションとは、プログラム本体やプログラム中で使用する定数、文字列、変数 に必要な性質に応じて区別して管理するためにあります。 書き換え不可能な領域(text)、書き換え可能な領域(data)、実行前に値を設定する 必要のない領域(bss)があります。複数のソースファイルを別々にアセンブルした 後に、まとめてリンカ(ld)でリンクすると同じセクションごとにまとめた形で 実行ファイルが生成されます。

text セクション (.text) にはプログラム本体や初期化されて変更する必要の ないデータを置きます。

data セクション (.data) は初期化が必要なデータを置きます。data セクションの データは実行時に書き換えることができます。

bss セクションは実行時にメモリを割り当てる領域です。実行開始時に 定義済みの値を持つ必要のないメモリ領域に使用します。bss セクションでは 通常 .byte, .long などのシンボル定義や、配列用などのまとまった領域を確保する .skip だけを使います。実行開始時にはすべて 0 に初期化されています。

ARMでは1命令の長さの制限(常に4バイト)のために簡単に任意のメモリアドレスを ラベルで参照することができません。文字列などのデータを使用するコードの 近くにデータを置くことが必要になる場合がよくあるため、書き換える必要がない データを積極的に text セクションに置くことになります。プログラムとデータを 分離したセグメントに置くことの多い i386 とは文化が違います。ARMでは命令長が 一定のため、逆アセンブルした場合のズレをあまり気にする必要がないと思います。


ラベルとシンボル

シンボル名は「.」「_」または英文字から始まる文字列で32ビットの値を 持っています。コロン「:」が後ろに付いたシンボルはラベルで、宣言された 場所のメモリアドレスを値として持ちます。英文字の大文字と小文字は区別 され、異なるシンボル名とみなされます。同じシンボル名は一度だけ宣言 できます。コロンを付けて宣言した場所のメモリアドレスを値に持つシンボルが 「ラベル」ですが、シンボルとラベルを厳密に区別して考えなくてもいいと 思います。

数字のみのシンボル名はローカルシンボル名として特別な機能を持ちます。 ローカルシンボル名は何度でも同じシンボル名を使うことができます。 参照する場合は前方なら「f」、後方なら「b」をローカルシンボル名の後ろに 付けて区別します。例えばソースファイル中で「55:」というローカルラベルが 何度も現われても「55b」として参照された場合はソースファイルの後方(上方) で一番近くにある「55:」を示します。同様に「55f」の場合は前方(下方)で 一番近くにある「55:」を示します。同じローカルシンボル名でもアセンブラは 内部で異なる値として管理しています。ローカルシンボル名は同じファイル内 で有効(スコープはファイル)です。

(ARMでは使えないようです)さらにスコープの狭いラベルも使うことができます。コロン「:」の代わりに 「$」を後ろに付けて宣言したローカルラベルは、通常のラベルとラベルの 間のみが有効範囲となります。この形式も同じシンボル名であってもアセンブラ 内部で異なるシンボル名として管理されます。「55$」と「55:」は区別して アセンブラ内で管理されます。

ピリオドだけのシンボル名「.」は、その行のアドレスを示します。 「=」を使ってシンボルに任意の値を割り当てることができます。 list01.s では次の部分でシンボルへの値の割り当てと「.」シンボルを使っています

msg0:   .asciz  "hello, world\n"

        .align  2               @ 4バイト境界に設定
msg_sz0 = . - msg0              @ 文字列の長さを計算

データ形式

整数は2進数、8進数、10進数、16進数で表記することができます。以下の表の書式で記述します。

整数 表記
2進数 「0b」または「0B」から始まる数値 0b0010, 0B111101
8進数 「0」から始まる数値 0123, 0765
10進数 「1」から「9」で始まる数値 234, 90
16進数 「0x」 「0X」から始まる数値 0x9FEB, 0X12ffab

命令のオペランドの即値とする場合は「#」を前につけます。 「.byte, .word, .long」はそのメモリ位置に値を書き込む擬似命令です。

@ 例
     mov    r0, #10
     add    r1, r2, #0x7F
     .byte  100, 0144, 0x64, 0X7f
     .word  12356
     .long  1234567890, 0xffffffff

文字列はダブルクォート「"」で囲みます。文字列中に特殊な文字を含める場合は エスケープ文字「\」に続けます。

表記 意味
\b バックスペース 0x08
\f フォームフィード 0x0C
\n 改行(LF) 0x0A
\r キャリッジリターン(CR) 0x0D
\t タブ 0x09
\3桁の8進数 対応する文字コードの文字
\x16進数 対応する文字コードの文字
\\ 文字「\」
\" 文字「"」

文字列は text セクションや data セクションでラベルに続けて使用します。 文字列に続けて命令や整数を置く場合は、文字列の直後に「.align 2」 を置いて4バイト境界に配置するようにします。「.asciz」は文字列の 直後に 0 が挿入されます。

@ 例
str1:       .ascii      "mov\tr1, r2\n"
            .align 2
num1:       .long       1234567890
str2:       .asciz      "New Line\n"
            .align 2
num2:       .long       0x12345678

文字定数はシングルクォート「'」を前においてあらわします。特殊文字は 文字列と同様にエスケープ文字「\」に続けた形式を利用できます。文字定数は 1バイトの値(文字コード)となります。オペランドで使う定数(即値)は前に「#」 をつける必要があります。

@ 例
            mov     r1, #'A
            add     r0, r1, #'0
            .byte   'J, '\n

整数定数やシンボルを記述する位置に整数定数やシンボルを使った式を 使うことができます。使用できる演算子には単項演算子と二項演算子 があります。単項演算子には2種類あり、定数の前につけて 2 の補数 (-1を乗算した値) を求める「-」と 1 の補数(ビット否定)を求める 「~」が利用できます。二項演算子はCと同様な以下の演算子があります。

優先順位 演算子 意味
1 * 乗算
/ 除算
% 剰余
< , << 左シフト
> , >> 右シフト
2 | ビット論理和 OR
& ビット論理積 AND
^ ビット排他的論理和 XOR
! ビット否定 NOT
3 + 加算
- 減算
== 等しい (真:-1, 偽:0)
<> 等しくない (真:-1, 偽:0)
< より小さい (真:-1, 偽:0)
> より大きい (真:-1, 偽:0)
>= 大きいか等しい (真:-1, 偽:0)
<= 小さいか等しい (真:-1, 偽:0)
4 && 論理積 (真:1, 偽:0)
|| 論理和 (真:1, 偽:0)

通常の計算と同じく括弧を使って優先順位を変更することもできます


擬似命令

アセンブラには直接命令に変換されない擬似命令が多く用意されています。 これまでにセクションの指定「.text, .data, .bss」、メモリの境界に 配置する「.align」、定数をメモリに書き込む「.byte, .word, .asciz」 などを使ってきました。

条件に従ってアセンブルを制御する条件アセンブリ擬似命令もあります。 デバッグ用のコードを挿入したり、1本のソースから複数のプログラム を作成する場合などに利用できます。

.ifdef SYMBOL
    /* シンボル SYMBOL が定義済みのときにアセンブルされるコード */
.else
    /* シンボル SYMBOL が未定義のときにアセンブルされるコード */
.endif

「.ifdef」以外にも「.ifndef」、「.ifeq」、「.ifne」などのバリエーション があります。

ARMが持っていない命令を別の命令を使って実現する擬似命令が4つあります。 ARMには「何もしない」専用の命令がありませんが、GNU as では「nop」を 命令として使うことができます。「nop」は実際にはアセンブラが「mov r0,r0」 に翻訳します。r0レジスタの内容をr0に転送しても何も変化しないためです。

ARMでは命令の長さが4バイトに固定されるため、32ビット(4バイト)の整数 やラベルの値(メモリアドレス)を直接レジスタに転送することができません。 GNU as は「adr」、「adrl」、「ldr」という擬似命令を用意していて、 置き換えが可能な場合はアセンブラが相当する命令に変換します。

「adr」命令を使ってラベルのメモリアドレスをレジスタに書き込む ことができます。この「adr」は「add または sub」に翻訳され、アセンブラが プログラムカウンタ(r15)に対して加減算した値をレジスタに書き込むことで 実現します。したがってあまり離れた位置のラベルを指定するとエラーに なります。また同一ファイルで同一セクション(text)のラベル以外を指定 してもエラーになります。「adrl」は加減算を2命令使い「adr」では届かない 位置を指定できますが、この命令でも届かない場合がありえます。

@ 例
            adr   r1, LABEL1
            adrl  r2, LABEL2
            :
            :
LABEL1:	.asciz  "string"

レジスタに任意の数値を転送するためにオペランドに「=」を使う「ldr」の 特別な形式があります。 数値を評価して、「mov または mvn」で実現できる場合には置き換えます。 実現できない場合は近くのリテラルプール(.pool で指定したメモリ領域) に数値を書き込み、プログラムカウンタ(r15)相対の「ldr」命令に置き換えます。

@ 例
            ldr   r1, =#1234567890
            ldr   r1, =#0x10000
            :
            :
.pool       @ リテラルプール

マクロ

最近のGNU asはマクロが使えるようになっていて cpp (Cのプリプロセッサ)を 使う必要がなくなっています。マクロは次の形式で宣言して、マクロ名を命令の ように使うことができます。

.macro マクロ名 引数
       定義
.endm

例えば、次のマクロは引数で指定したレジスタの示すメモリアドレスに 格納された0で終わる文字列を表示するマクロです。マクロの定義中で引数は 「\」を引数名の前につけて参照します。

@ レジスタの値を先頭アドレスとする文字列を表示するマクロ
@ 文字列先頭アドレスの直接指定
@   ex. PRINTSTR v11
.macro  PRINTSTR   reg
        stmfd   sp!, {r0, lr}
        mov     r0, \reg
        bl      OutAsciiZ
        bl      NewLine
        ldmfd   sp!, {r0, lr}
.endm

@ レジスタが示すアドレスに格納された値を先頭アドレスとする文字列を
@ 表示するマクロ
@ 文字列先頭アドレスの間接指定
@   ex. PRINTSTRI v11
.macro  PRINTSTRI  reg
        stmfd   sp!, {r0, lr}
        ldr     r0, [\reg]
        bl      OutAsciiZ
        bl      NewLine
        ldmfd   sp!, {r0, lr}
.endm

デバッグ用に使用するため使用するレジスタを保存しています。OutAsciiZ は、 0で終わる文字列を標準出力に出力するサブルーチン、NewLine は改行(0x0A)を 標準出力に出力するサブルーチンで次回に説明します。