主記憶領域
ロード命令とストア命令
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] | reg1−reg2 |
[reg1, reg2, shift] | reg1 + (reg2をshiftに従ってシフト) |
[reg1, −reg2, shift] | reg1 − (reg2をshiftに従ってシフト) |
ただし,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
の各要素に値を代入している.id
はstruct 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番地はプログラムカウンタのレジスタであるpc
(r15
の別名)をベースにして,オフセット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)より前に書く.