ラズパイでARM入門(3. メモリ、アドレス、ロード・ストア)


前回:2. レジスタと基本的な算術演算 次回:4. GNU デバッガ (GDB)
目次(記事一覧)

※この記事はRoger Ferrer IbáñezさんのブログARM assembler in Raspberry Pi – Chapter 3の翻訳です。

第1章と第2章では、mov命令を使って値をレジスタに移すことができるのを確認しました。もしプロセッサーがレジスタでしか作業ができないのだとしたら、かなりできることが少なくなってしまいます。

メモリ

コンピューターには、コード(アセンブラの.textセクション-後述)とデータが格納されるメモリがあります。そのためメモリにプロセッサーからアクセスする方法が必要です。余談ですが、386(※=i386=x86の32bit版)及びx86-64アーキテクチャでは、命令はレジスタとメモリのどちらでもアクセスできます。例えば、2つの値のうち片方がメモリにあっても加算ができます。しかしARMではこのようなことはできません。オペランド(引数)は全てレジスタである必要があるからです。この問題(実際の所、問題というより意図的にそのような仕組みになっているのですが、その説明はこのテキストの範囲を超えます)はメモリからレジスタに積む(ロードする)ことと、レジスタからメモリに保存する(ストアする)ことで解消できます。

メモリとデータをロード/ストアする方法はいくつかありますが、今回は一番簡単な方法を使います。レジスタにロードする(load to register)のldr命令と、レジスタからストアする(store from register)のstr命令です。

データをメモリからロードするのは少し複雑で、アドレスの話をしなくてはいけません。

アドレス

データにアクセスするには、データに名前をつける必要があります。そうしないと欲しいデータを参照できないからです。実際、メモリのデータには名前が振られていてそれをアドレスと呼びます。アドレスは数字で、ARMでは32ビットの数字でメモリのすべてのバイト(8ビットのまとまり)を識別します。

図 メモリは、各バイトが独自のアドレスを持つバイト配列のようなものです。

メモリとデータをロード/ストアする場合、アドレスを計算する必要があります。アドレスはさまざまな方法で計算できます。アドレスの計算方法の種類をアドレッシングモードと呼び、ARMには複数のアドレッシングモードが用意されています。すべてを説明するのは時間がかかるためレジスタを介したアドレッシングモードだけを紹介します。

ARMに32ビットの整数レジスタがあることとメモリのアドレスが32ビットの数値であることは偶然ではありません。アドレスをレジスタに入れることができるということです。アドレスをレジスタに入れたら、そのレジスタを利用してデータをロードまたはストアすることができます。

データ

第1章で、アセンブリコードにはテキストとデータの両方が含まれていることを確認しました。実は、アセンブリコードのラベルを説明するときわざと詳しい説明を避けていましたが今は詳しい説明ができます。ラベルはプログラム中のアドレスを示す単なる名前です。アドレスはデータかテキストを参照します。これまではmain関数のアドレスを指定するためにmainというラベルひとつだけを使いました。ラベルはアドレスのみを示し、その内容は示さないということを心に留めておいてください。

ラベルのアドレスに実際の値を割り当てるというをしているのはアセンブラ(as)です。アセンブリコードを書く人はそのような面倒な作業を気にせずにラベルを使えます。

したがって、データを定義しそれのアドレスにラベルをひも付けることができるのわけですが、ラベルに参照される場所が適切なサイズと値を持っていることを確認するのはアセンブリコードを書くプログラマーの責任です。

では、4バイトの変数を定義し、それを3で初期化しましょう。myvar1というラベルを付けます。

.balign 4
myvar1:
    .word 3

新出のディレクティブ(疑似命令)の.balign.wordがあります。アセンブラ(as)は.balign 4のその後のアドレスが4バイト境界で始まることを保証します。つまり、命令やデータを表す次のバイナリのアドレス値が4の倍数になります。ARMは作業するデータのアドレスにいくつかの決まりごとを定めているので、こうすることは重要です。この.balignは既に4バイト境界の場合は何もせず、そうでない場合にはプログラムで無視されるバイト(パディングバイト)を用いて要求された境界になるようにします。アセンブリコード中のデータやテキストが全て4バイト(32ビット)幅なら省略できることもありえますが、そうではないサイズのデータを用いるならこのディレクティブを使わなくてはなりません。

さて、上の例ではmyvar1というアドレスを定義しました。前の行の.balignのおかげでアドレスが4バイト境界を満たすことがわかります。

.wordディレクティブは、引数の値を4バイト整数として出力するようにアセンブラに指示します。この場合、値3を表現する4バイトが出力されます。データサイズを決めるために.wordが4バイトを出力するという事実に依存しているので注意です。

セクション

データはコード(テキスト)と同じようにメモリ内に存在していますが、ある役に立つ技術(詳しくは触れません)を用いてデータセクションと呼ばれる場所にまとめられます。.dataディレクティブは、データセクションの内容を出力するようにアセンブラに指示します。.textはテキスト(コード)に対して同様のことをします。したがって、データは.dataの後に置き、テキスト(コード)は.textの後に置きます。

ロード

では、第2章で使ったサンプルコードをメモリへのアクセスを使って拡張してみましょう。2個の4バイト変数myvar1myvar2を定義してそれぞれ3と4に初期化します。それらをldrを使ってロードして、加算を実行します。結果のエラーコードは第2章のサンプルコードと同じく7になるはずです。

load01.s
/* -- load01.s */

/* -- Data section */
.data

/* Ensure variable is 4-byte aligned */
.balign 4
/* Define storage for myvar1 */
myvar1:
    /* Contents of myvar1 is just 4 bytes containing value '3' */
    .word 3

/* Ensure variable is 4-byte aligned */
.balign 4
/* Define storage for myvar2 */
myvar2:
    /* Contents of myvar2 is just 4 bytes containing value '4' */
    .word 4

/* -- Code section */
.text

/* Ensure code is 4 byte aligned */
.balign 4
.global main
main:
    ldr r1, addr_of_myvar1 /* r1 ← &myvar1 */
    ldr r1, [r1]           /* r1 ← *r1 */
    ldr r2, addr_of_myvar2 /* r2 ← &myvar2 */
    ldr r2, [r2]           /* r2 ← *r2 */
    add r0, r1, r2         /* r0 ← r1 + r2 */
    bx lr

/* Labels needed to access data */
addr_of_myvar1 : .word myvar1
addr_of_myvar2 : .word myvar2

アセンブラの制限のため、上の例では少しごまかしました。見ての通り4つのldr命令があります。それらの意味について説明します。最初に、次の2つのラベルについて議論します。

/* Labels needed to access data */
addr_of_myvar1 : .word myvar1
addr_of_myvar2 : .word myvar2

さて、この2つのラベルはmyvar1myvar2のアドレスを含んでいます。どうして既にデータのアドレスがmyvar1myvar2にあるのにさらにラベルが要るのかと疑問に思うかもしれません。詳細は少し長くなります。ここで問題となるのはmyvar1myvar2が別のセクション、つまり.dataセクションにあることです。プログラムはそのセクションのデータを操作できます。そのため変数をデータセクションに保持するのです。一方、通常は効率とセキュリティー上の理由からテキスト(コード)をプログラムによって変更することはありません。異なる性質を持つ2つのセクションが結び付けられているのはそのためです。あるセクションから別のセクションの識別子に直接アクセスすることはできません。したがって、.codeセクションに.dataセクション内の実体を指すアドレスを参照する特別なラベル(※addr_of_myvar1addr_of_myvar2)が必要となります。

さて、アセンブラがバイナリコードを出力するとき、.word myvar1myvar1のアドレスではなく再配置アドレスとなります。再配置はアドレスをアセンブラがアドレスを発行するときに使用する方法であり、その正確な値は不明ですがプログラムがリンクされた時(最終的な実行ファイルが作成された時)にわかります。雰囲気としてはアセンブラが「この変数が実際にどこに配置されるかはわからない。後でリンカに値を埋めてもらおう。」と言っているようなものです。つまり、このaddr_of_myvar1が代わりに使用されます。addr_of_myvar1のアドレスは同じ.textセクションにあります。リンク段階(最終的な実行ファイルが作成され、プログラム中の全実体がメモリのどこに絶対的に配置されるかがわかる時)でリンカはその値を埋めます。これが、(gccによって内部的に呼び出される)リンカがld (Link eDitor: 連係編集プログラム)と呼ばれる理由です。

    ldr r1, addr_of_myvar1 /* r1 ← &myvar1 */
    ldr r1, [r1]           /* r1 ← *r1 */

ここで、2回ロードをします。1つ目は再配置されるmyvar1のアドレスの値を実際にロードします。つまり、メモリに置いたデータのアドレスはmyvar1の実際のアドレスを含む4バイトの大きさのaddr_of_myvar1です。よって、最初のldrによってr1myvar1の実際のアドレスが入ります。アドレス自体は欲しくありませんが、そのアドレスのメモリの中身が要るので2つ目のldrを使います。

図 メモリの内容からして、この図はロード命令でレジスタに起こることを表しています。

おそらく、2つのロードの構文が異なる理由を疑問に思うことでしょう。1つ目のldraddr_of_myvar1のシンボリックアドレスを使用しています。2つ目のldrはレジスタの値をアドレッシングモードとして使用しています。つまり、2つ目のケースではr1の中の値をアドレスとして使用しています。1つ目のケースではアセンブラが実際のアドレッシングモードに何を使うかわからないため、ここでは無視します。

プログラムは、myvar1myvar2から2つの32ビット値をロードします。初期値は3と4で、加算し、mainを離れる前に結果をプログラムのエラーコードとしてr0レジスタに保存します。

$ ./load01 ; echo $?
7

ストア

さきほどのサンプルコードを作り変えます。myvar1myvar2の初期値に3と4をセットする代わりに、両方とも0をセットします。コードを再利用しますが、変数に3と4をストアするためにいくつか付け加えます。

store01.s
/* -- store01.s */

/* -- Data section */
.data

/* Ensure variable is 4-byte aligned */
.balign 4
/* Define storage for myvar1 */
myvar1:
    /* Contents of myvar1 is just '3' */
    .word 0

/* Ensure variable is 4-byte aligned */
.balign 4
/* Define storage for myvar2 */
myvar2:
    /* Contents of myvar2 is just '3' */
    .word 0

/* -- Code section */
.text

/* Ensure function section starts 4 byte aligned */
.balign 4
.global main
main:
    ldr r1, addr_of_myvar1 /* r1 ← &myvar1 */
    mov r3, #3             /* r3 ← 3 */
    str r3, [r1]           /* *r1 ← r3 */
    ldr r2, addr_of_myvar2 /* r2 ← &myvar2 */
    mov r3, #4             /* r3 ← 4 */
    str r3, [r2]           /* *r2 ← r3 */

    /* Same instructions as above */
    ldr r1, addr_of_myvar1 /* r1 ← &myvar1 */
    ldr r1, [r1]           /* r1 ← *r1 */
    ldr r2, addr_of_myvar2 /* r2 ← &myvar2 */
    ldr r2, [r2]           /* r2 ← *r2 */
    add r0, r1, r2
    bx lr

/* Labels needed to access data */
addr_of_myvar1 : .word myvar1
addr_of_myvar2 : .word myvar2

str命令の少し奇妙な点に注意してください。命令のオペランドのうち保存先は第1オペランドではありません。第1オペランドはソースレジスタで、第2オペランドにアドレッシングモードが入ります。

$ ./store01; echo $?
7

今日はここまで。

次回:4. GNU デバッガ (GDB)