主記憶領域

ロード命令とストア命令

ARMの主記憶アクセスはi386ほど便利にはできていない. まず,主記憶アクセス命令にはロード命令とストア命令の2種類しかない. さらに,絶対番地指定(例えば [data] のようにラベルや番地をロード・ストア命令中に直接指定すること)はできない.レジスタ間接指定だけができる.

主要な主記憶アクセス命令

最初に主要な主記憶アクセス命令を示す.

LDR dst, [src]
主記憶上の,レジスタsrcが保持する番地を先頭番地とする4バイトから値を読出し, レジスタdstに格納する.
C言語風に書くと: dst = *src
STR src, [dst]
主記憶上の,レジスタdstが保持する番地を先頭番地とする4バイトに, レジスタsrcの値を書き込む.
C言語風に書くと: *dst = src

主記憶上のデータに対する演算

主記憶上の値を読み出してレジスタに加算したい場合, i386では,例えばADD命令にメモリオペランドを指定して

    add     eax, [x]
    ...
x:  dd      10

のように書くことができた.ARMではそれができない.このようなことをしたければ, LDR命令で主記憶の内容を一旦レジスタにロードして,それを加算する.

    ldr     r1, =x         @ x の番地を r1 レジスタにロード
    ldr     r2, [r1]       @ r1 レジスタが指す番地の内容を r2 レジスタにロード
    add     r0, r0, r2     @ r0 レジスタにロードしたデータを加算
    ...
x:  .word  10

.wordはNASMのddに当たる疑似命令.)

この例ではxの番地をr1レジスタに,そこから読み出した値をr2レジスタに格納しているが, これ以降xの番地を使わないのであれば,r1レジスタが指す番地の中身をr1レジスタにロード(ldr r1, [r1])してレジスタを節約してもよい.

主記憶への書込みに関しても同様である.例えばC言語のx += yを実現したければ,i386では

    mov     eax, [y]
    add     [x], eax

のように書くだろう.一方,ARMでは次のように書く必要がある.

    @ r0 に y をロード
    ldr     r0, =y
    ldr     r0, [r0]
    @ r1 に x をロード (x の番地は r2 に残しておく)
    ldr     r2, =x
    ldr     r1, [r2]
    @ r0 + r1 を計算して x にストア
    add     r1, r0, r1
    str     r1, [r2]

これが正しく動作することを確認するために,chap4ディレクトリの中に123.sというファイルを作り, 次のプログラムを入力してビルドして実行してみよう.

    @ x += y を実行し, x を終了コードとして出力
    .section .text
    .global _start
_start:
    @ r0 に y をロード
    ldr     r0, =y
    ldr     r0, [r0]
    @ r1 に x をロード (x の番地は r2 に残しておく)
    ldr     r2, =x
    ldr     r1, [r2]
    @ r0 + r1 を計算して x にストア
    add     r1, r0, r1
    str     r1, [r2]

    @ 一旦忘れて
    mov     r0, #0
    mov     r1, #0
    mov     r2, #0

    @ x を終了コードにして exit
    ldr     r0, =x
    ldr     r0, [r0]
    mov     r7, #1
    swi     #0

    .section .data
x:  .word   123
y:  .word   45

Makefileを適切に書いて,

$ make 123
$ scp 123 <Raspberry PiのIPアドレス>:

Raspberry Piで実行する.

$ ssh <Raspberry PiのIPアドレス>
> ./123
> echo $?
168
> rm 123

アクセス幅

i386では,レジスタオペランドに16ビットレジスタや8ビットレジスタを指定したり, byte [x]のようにビット幅を明示的に指定することで主記憶にアクセスするビット幅を指定できた. ARMでは命令に接尾辞を付けることによってアクセス幅を指定する.

例えば1バイトのロードには接尾辞Bを付けてLDRB命令を使う.次の命令は, r1レジスタが保持する番地から1バイト読出してr0レジスタに格納する.

    ldrb    r0, [r1]

補足

LDR命令やSTR命令にも条件属性の接尾辞を付けることができる.そのとき, アクセス幅の接尾辞と条件属性の接尾辞のどちらを先に書くのかはアセンブラにより異なる. 本科目で使っているGNU Assemblerでは,条件属性の接尾辞を先に書き,例えばldrnebのように書く. これは RealView(R) Compilation Tools アセンブラガイド に記載の構文とは逆なので注意.

符号拡張とゼロ拡張

ところで,1バイトのロードを行ったとき,読み出した8ビットは32ビットレジスタ中のどこに格納され,残りの24ビットはどうなるのだろうか?

ビット幅の小さいデータからビット幅の大きいデータへの変換を拡張と言う.拡張には,ゼロ拡張 (zero extension) と 符号拡張 (sign extension) の2種類がある.

  • どちらも,元のビット列を最下位に配置し,上位にビットを補ってビット幅を増やす.
  • ゼロ拡張では,増やしたビットはすべて0にする.扱うデータが符号無し数の場合,ゼロ拡張しても値が変化しない.
  • 符号拡張では,増やしたビットはすべて元のデータの符号ビットと同じにする.扱うデータが符号付き数の場合,符号拡張しても値が変化しない.

LDRB命令はゼロ拡張を行う. それとは別に,符号拡張を行うLDRSB命令がある.

ロードする値 0xxxxxxx
ゼロ拡張 00000000 00000000 00000000 0xxxxxxx
符号拡張 00000000 00000000 00000000 0xxxxxxx
ロードする値 1xxxxxxx
ゼロ拡張 00000000 00000000 00000000 1xxxxxxx
符号拡張 11111111 11111111 11111111 1xxxxxxx

ビット幅が短くなる変換ではこのようなことを考える必要はないので,STRB命令はあるがSTRSB命令はない.

アクセス幅の接尾辞のまとめ

アクセス幅を指定する接尾辞をまとめる.

接尾辞 アクセス幅 上位桁
なし 32ビット ---
B 8ビット ゼロ拡張
SB 8ビット 符号拡張

アドレッシングモード

ここまで,ロードやストアの対象となる番地はレジスタ間接指定で指定すると説明してきた. しかし,実は単にレジスタを指定する以外にもいくつかの指定の方法がある.番地の指定の方法を アドレッシングモード という.

ARMのアドレッシングモードの基本

以下の式に沿って番地が計算される.

番地 = ベース + オフセット

ベースは必ずレジスタで指定する.オフセットは即値かレジスタで指定する.オフセットにレジスタを指定するときは,演算命令のフレキシブル第2オペランドのようにシフトさせることがきる.

番地の指定 番地の計算方法
[reg1] reg1
[reg1, #imm] reg1+imm
[reg1, reg2] reg1+reg2
[reg1, −reg2] reg1reg2
[reg1, reg2, shift] reg1 + (reg2shiftに従ってシフト)
[reg1, −reg2, shift] reg1 − (reg2shiftに従ってシフト)

ただし,reg1はベースを指定するレジスタ,immはオフセットを指定する即値,reg2はオフセットを指定するレジスタであり, shiftフレキシブル第2オペランドで説明したシフト命令である.

C言語とARMのアドレッシングモード

ARMのアドレッシングモードはC言語の構造体や配列のアクセス,ローカル変数へのアクセスを短く表現できるように設計されている.

例えば,次のC言語のプログラムを見てみよう.

struct item {
  int   id;      /* 4 byte */
  char *name;    /* 4 byte */
  int   price;   /* 4 byte */
};
struct item x;
void f() {
  x.id = 1;
  x.name = "yakuso";
  x.price = 10;
}

関数fでは構造体struct itemの型をもつ変数xの各要素に値を代入している.idstruct itemの先頭要素なので, x.idの番地とxの番地は一致する.したがって,x.id = 1

    mov     r0, #1
    ldr     r1, =x
    str     r0, [r1]

と書ける.priceは,前にint型とchar *型の要素が定義されているので,struct itemの先頭からは8バイトの位置にある. したがって,8バイトずらしてアクセスする.

    mov     r0, #1
    ldr     r1, =x
    add     r1, r1, #8     @ 8バイトずらす
    str     r0, [r1]

このように書いてもよいが,オフセットを指定して次のように短く書くことができる.

    mov     r0, #1
    ldr     r1, =x
    str     r0, [r1, #8]

次に配列のアクセスを見てみよう.

int a[100];
void f() {
  int i;
  for (i = 0; i < 100; i++) {
    a[i] = i + 1;
  }
}

このプログラムのa[i] = i + 1を実現する機械語命令を考える.a[i]の番地は,aの先頭番地からi要素分だけずれた位置である. aの要素はint型なので1要素4バイトである.したがって,

(a[i]の番地) = (aの番地) + i * 4

で計算できる.4倍は2ビット左シフトで表せるので,

(a[i]の番地) = (aの番地) + (i << 2)

でもよい.

iの値がr0に入っているとして,修飾のないレジスタ間接指定しか使えないとすると,a[i] = i + 1は次のように書くことになる.

    add     r2, r0, #1            @ r2 = i + 1
    ldr     r1, =a
    add     r1, r1, r0, lsl #2    @ r1 = a + (i << 2)
    str     r2, [r1]

シフト付きのレジスタオフセットを使うと,

    add     r2, r0, #1
    ldr     r1, =a
    str     r2, [r1, r0, lsl #2]  @ a + (i << 2) の番地に書き込む

と短く書くことができる.

LDR dst, =imm の秘密

MOV命令の制限を超えた定数をレジスタにロードしたい場合はLDR命令を使ってきた.

    ldr     r0, =0x12345678

定数をロードするためのこのLDR命令は,実はCPUの命令ではない.アセンブラがCPUの命令の組み合わせに変換している.

どのような命令に変換されているか覗いてみよう. chap4の中に12345678.sというファイルを作って次のプログラムを書きなさい.

    .section .text
    .global _start
_start:
    ldr     r0, =0x12345678
    lsr     r0, r0, #24
    mov     r7, #1
    swi     #0

実行可能ファイルをビルドし,さらに下記のコマンドで逆アセンブルしなさい.

$ arm-none-eabi-as 12345678.s -o 12345678.o
$ arm-none-eabi-ld 12345678.o -T ../raspbian.lds -m armelf -o 12345678  -- 実行可能ファイルを生成
$ arm-none-eabi-objdump -d 12345678  -- 逆アセンブル

その結果,次のように表示されるだろう.

12345678:     file format elf32-littlearm


Disassembly of section .text:

00010054 <_start>:
   10054:    e59f0008     ldr    r0, [pc, #8]    ; 10064 <_start+0x10>
   10058:    e1a00c20     lsr    r0, r0, #24
   1005c:    e3a07001     mov    r7, #1
   10060:    ef000000     svc    0x00000000
   10064:    12345678     .word    0x12345678

ldr r0, =0x12345678は10054番地の命令ldr r0, [pc, #8]と10064番地の.word 0x12345678で実現されている. 10064番地にはロードしたい定数そのものが置かれている.そして,10054番地でLDR命令を使って10064番地からr0レジスタにロードする. このとき,10064番地はプログラムカウンタのレジスタであるpcr15の別名)をベースにして,オフセット8を加えることで指定している. ARMでは,設計上の都合で,プログラムカウンタは実行中の命令の2つ先の命令の番地を保持している.この場合は1005c番地である. そこに8バイトを加えると,確かに10064番地にたどり着く.

このように,定数をロードするLDR命令は,プログラムの最後に定数を置いた上で,その場所を現在位置からの相対番地で指定してロードする命令に展開される.

なぜ,このような複雑な仕組みになっているのだろうか.いくつかのプログラムを逆アセンブルすると,どの命令も4バイトであることに気付くだろう. ARMではすべての命令の長さを4バイトと決めているが,4バイトの定数をロードする命令は4バイトでは表せない(定数だけで4バイト占めてしまう).上記の定数をロードする仕組みでは,定数を命令の外に置き,それをロードする命令に置き換えることで,この問題を解決している.

注意: 相対番地を表すためのオフセット値(例えば ldr r0, [pc, #8] における 8)の範囲にも制限があるので,定数を置く番地はLDR命令から離れすぎてはいけない. アセンブラが定数を置く適切な位置を見つけられない場合(例えば,1個のソースファイル中のコードが非常に長い場合など),アセンブル時にエラーになる.

ベースとなるレジスタの更新

ARMのアドレッシングモードには,ベースに指定したレジスタの値を更新するオプションがある.

  • 通常は,ベースにオフセットを足してアクセス対象番地を計算し,その番地にアクセスする.
  • !を付けることで,ベースにオフセットを足してアクセス対象番地を計算し,その番地をベースに指定したレジスタに書き戻してから,その番地にアクセスする.この動作はC言語の*++p = 0のようなプレインクリメント/デクリメントに対応する.
  • オフセットを[ ]の外に書くことで,ベースが指す番地にアクセスした後,ベースのレジスタにオフセットを加算する.この動作はC言語の*p++ = 0のようなポストインクリメント/デクリメントに対応する.
番地の指定 番地の計算方法 実行後のreg1の値
[reg1, #imm] reg1+imm reg1
[reg1, #imm]! reg1+imm reg1+imm
[reg1], #imm reg1 reg1+imm
[reg1, reg2] reg1+reg2 reg1
[reg1, reg2]! reg1+reg2 reg1+reg2
[reg1], reg2 reg1 reg1+reg2

reg2(レジスタオフセット)にはマイナス (−) を付けたりシフトを指定したりもできる.

データ領域を定義する疑似命令

第I部で使っていたNASMでは

x:  dd      0

のようにしてデータを定義した.このddはCPUではなくアセンブラが解釈する 疑似命令 (pseudo-instruction) である. 基本的にアセンブラごとに疑似命令の記法は異なる.上のような4バイトのデータの定義は,Raspberry Pi用に使っているGNU Assembler (arm-none-eabi-as) では次のように書く.

x:  .word    0

GNU Assembler の記法を次の表に示す. 詳細はGNU Assemblerのマニュアル を参照するとよい.

疑似命令 用例 意味
.byte val .byte 1 1バイトの領域を作り,その内容をvalで初期化する.
.word val .word 0x1234 4バイトの領域を作り,その内容をvalで初期化する.
.ascii str .ascii "hello" 文字列strを格納するのに十分な領域を作り,strで初期化する.
.space size .space 10 sizeバイトの領域を作る.

主記憶アクセス命令一覧

主記憶アクセス命令の一覧を示す.**は主記憶アクセスの対象番地であり,上で説明したアドレッシングモードのいずれかで指定する.

LDR dst, src**
主記憶上のsrcで示す番地から始まる4バイトの値を読出してレジスタdstに格納する.
STR src, dst**
レジスタsrcの値を主記憶上のdstで示す番地から始まる4バイトに書き込む.
LDRB dst, src**
主記憶上のsrcで示す番地から値を読出してレジスタdstに格納する.
LDRSB dst, src**
主記憶上のsrcで示す番地から値を読出して符号拡張し,レジスタdstに格納する.
STRB src, dst**
レジスタsrcの下位8ビットを主記憶上のdstで示す番地に書き込む.

注意 条件属性の接尾辞はアクセス幅の接尾辞(B/SB)より前に書く.

results matching ""

    No results matching ""