サブルーチン

これまでのプログラミングの授業で学習したように,サブルーチンとは, あるまとまった処理をひとかたまりにして,プログラム中のいろいろな場所から利用できるようにしたもののことである. サブルーチンというもの(いろいろな場所から利用できるひとまとまりの処理)をどのように実現するかは本来プログラマの自由であり,各自がCPU命令を組み合わせて好きに実現すればよいのだが,標準的なサブルーチンの実現方法を決めておくことにも利点がある. その標準的な実現方法に従ってプログラムすれば, 他の人が作ったサブルーチンを利用したり,自分のサブルーチンを他の人に提供することが容易になる,という利点だ. この「標準的なサブルーチンの実現方法」が,第I部 11章で学習した呼び出し規約だ.

呼び出し規約をひとつに定めるもう一つの利点は,CPU設計者がそれに合わせて命令を用意できるという点だ. ARMでもi386と同様に,標準的な呼び出し規約が定められており,それに合わせた命令が準備されている. ただし,本章では呼び出し規約の詳細はあまり気にせずに, CPUが提供している命令をうまく使ってサブルーチンを実現することを考える.

サブルーチンの実現に必要な命令

本章では,第I部と同じく,同じサブルーチンの中からでも呼び出せる,つまり再帰呼び出しに対応したサブルーチンを扱う.

再帰呼び出しに対応したサブルーチンの実現には,制御の移動(サブルーチンの呼び出しと復帰)と, スタックを使ったデータの保存が必要になる.どちらもi386で学習した事柄だが,この章ではARMでの実現方法を説明する.

制御の移動

サブルーチン呼び出しを実現するのに必要な機能は以下の2つである.

  1. サブルーチンに制御を移す機能
  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相当のものとして扱われる. (そのため,ラベル名を書き間違っていてもアセンブラは何も言わない.リンクするときに「ラベルが未定義」というエラーになる.)

results matching ""

    No results matching ""