サブルーチン
これまでのプログラミングの授業で学習したように,サブルーチンとは, あるまとまった処理をひとかたまりにして,プログラム中のいろいろな場所から利用できるようにしたもののことである. サブルーチンというもの(いろいろな場所から利用できるひとまとまりの処理)をどのように実現するかは本来プログラマの自由であり,各自がCPU命令を組み合わせて好きに実現すればよいのだが,標準的なサブルーチンの実現方法を決めておくことにも利点がある. その標準的な実現方法に従ってプログラムすれば, 他の人が作ったサブルーチンを利用したり,自分のサブルーチンを他の人に提供することが容易になる,という利点だ. この「標準的なサブルーチンの実現方法」が,第I部 11章で学習した呼び出し規約だ.
呼び出し規約をひとつに定めるもう一つの利点は,CPU設計者がそれに合わせて命令を用意できるという点だ. ARMでもi386と同様に,標準的な呼び出し規約が定められており,それに合わせた命令が準備されている. ただし,本章では呼び出し規約の詳細はあまり気にせずに, CPUが提供している命令をうまく使ってサブルーチンを実現することを考える.
サブルーチンの実現に必要な命令
本章では,第I部と同じく,同じサブルーチンの中からでも呼び出せる,つまり再帰呼び出しに対応したサブルーチンを扱う.
再帰呼び出しに対応したサブルーチンの実現には,制御の移動(サブルーチンの呼び出しと復帰)と, スタックを使ったデータの保存が必要になる.どちらもi386で学習した事柄だが,この章ではARMでの実現方法を説明する.
制御の移動
サブルーチン呼び出しを実現するのに必要な機能は以下の2つである.
- サブルーチンに制御を移す機能
- サブルーチンの実行が終わった後に,サブルーチンの呼び出し元に制御を移す機能
サブルーチンに制御を移すにはBL命令を使う.既に学習したB命令(ジャンプ命令)でも制御を移すことはできるが,
呼び出し元がどこであったかが分からなくなる.BL命令は,呼び出し元(正確にはBL命令の次の命令)の番地をr14
レジスタに保存してからジャンプする.
これまでに学習した命令と違って,BL命令は暗黙にr14
レジスタを指定していることに注意しよう.
r14
レジスタにはこのような特別な役割があるので,リンクレジスタ と呼ばれており,
lr
という別名が与えられている.
サブルーチンの実行が終わった後,
サブルーチンの呼び出し元に制御を移すには,r14
レジスタが示す番地にジャンプすればよい.
しかしB命令のオペランドにはレジスタは指定できない
(番地の値またはラベルしか指定できない)ので,もうひとつ新しい命令が必要になる.
レジスタでジャンプ先を指定するにはBX命令を使う.
chap5 というディレクトリを作って, call.s というファイルに次のプログラムを書こう.
.section .text
.global _start
_start:
mov r0, #0
bl some_subroutine @ some_subroutine に制御を移す
@ 同時に,次の命令の番地を r14 レジスタに退避する.
add r0, r0, #1
add r0, r0, #1
@ r0 の中身を終了コードにして EXIT
mov r7, #1
swi #0
some_subroutine:
add r0, r0, #10
bx r14 @ 呼び出し元に戻る
適切にMakefileを書き,ビルドして
$ make call
$ scp call <Raspberry Pi のIPアドレス>:
Raspberry Pi上で実行して終了コードを調べよう.
> ./call
> echo $?
> rm call
何が表示されただろうか.期待通りだっただろうか.
参考:BL命令の中身
実はBL命令を使わなくとも,これまでに学習した命令だけで同じことが実現できる.r15
レジスタには,
現在実行中の命令の番地 + 8 が格納されている.したがって,call.sのBL命令は次の2命令に置き換えてもよい(が,読みにくいだけなので,BL命令を使った方がよいであろう).
mov r14, r15
b some_subroutine
なお,r15
は限られた場合にしか命令のオペランドに指定できない.例えば,意味があるかどうかは分からないが
r15
をシフトしてr14
に格納するといったことはできない.
ジャンプ関連命令一覧
- B label
- labelの付いた命令に制御を移す.
- BL label
- 次の命令の番地を
r14
レジスタに退避してから,labelの付いた命令に制御を移す. - BX r
- レジスタrが示す番地の命令に制御を移す.
スタック
第I部 第6章では,スタックの操作にPUSH命令・POP命令を使うことを学習した.
i386では,専用のスタックポインタレジスタがあり,PUSH命令とPOP命令は暗黙にスタックポインタを操作していた.
ARMでは,汎用レジスタのひとつであるr13
をスタックポインタとして使うことになっている.
そのため,r13
にはsp
という別名が付いている.
これは呼び出し規約の一部であり,
自分一人でプログラムを書くのであれば別のレジスタをスタックポインタとして使ってもよい.
ただし,OSの上でプログラムを実行する場合,
実行開始時にr13
レジスタがスタック領域を指すようプロセスの初期化ルーチンが初期化してくれているので,
敢えて別のレジスタを使う理由はないだろう.
PUSH命令/POP命令
PUSH命令とPOP命令に話を戻そう.ARMでは専用のPUSH命令やPOP命令は存在しない. これらの命令はベースとなるレジスタを更新するアドレッシングモードで実現する.
ARMの呼び出し規約では,スタックは高位番地から低位番地に向けて成長する(i386の場合と同じ). つまり,PUSH命令ではスタックポインタから4(= 1ワードのバイト数)を減じ,POP命令ではスタックポインタに4を加える. このとき,PUSH命令ではデータを書き込む前にスタックポインタを更新し,POP命令ではデータを読み出した後にスタックポインタを更新する.これにより,スタックポインタは常に,スタックトップにある有効なデータを指すことになる. 従って,
- r0をプッシュする命令は
str r0, [sp, #-4]!
- ポップしてr0に取り出す命令は
ldr r0, [sp], #4
となる.
スタックの用途
スタックの主な用途は
- レジスタの退避と復元
- サブルーチンの中だけで使われるメモリ領域(局所変数)の確保
である.i386では,CALL命令が戻り番地をスタックに保存したが,ARMでは戻り番地はリンクレジスタに保存される.サブルーチンがさらにサブルーチンを呼び出す場合は,リンクレジスタの値をスタックに退避してからBL命令を実行すればよい.
レジスタの退避と復元
第I部で作ったprint_eax
のような汎用サブルーチンはプログラムの様々な場所で利用したくなる.
その際,呼び出し元では,サブルーチンから戻ってきてもレジスタの内容は全て保存されていることを期待するだろう.
この期待に応えるためには,サブルーチンの先頭で各レジスタの値をスタックに保存しておき,サブルーチンから戻る前にレジスタに書き戻せばよい.
ただし,特別な用途のレジスタについては扱いが異なる.
r13
レジスタ(スタックポインタ) … 少なくともスタックに保存するのは無意味(金庫のありかをその金庫にしまうのと同じ).明示的に保存しなくても,プッシュとポップの回数が一致していれば,サブルーチンから戻るときにはサブルーチン呼び出し時と同じ内容になるはずである.r14
レジスタ(リンクレジスタ) … BL命令を実行した瞬間に書き変わってしまうので,サブルーチン側で呼び出し前の値を保存することはできない.r14
の保存が必要ならサブルーチンを呼び出す側が行う必要がある. 特に,サブルーチン内で他のサブルーチンを呼び出す場合は,その前にr14
を保存する必要がある(さもないと自分の呼び出し元に戻れない).なお,他のサブルーチンを呼び出すかどうかに関わらず,サブルーチンの先頭で他のレジスタと一緒にr14
を保存しておいてもよい(どうせr14
の値を使うのはサブルーチンから復帰するときであり,サブルーチンの途中では必要ないので,最初に退避し最後に復元すれば何も困らない)(下記のフィボナッチ数を求めるサブルーチンも参照).r15
レジスタ(プログラムカウンタ) … 保存する必要はない.
局所変数領域
サブルーチン中でレジスタに収まらないデータを利用する場合,主記憶に保存することになる.
例えば .word
疑似命令等を使って定義したデータ領域は,高級言語での大域変数に当たり,サブルーチンを再帰呼び出ししても同じ領域を使うことになる.
例えば再帰関数を使ってフィボナッチ数を計算するプログラムを考えよう.
fib(n) = n (n <= 1 のとき)
fib(n) = fib(n-1) + fib(n-2) (それ以外)
次のプログラムは再帰関数を使ってフィボナッチ数を求めるプログラムだが, 作業領域に大域変数を使っているのでうまく動かない.
.section .text
.global _start
.equ N, 13
_start:
ldr r0, =N
bl fib
mov r7, #1
swi #0
@ fib(r0) を求めて結果を r0 に戻す. r1〜r13の内容は保存される.
fib:
str r14, [sp, #-4]! @ push r14
str r1, [sp, #-4]! @ push r1
str r2, [sp, #-4]! @ push r2
cmp r0, #1 @ r0 <= 1 のときは return r0
bls owari
mov r1, r0 @ fib(r0-2) の呼び出しのために r0 を保存
sub r0, r0, #1
bl fib @ fib(r0-1) の呼び出す
ldr r2, =fibn_1
str r0, [r2] @ fib(r0-1)の結果を保存しておく
sub r0, r1, #2
bl fib @ fib(r0-2) の呼び出し
ldr r1, [r2] @ 保存しておいた fib(r0-1) の結果を読み出すが,中身が変わっている!
add r0, r0, r1 @ fib(r0-1)+fib(r0-2) のつもり
owari:
ldr r2, [sp], #4 @ pop r2
ldr r1, [sp], #4 @ pop r1
ldr r14, [sp], #4 @ pop r14
bx r14
.section .data
fibn_1: @ fib(n-1) の内容を保存しておく領域
.word 0
高級言語における局所自動変数, すなわち呼び出しのたびに新しく作られるデータ領域を使いたい場合,スタック上に領域を作ることで実現できる. スタックポインタを低位番地(スタックトップより先)にずらして隙間を作り,そこを使えばよい. 隙間にはスタックポインタからの相対番地でアクセスすることができる. スタックに退避したレジスタ値をポップする前に,ずらしたスタックポインタを元に戻すことを忘れてはいけない.
.section .text
.global _start
.equ N, 13
_start:
ldr r0, =N
bl fib
mov r7, #1
swi #0
@ fib(r0) を求めて結果を r0 に戻す. r1〜r13の内容は保存される.
fib:
str r14, [sp, #-4]! @ push r14
str r1, [sp, #-4]! @ push r1
str r2, [sp, #-4]! @ push r2
sub sp, sp, #4 @ 4バイトの局所変数領域を確保
cmp r0, #1 @ r0 <= 1 のときは return r0
bls owari
mov r1, r0 @ fib(r0-2) の呼び出しのために r0 を保存
sub r0, r0, #1
bl fib @ fib(r0-1)
str r0, [sp, #0] @ スタックポインタの指す領域に保存
sub r0, r1, #2
bl fib @ fib(r0-2)
ldr r1, [sp, #0] @ スタックポインタの指す領域から読み出す
add r0, r0, r1 @ fib(r0-1)+fib(r0-2)
owari:
add sp, sp, #4 @ スタックポインタを戻す
ldr r2, [sp], #4 @ pop r2
ldr r1, [sp], #4 @ pop r1
ldr r14, [sp], #4 @ pop r14
bx r14
複数ファイルの結合
複数のファイルをリンクする方法は第I部と同じである.各ソースファイルをアセンブルして .o ファイルを生成し,それら全てをリンカ(第II部ではarm-none-eabi-ld
)の引数として指定すればよい(第I部6-a節を参照).
なお,GNU Assembler では,NASMのextern
に相当する指定は不要である.定義されていないシンボルは全て自動的にextern
相当のものとして扱われる.
(そのため,ラベル名を書き間違っていてもアセンブラは何も言わない.リンクするときに「ラベルが未定義」というエラーになる.)