0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ラズパイでARM入門(8.配列と構造体、その他のインデクシングモード)

Last updated at Posted at 2021-03-04

前回:7.インデクシングモード 次回:9.関数その1
目次(記事一覧)

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

前章では、ビットをシフトまたはローテートするシフト演算子がほとんどの算術命令の第2オペランドで利用できることがわかりました。本章では、引き続きARMの命令で利用できるインデクシングモードについて学習します。今回は、ロード命令とストア命令に焦点を当てます。

配列と構造体

前章までに、32ビットのデータをメモリからレジスタに移動し(ロード)、メモリに戻す(ストア)ことができるようになりました。しかし、32ビット(スカラー値)だけで作業するのは少々不便です。配列と構造体について学びましょう。

配列は、同じ種類の要素の列です。配列は基本的なデータ構造であり、ほぼすべての低水準言語にあります。配列には必ずベースアドレスがあり、普通は配列の名前が使われます。また、配列にはN個の要素があり、各要素には昇順に0~N-1、または1~Nのようにインデックスが関連付けられます。ベースアドレスとインデックスを使うことで配列の要素にアクセスできます。第3章でメモリはバイト配列とみなすことができると述べました。メモリの中に作る配列も同様ですが、1つの要素は1バイトより大きくなることもあります。

構造体(レコード、タプル)は異なる種類(同じでも良い)の要素の列です。構造体の各要素はフィールドと呼ばれます。フィールドにはインデックスは付いていませんが、構造体の先頭に対するオフセットが付いています。構造体は全てのフィールドが適切なメモリ配置になるようにします。構造体のベースアドレスは1つ目のフィールドのアドレスと同じです。構造体はベースアドレスがメモリ境界に位置する場合、同様に全フィールドが適切なメモリ境界に位置するように配置される必要があります(※訳注 メモリ境界についてはWikipediaも参照)。

配列と構造体はロードとストアのインデクシングモードとどんな関係があるのでしょうか?実は、ロードとストアのインデクシングモードは、配列や構造体へのアクセスがしやすいように設計されています。

配列と構造体を定義する

配列と構造体の使い方を説明するために、以下のC言語での宣言をアセンブリ言語に実装してみます。

int a[100];
struct my_struct
{
  char f0;
  int f1;
} b;

ではまず、配列「a」を定義しましょう。ちょうど100個の整数の配列です。ARMの整数は32ビット幅のため、アセンブリコードでは400(=4*100)バイトのスペースを確保する必要があります。

array01.s(一部)
/* -- array01.s */
.data

.balign 4
a: .skip 400

最後の行で識別子aを定義し、400バイトのスペースを確保します。ディレクティブ.skipは、指定したバイト数だけ進めてから次のデータを発行するようにアセンブラに指示をします。今回は、整数配列が400バイト(1個当たり4バイトの整数が100個)を使用するため400バイトをスキップしています。構造体の宣言もほとんど違いはありません。

// array01.sの続き
.balign 4
b: .skip 8

ここで、構造体の持つデータが5バイトであるにも関わらず8バイトをスキップしたことを不思議に思うかもしれません。確かに構造体に保存される有用な情報は5バイトまでです。最初のフィールドf0char型で、1個のcharは1バイトのストレージを使用します。次のフィールドf1は整数型で、1個の整数は4バイトを使用しますが、整数は4バイト境界に配置されなくてはなりません。したがって、フィールドf0f1の間には3個の空きバイトを入れる必要があります。境界をあわせるために配置されるこの空きストレージのことをパディングバイトと呼びます。パディングバイトをプログラム中で取扱わないでください。

インデクシングモードを使わない例

では、配列a[i]の全要素を初期化するコードを書いてみましょう。実行する処理は以下のC言語コードと同等です。

for (i = 0; i < 100; i++)
  a[i] = i;
// array01.sの続き
.text

.global main
main:
    ldr r1, addr_of_a       /* r1  &a */
    mov r2, #0              /* r2  0 */
loop:
    cmp r2, #100            /* 100回目か? */
    beq end                 /* もしそうならループを抜け、違うなら続行 */
    add r3, r1, r2, LSL #2  /* r3  r1 + (r2*4) */
    str r2, [r3]            /* *r3  r2 */
    add r2, r2, #1          /* r2  r2 + 1 */
    b loop                  /* loopの最初へ戻る */
end:
    bx lr
addr_of_a: .word a

以前の章で学んだことがたくさん入っていますね。main:の次の行で、配列のベースアドレスをr1にロードします。配列のアドレスは変わらないのでロードは一度だけで良いです。レジスタr2の役割は0~99のインデックスを保持することです。loop:の次の行では、r2を100と比較しループの終わりに到達したかを確認します。

loop:から3行後のadd r3, r1, r2, LSL #2 は重要です。ここでは配列の要素のアドレスを計算します。r1はベースアドレスで、配列の各要素は4バイト幅です。さらに、r2は配列の各要素へのアクセスに使うループのインデックスを保持しています。各要素間が4バイトのため、要素のインデックスがiならその要素のアドレスは&a + 4*iとなります。したがって、r3にはその時のループ回数に即した配列の要素のアドレスが入ります。次の行のstr r2, [r3]では、r2の値(=i)をr3が指すメモリ(=i番目の配列の要素=a[i])へとストアします。

その後、r2をインクリメントし、次のループへとジャンプします。

このように、配列にアクセスするにはアクセスする要素のアドレスを計算する必要があります。ARMの命令セットにはもっと簡潔な方法があるのでしょうか?はい、いくつかのインデクシングモードが提供されています。

インデクシングモードを使う

前章までの使い方では、インデックスを付けるようなことはしなかったため「インデクシングモード」という名前は少々ずれていました。今回はインデックスから配列の要素のアドレスを計算するので、その名前も少しばかり意味を持つでしょう。ARMはそんなインデクシングモードを9つ提供します。それらのインデクシングモードは副作用の有無で更新なしモードと更新ありモードで区別できます。副作用については更新ありインデクシングモードで解説します。

更新なしインデクシングモード

番号 構文
1 [Rsource1, +#immediate] または [Rsource1, -#immediate]

アドレスを作るために単純に即値を加算または減算します。これはインデックスがコード内で固定値である配列の要素へのアクセスや、オフセットが常に固定値である構造体のフィールドへのアクセスに役に立ちます。Resource1にはベースアドレスを入れ、immediateにはオフセットをバイト単位で入れます。immediateは12ビット[0,4096)より大きくすることはできません。immediate#0の場合は、これまでに使ってきた通常の[Rsource1]のようになります。

例えば、このようにしてa[3]に3をセットします(r1はすでにベースアドレスを保持していると仮定します)。オフセットはバイト単位のためオフセットが12(4バイト*3要素をスキップ)となることに注意してください。

mov r2, #3          /* r2  3 */
str r2, [r1, #+12]  /* *(r1 + 12)  r2 */
番号 構文
2 [Rsource1, +Rsource2] または [Rsource1, -Rsource2]

これは1番目の構文と似ていますが、レジスタの値で加算または減算するオフセットが決まります。これは、オフセットが即値に使用できないくらい大きいときに役に立ちます。+Rsource2の場合、2つのレジスタは場所を交換できます(アドレス計算の結果に影響を与えません)。

例です。1番目の構文と同じですが、レジスタを使用します。

mov r2, #3         /* r2  3 */
mov r3, #12        /* r3  12 */
str r2, [r1,+r3]   /* *(r1 + r3)  r2 */
番号 構文
3 [Rsource1, +Rsource2, shift_operation #immediate] または
[Rsource1, -Rsource2, shift_operation #immediate]

この構文は他の命令で利用できる通常のシフト演算に似ています。シフト演算(LSL, LSR, ASR, RORなど)がRsource2に適用され、その後、Rsource2に適用されたシフト演算の結果がRsource1に加算または減算されます。これは、オフセットの計算に一定量を掛ける必要がある場合に役に立ちます。整数配列aの要素にアクセスするときに、インデックスに4を掛けましたね。

要素のインデックスがr2の場合のアドレス計算を思い出してみましょう。

// array01.sの一部
add r3, r1, r2, LSL #2  /* r3  r1 + r2*4 */
str r2, [r3]            /* *r3  r2 */

これをもっとコンパクトに表現できます(r3レジスタは不要です)。

str r2, [r1, +r2, LSL #2]  /* *(r1 + r2*4)  r2 */

更新ありインデクシングモード

このインデクシングモードでは、Rsource1レジスタがロード、またはストア命令によって合成されたアドレスで更新されます。なぜそうしたいのか疑問に思うかもしれません。少し遠回りになりますが、配列のロードを再確認しましょう。ベースアドレスから4バイトずつ離れていくのに、ベースアドレスを保持する必要はあるのでしょうか?現在使用しているアドレスを保持する方が良いのではないでしょうか?つまり、

// array01.sの一部
add r3, r1, r2, LSL #2  /* r3  r1 + r2*4 */
str r2, [r3]            /* *r3  r2 */

の代わりに、

str r2, [r1]        /* *r1  r2 */
add r1, r1, #4      /* r1  r1 + 4 */

とすると、次の要素に順番にアクセスしていくようになり、アドレスを毎回最初から計算する必要がなくなります。少し改善したように見えますが、まだもう少し改善できます。仮にr1を更新する命令があったらどうでしょうか。こんな感じに...(正しい構文ではありません)

/* 不正な構文 */
str r2, [r1] "そして" add r1, r1, #4

このようなことをするインデクシングモードが存在します。Rsource1の更新タイミングに応じて2種類に分類できます。ロードまたはストア後にRsource1が更新される場合(この場合Rsource1の初期値がロードまたはストアされる)、これをポストインデクシングモードと呼びます。実際のロードまたはストアの前にRsource1が更新される場合(この場合、更新後のRsource1の値をアドレスとしてロードまたはストアが行われる)、これをプレインデクシングモードと呼びます。いずれの場合も、命令終了時までにインデクシングモードの計算した値がRsource1に入ります。複雑に聞こえたかもしれませんが、先程の例を見てみましょう。始めにr1を使ってロードして、そしてr1 ← r1 + 4をします。よってこれはポストインデクシングです。r2の値をストアする場所のアドレスとしてr1の値を最初に用いて、そしてr1r1 + 4で更新します。次に、別の架空の構文を作って考えてみます。

/* 不正な構文 */
str r2, [add r1, r1, #4]

こちらはプレインデクシングです。最初にr1 + 4をして、それをr2のアドレスをストアする場所のアドレスとして使います。こちらもr1の最後の時点でr1の更新が完了していますが、更新された値はすでにロードまたはストアのアドレスとして使用されています。

ポストインデクシングモード

番号 構文
4 [Rsource1], #+immediate または [Rsource1], #-immediate

Rsource1の値はロードまたはストアのアドレスとして使用されます。その後、Rsource1immediateの値を加算したまたは減算した値で更新されます。このインデクシングモードを使って、最初の例を以下のように書き直すことができます。

// array01.sの一部
loop:
    cmp r2, #100            /* 100回目か? */
    beq end                 /* もしそうならループを抜け、違うなら続行 */
-   add r3, r1, r2, LSL #2  /* r3  r1 + (r2*4) */
-   str r2, [r3]            /* *r3  r2 */
+   str r2, [r1], #4        /* *r1  r2 then r1  r1 + 4 */
    add r2, r2, #1          /* r2  r2 + 1 */
    b loop                  /* loopの最初へ戻る */
end:
番号 構文
5 [Rsource1], +Rsource2 または [Rsource1], -Rsource2

4番目の構文と同様ですが、即値の代わりにResource2の値が使用されます。オフセットが即値で表現できる範囲を超えるときの回避策として利用できます。

番号 構文
6 [Rsource1], +Rsource2, shift_operation #immediate または
[Rsource1], -Rsource2, shift_operation #immediate

Rsource1の値はロードまたはストアのアドレスに使用されます。その後、Rsource2にはシフト演算(LSL, LSR, ASR, ROL)が適用されます。そのシフト演算の結果がRsource1に加算または減算され、最終的にその値で更新されます。

プレインデクシングモード

プレインデクシングモードは最初は少し奇妙に見えるかもしれませんが、計算したアドレスにがすぐに再利用される場合に役に立ちます。再計算する代わりに更新されたRsource1を再利用できます。プレインデクシングモードと更新なしインデクシングモードとの区別のため!を用いることに注意してください。

番号 構文
7 [Rsource1, #+immediate]! または [Rsource1, #-immediate]!

1番目の更新なしインデクシングモードと同じように動作しますが、Rsource1は計算したアドレスで更新されます。例えばa[3] = a[3] + a[3]を実行したいとすると、次のように書くことができます(r1にはすでに配列のベースアドレスが入っていると仮定します)。

ldr r2, [r1, #+12]!  /* r1  r1 + 12 そして r2  *r1 */
add r2, r2, r2       /* r2  r2 + r2 */
str r2, [r1]         /* *r1  r2 */
番号 構文
8 [Rsource1, +Rsource2]! または [Rsource1, -Rsource2]!

7番目の構文と同様ですが、即値の代わりにレジスタを使用します。

番号 構文
9 [Rsource1, +Rsource2, shift_operation #immediate]! または
[Rsource1, -Rsource2, shift_operation #immediate]!

3番目の更新なしインデクシングモードと同様ですが、Rsource1はロードまたはストア命令で使用されたアドレスで更新されます。

構造体に戻る

この章の例ではすべて配列を使用してきました。構造体はもう少し単純です。フィールドへのオフセットは常に一定のため、構造体のベースアドレス(最初のフィールドのアドレス)を一度取得すれば、フィールドへのアクセスは(たいてい即値の)オフセットを伴う一度のインデクシングモードの使用で済みます。例に出した構造体は、あえて最初のフィールドf0char型としています。現在、メモリ内の4バイト以外のスカラーを扱う方法は学んでいません。よって、最初のフィールドについては後ほどの章で扱います。

例えば、このようにフィールドf1を加算したいとします。

b.f1 = b.f1 + 7;

構造体のベースアドレスがr1に入っているとします。今は利用可能なすべてのインデクシングモードを知っているので、フィールドf1にアクセスするコードを書くのは非常に簡単です。

ldr r2, [r1, #+4]!  /* r1  r1 + 4 そして r2  *r1 */
add r2, r2, #7     /* r2  r2 + 7 */
str r2, [r1]       /* *r1  r2 */

プレインデクシングモードを使用してフィールドf1のアドレスをr1に保存していることに注意してください。このようにすることで、2番目のストアにおいてアドレスを再計算する必要はなくなります。

今日はここまで。

次回:9.関数その1

0
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?