前回:8.配列と構造体、その他のインデクシングモード 次回:10. 関数その2、スタック
目次(記事一覧)
※この記事はRoger Ferrer IbáñezさんのブログARM assembler in Raspberry Pi – Chapter 9の翻訳です。
これまでに学んだことは、レジスタ、算術演算、ロードとストア、分岐といったARMアセンブリ言語の基礎知識です。今回は、これまでの知識をフル活用しつつ関数という高度な抽象化手法を習得しましょう。
なぜ関数を使うのか?
関数はコードを再利用する方法です。複数回必要になるコードがある場合、それを再利用できることは一般に良いことです。コードを再利用した場合、その箇所だけ正しいことを確認するだけで済みます。一方、コードを繰り返し書いた場合、すべての箇所にてコードが正しいことを確認する必要があります。これは明らかに無駄です。また、関数はパラメーターを受け取ることもできます。関数に異なるパラメーターを渡すことで、単に再利用するだけでないさまざまな活用方法が生まれます。とはいえ、この関数という魔法の使用にはある程度の代償が伴います。関数を作るときは行儀良く振る舞うようにしなくてはなりません。
関数には決めごとがある
アセンブリコードは多くの力を秘めています。しかし、多くの力には多くの責任も伴います。アセンブリコードは非常にローレベルな部分を扱うため、様々なものを破壊することができます。使い方を間違えるとエラーや厄介なことが起こる可能性があるのです。すべての関数を同じように動作させるために、関数がどのようにどうする必要があるかを要求する呼出規約がすべての環境にあります。私たちはLinuxを動かしているラズベリーパイで作業するので、AAPCS(Procedure Call Standard for the Arm® Architecture)を使用します(場合によっては、RISC OSやWindows RTのような他のARMオペレーティングシステムもそれに従います)。ARMのサイトで関係するドキュメントを見つけることができますが、この章でも要約します。
新しい特殊な名前付きレジスタ
分岐に付いて触れた時、r15
レジスタをpc
とも呼ぶことを学びました。それ以降はr15
という呼び方を使わないようにしました。さらに今からは、r14
をlr
と、r13
をsp
と呼ぶことにしましょう。lr
はlink registerの略で、呼び出された命令の次の命令のアドレスです(意味は後ほどわかります)。sp
はstack pointerの略です。stack(スタック)は現在の関数が所有するメモリの領域で、sp
レジスタはスタックの先頭アドレスを保持します。スタックに関してはひとまず置いておき、次の章で解説します。
パラメーターについて
関数はパラメーターを受け取ることができます。最初から4個目までのパラメーターは順番にr0
、r1
、r2
、r3
レジスタにストアする必要があります。4個より多くのパラメーターを渡す場合はスタックを使う必要があるので、それについては次章で扱います。本章では4個以下のパラメーターしか渡しません。
「行儀の良い」関数のためのルール
関数をAAPCSに準拠させるためには、最低限次のルールに従う必要があります。
- 関数は、
cpsr
(ステータスレジスタ)の内容を想定してはならない。したがって、関数に入った直後では条件コードフラグN、Z、C、Vは不明である。 - 関数は、
r0
、r1
、r2
、r3
レジスタを自由に変更することができる。 - 関数は、
r0
、r1
、r2
、r3
レジスタがパラメーターの役割を果たしている場合を除き、それらの内容を想定することはできない。 - 関数は、
lr
を自由に変更できるが、関数に入るときのそれの値が関数を離れるときに必要になる(したがって、そのような値はどこかに保持しなくてはならない)。 - 関数は、残りのすべてのレジスタにおいて、関数を離れるときに値が復元される限りそれらのレジスタを変更できる。これは
sp
とr4
からr11
までのレジスタを含む。このことは、関数を呼び出した後、我々はr0
、r1
、r2
、r3
レジスタとlr
(だけ)が上書きされたと想定する必要があることを意味する。
関数を呼び出す
関数を呼び出す方法は2つあります。関数が静的にわかっている場合(つまりどの関数を呼び出すかわかっている場合)は、bl label
を使用します。label
は.text
セクションに定義されたラベルでなければなりません。これは、直接(または即時)呼び出しと呼ばれます。逆に、間接呼び出しをする場合は、最初に関数のアドレスをレジスタに格納し、次にblx Rsource1
を使用します。
両者とも動作は次のとおりです。関数のアドレス(bl
における直接エンコードされた値、またはblx
におけるレジスタの値)はpc
にコピーされ、bl
またはblx
命令の次の命令のアドレスがlr
にコピーされます。
関数を離れる
これまでに述べたように、行儀の良い関数はlr
の初期値をどこかに保持する必要があります。関数を離れるときに、その値を回収してレジスタに入れます(lr
に再び保持しすることも可能ですが強制ではありません)。その後、bx Rsource1
を使います(blx
も使えますが、lr
が無意味に更新されます)。
関数からデータを返す
関数はデータを返すとき、32ビット(またはそれ以下)のデータにはr0
を使う必要があります。つまり、C言語の型であるchar、short、int、long(それとまだ学んでいない浮動小数点数のfloat)はr0
に入れて返されます。32ビット以下ではないデータはスタックを介して返されます。
Hello world
普通これは、高水準言語で最初のプログラミングとして書くコードです。このARMアセンブリ言語でこれを書くためには、始めにたくさんのことを学ぶ必要がありました。とにかく、コードはここにあります。ARMアセンブリコードによる"Hello world"です。
(詳しい方への注意:スタックについては次の章まで解説しないため、わざわざ面倒なことをしているコードに見えるかもしれません)
/* -- hello01.s */
.data
greeting:
.asciz "Hello world"
.balign 4
return: .word 0
.text
.global main
main:
ldr r1, address_of_return /* r1 ← &address_of_return */
str lr, [r1] /* *r1 ← lr */
ldr r0, address_of_greeting /* r0 ← &address_of_greeting */
/* putsの第一パラメーター */
bl puts /* putsの呼び出し */
/* lr ← 次の命令のアドレス */
ldr r1, address_of_return /* r1 ← &address_of_return */
ldr lr, [r1] /* lr ← *r1 */
bx lr /* mainからリターンする */
address_of_greeting: .word greeting
address_of_return: .word return
/* External */
.global puts
コード内でputs
関数を呼び出します。この関数はC言語ライブラリに定義されており、int puts(const char*)
というプロトタイプです。puts
は、第一パラメーターとしてC-string(最後尾のみゼロのバイト列)のアドレスを受け取ります。実行されると、その文字列をstdout
に出力します(つまりデフォルトではターミナルに表示されます)。最後に、書き込んだバイト数を返します(※訳注 この動作はputs
関数の定義にはないので環境依存と考えられます)。
始めに、.data
セクション内でgreeting
ラベルを定義します。このラベルの部分には挨拶文のアドレスが含まれます。GNU as
(GNUアセンブラ)には便利な.asciz
ディレクティブがあります。このディレクティブは、文字列を表示するのに必要なバイト+最後の0のバイトを出力します。最後の0のバイトを自分で明示的に付け加える場合は、別のディレクティブである.ascii
ディレクティブを使うことができます。
挨拶文のバイト列のあと、次のラベルが4バイト境界にあることを保証してから、return
ラベルを定義します。そのラベルにはmain
関数のリターンに使うlr
の値を保持します。上述したように、これは、行儀の良い関数の要件である、lr
の初期値の保存です。そのためにlr
値を入れる場所を作るのです。
メイン関数の最初の2つの命令、つまりmain:
ラベルの直後の2行では、lr
の値を上記で定義されたreturn
変数に保存します。さらに次の命令では、puts
関数を呼び出すための引数を用意します。挨拶文のアドレスをr0
レジスタにロードします。このレジスタはputs
の第一パラメーター(puts
のパラメーターは1個だけですが)を保持します。次の命令では、関数を呼び出します。bl
命令がlr
に次の命令のアドレスをセットすることを思い出してください(次の命令とはldr r1, address_of_return
です)。つまりlr
の値がbl
によって上書きされてしまうのです。これこそ、main
関数の始めにlr
の値をコピーした理由です。
このまま、puts
が実行し挨拶文がstdout
に表示されます。ようやくmain
から正常にリターンするためにlr
の初期値を取得するときが来ました。取得したらリターンします。
このmain
関数は本当に行儀が良い関数でしょうか?答えはYesです。lr
を保持し関数を抜けるときにもとに戻しています。そして、r0
とr1
だけしか変更していません。puts
も行儀が良い関数であると想定できるので、すべてが正常に機能するはずです。このサンプルコードには出力に書き込まれたバイト数を確認できるというおまけもあります。
$ ./hello01
Hello world
$ echo $?
12
"Hello world"は11バイトだけ(最後のゼロは終了バイトの役割を果たしているだけでカウントされません)ですが、プログラムは12を返します。これはputs
が末尾に改行バイトを追加し、1つバイト数が増えるからです。
インタラクティブなサンプル
関数の呼び出し方を学習したので、関数を組み合わせることもできるようになりました。printf
とscanf
を呼び出して、数値を読み取りそれを標準出力に再び表示しましょう。
/* -- printf01.s */
.data
/* 1つ目のメッセージ */
.balign 4
message1: .asciz "Hey, type a number: "
/* 2つ目のメッセージ */
.balign 4
message2: .asciz "I read the number %d\n"
/* scanf用のフォーマット文字列 */
.balign 4
scan_pattern : .asciz "%d"
/* scanfが読み取った数字を保存する場所 */
.balign 4
number_read: .word 0
.balign 4
return: .word 0
.text
.global main
main:
ldr r1, address_of_return /* r1 ← &address_of_return */
str lr, [r1] /* *r1 ← lr */
ldr r0, address_of_message1 /* r0 ← &message1 */
bl printf /* printfの呼び出し */
ldr r0, address_of_scan_pattern /* r0 ← &scan_pattern */
ldr r1, address_of_number_read /* r1 ← &number_read */
bl scanf /* scanfの呼び出し */
ldr r0, address_of_message2 /* r0 ← &message2 */
ldr r1, address_of_number_read /* r1 ← &number_read */
ldr r1, [r1] /* r1 ← *r1 */
bl printf /* printfの呼び出し */
ldr r0, address_of_number_read /* r0 ← &number_read */
ldr r0, [r0] /* r0 ← *r0 */
ldr lr, address_of_return /* lr ← &address_of_return */
ldr lr, [lr] /* lr ← *lr */
bx lr /* lrを用いてmainを終了する */
address_of_message1 : .word message1
address_of_message2 : .word message2
address_of_scan_pattern : .word scan_pattern
address_of_number_read : .word number_read
address_of_return : .word return
/* External */
.global printf
.global scanf
この例では、ユーザーに数字を入力するように求めてからそれを再表示します。エラーコードでもその数字を返すので、うまく処理されているかを2通りの方法で確認できます。エラーコードで確認する場合は、入力した数値が255未満である必要があります(そうでない場合、エラーコードはその数値の下位8ビットのみが表示されます)。
$ ./printf01
Hey, type a number: 123↴
I read the number 123
$ ./printf01 ; echo $?
Hey, type a number: 124↴
I read the number 124
124
関数を定義する
関数を定義するのは初めてです。前のサンプルに対して、数字に5を掛けるという拡張を施します。
// printf01.sの一部
//省略
.balign 4
return: .word 0
.balign 4
return2: .word 0
.text
/*
mult_by_5 function
*/
mult_by_5:
ldr r1, address_of_return2 /* r1 ← &address_of_return */
str lr, [r1] /* *r1 ← lr */
add r0, r0, r0, LSL #2 /* r0 ← r0 + 4*r0 */
ldr lr, address_of_return2 /* lr ← &address_of_return */
ldr lr, [lr] /* lr ← *lr */
bx lr /* lrを用いてこの関数を終了する */
address_of_return2 : .word return2
.global main
main:
//省略
この関数にはmain
関数で使用しているようなリターン用の別の変数が必要となります。しかし、このようにしているのは説明のためで、実際のところこの関数は他の関数を呼び出さず、bl
やblx
命令がlr
を変更することはないため、lr
を保持する必要はありません。
ご覧の通り、一度関数が値を計算したら、それをr0
に保持するだけで十分です。この場合は、1つの命令だけで十分で、非常に簡単になります。
例の全体です。
/* -- printf02.s */
.data
/* 1つ目のメッセージ */
.balign 4
message1: .asciz "Hey, type a number: "
/* 2つ目のメッセージ */
.balign 4
message2: .asciz "%d times 5 is %d\n"
/* scanf用のフォーマット文字列 */
.balign 4
scan_pattern : .asciz "%d"
/* scanfが読み取った数字を保存する場所 */
.balign 4
number_read: .word 0
.balign 4
return: .word 0
.balign 4
return2: .word 0
.text
/*
mult_by_5 関数
*/
mult_by_5:
ldr r1, address_of_return2 /* r1 ← &address_of_return */
str lr, [r1] /* *r1 ← lr */
add r0, r0, r0, LSL #2 /* r0 ← r0 + 4*r0 */
ldr lr, address_of_return2 /* lr ← &address_of_return */
ldr lr, [lr] /* lr ← *lr */
bx lr /* lrを用いて関数を終了する */
address_of_return2 : .word return2
.global main
main:
ldr r1, address_of_return /* r1 ← &address_of_return */
str lr, [r1] /* *r1 ← lr */
ldr r0, address_of_message1 /* r0 ← &message1 */
bl printf /* call to printf */
ldr r0, address_of_scan_pattern /* r0 ← &scan_pattern */
ldr r1, address_of_number_read /* r1 ← &number_read */
bl scanf /* call to scanf */
ldr r0, address_of_number_read /* r0 ← &number_read */
ldr r0, [r0] /* r0 ← *r0 */
bl mult_by_5
mov r2, r0 /* r2 ← r0 */
ldr r1, address_of_number_read /* r1 ← &number_read */
ldr r1, [r1] /* r1 ← *r1 */
ldr r0, address_of_message2 /* r0 ← &message2 */
bl printf /* call to printf */
ldr lr, address_of_return /* lr ← &address_of_return */
ldr lr, [lr] /* lr ← *lr */
bx lr /* lrを用いてmain関数を終了する */
address_of_message1 : .word message1
address_of_message2 : .word message2
address_of_scan_pattern : .word scan_pattern
address_of_number_read : .word number_read
address_of_return : .word return
/* External */
.global printf
.global scanf
printf
関数を呼び出す前の辺りに注目してください。そこでは、printf
に渡すパラメーターを3つ用意します。フォーマット文字列とフォーマット文字列が参照する整数2個です。1個目の整数はユーザーによって入力された数値です。それに5を掛けた数値が2個目の整数となります。mult_by_5
を呼び出した後、r0
にはユーザーが入力した数に5を掛けた数が入ります。その数を第3パラメーターにするために、r2
にムーブします。その後、ユーザーによって入力された数をr1
にロードします。最後に、r0
にprintf
用のフォーマット文字列のアドレスをロードします。ここで、呼び出しの時点で引数の値が正しい限り、呼び出しの引数を用意する順番は関係がないことに注意してください。r0
を上書きする必要があることがわかっているので、便宜上、先にr0
をr2
にコピーしています。
$ ./printf02
Hey, type a number: 1234↴
1234 times 5 is 6170
今日はここまで。