前回:4. GNU デバッガ (GDB) 次回:6. 制御構造(if文 while文)
目次(記事一覧)
※この記事はRoger Ferrer IbáñezさんのブログARM assembler in Raspberry Pi – Chapter 5の翻訳です。
分岐
これまでのサンプルコードは命令を順々に実行していました。ARMプロセッサーが順々に実行することしかできないのなら、作れるものは少なくなってしまいます。つまり、これまでに学んだことだけでは条件に従って制御フローを切り替えることができないのです。分岐命令はそれを実現するためにあります。
特殊レジスタ
第2章では、Raspberry PiのARMプロセッサーに16個の汎用レジスタがあり、一部はプログラムで特別な役割を担っていることを学びました。どのレジスタが特別なのかまでは当時の段階を踏まえてあえて説明していませんでした。
しかしそろそろr15
レジスタについて説明するべき段階が来ました。このレジスタは非常に特別なのでpc
という別名があります。混乱を招くためr15
と表記することはほとんどありません(ARMアーキテクチャ的には正しいですが)。ここからはこの記事においてもpc
と書くようにします。
pc
は program couter(プログラムカウンター)の略です。この呼称の起源はコンピューターの黎明期にあり、今日ではほとんど意味を成しません。一般にpc
レジスタは次に実行することになる命令のアドレスを格納しています(pc
は386やx86_64などではip
:instruction pointer、命令ポインタとも呼ばれます。)。
ARMプロセッサーが命令を実行する時、実行の最後で行われることは2通りあります。命令がpc
を変更しなかった場合(ほとんどの命令がそう)、pc
は4加算されます(add pc, pc, #4
とするみたいに)。4であるのは、ARMの命令が32ビットであり命令が4バイトずつ並んでいるからです。命令がpc
を変更した場合、pc
は変更された状態そのままで命令の実行を終えます。
プロセッサは命令を完全に実行すると、次に実行する命令のアドレスとしてpc
の値を使用します。よって、pc
を変更しない命令の後にはメモリ内で連続する次の命令が続きます(pc
が自動的に4加算されるから)。このように命令の一つが実行されその後にメモリ上の次の命令が実行されるということを命令の暗黙的順次実行と呼びます。しかし、命令が(単に4を加算する以外の方法で)pc
を変更することでプログラムの別の命令を実行できるようになります。このpc
の値を変更するプロセスを分岐と呼びます。ARMではこの操作を分岐命令を使って行います。
無条件分岐命令
無条件に分岐させる命令の書き方はb ラベル
(branch: 分岐)です。次のプログラムを見てください。
/* -- branch01.s */
.text
.global main
main:
mov r0, #2 /* r0 ← 2 */
b end /* branch to 'end' */
mov r0, #3 /* r0 ← 3 */
end:
bx lr
このプログラムを実行すると、エラーコードが2になることがわかります。
$ ./branch01 ; echo $?
2
ここではb end
命令によってend
ラベルのあるbx lr
への分岐が起こりました。一方mov r0
命令はまったく実行されていません(プロセッサーがその命令に到達することはありえません)。
この段階では、無条件分岐命令b
はそれほど役立たないように見えますが、ある状況、特に条件分岐命令も使用する場合では不可欠なものとなります。条件分岐について話したいところですがその前に条件について話す必要があります。
条件分岐命令
ただなんとなく分岐できるだけではあまり役立ちません。条件を満たしたときに分岐できる方がはるかに便利です。そのため、プロセッサーはいくつかの条件を評価できる必要があります。
話を続ける前に、cpsr
(Current Program Status Register)という別のレジスタについて明らかにする必要があります。このレジスタは少し特殊で、直接的に値を変更することに関してはこの章の範囲を超えます。とはいえ、このレジスタが保持する値は、命令を実行するときに読み取られたり変更されたりします。その値は、4つの条件コードフラグを含み、それぞれN (negative)、Z (zero)、C (carry)、V (overflow)フラグと呼ばれます。この4つの条件コードフラグは通常、分岐命令によって読み取られます。また、これらの条件コードフラグは、算術命令や特殊なテスト命令、比較命令によって必要に応じて更新できます。
4つの条件コードフラグの意味はおおよそ次のとおりです。
- N: 命令の結果が負の数の場合、有効になる。その他の場合は無効になる。
- Z: 命令の結果がゼロの場合、有効になる。その他の場合、無効になる。
- C: 命令の結果が完全な表現のために33番目のビットを必要とした場合に有効なる。例えば加算の結果が32ビット整数の範囲からあふれた場合。減算は特殊で、繰り下がりなしの場合に有効になり、その他の場合に無効になる。つまり、大きい数字から小さい数字を引くと有効になるが、逆にして引くと無効になる。
- V: 命令の結果が32ビットの2の補数で表すことができない値である場合、有効になる。
(※訳注 Cフラグ中の「繰り下がり」は存在しない33番目の桁からの繰り下がりという意味です。また、同じ数字で引き算した場合にもCフラグは有効になります。)
これで、条件分岐するために必要な全要素がそろいました。では分岐をする前に最初に2値の比較をしましょう。比較はcmp
命令で行います。
cmp r1, r2 /* updates cpsr doing "r1 - r2", but r1 and r2 are not modified */
/* 日本語訳:"r1 - r2"をしてcpsrレジスタを更新する。r1, r2レジスタ自体は変化しない。*/
この命令は、1つ目のレジスタの値から2つ目のレジスタの値を引きます。以下はこのコードで起こることの例です。
-
r2
がr1
より大きい場合、Nフラグが有効化する。なぜなら、r1-r2
が負になるからである。 -
r1
とr2
が等しい場合、Zフラグが有効化する。なぜなら、r1-r2
がゼロになるからである。 -
r1
が1、r2
が0なら、r1-r2
に繰り下がりは起きない。よってCフラグが有効化する。逆に、r1
が0、r2
が1なら繰り下がりが起こり、Cフラグが無効化する。 -
r1
が2147483647($ = 2^{31}-1 $ つまり32ビットの2の補数における最大の正の整数)であり、r2
が1なら、r1-r2
は2147483648($ = 2^{31} $)となるが、そのような数値は32ビットの2の補数で表現できないため、Vフラグが有効化する。
これらのフラグを利用してよく使う条件を表すことができます(以下の表を参照)。
条件 | 由来 | 意味 | フラグの状態(実際の値は有効は1、無効は0) |
---|---|---|---|
EQ |
equal | == |
Zが有効 |
NE |
not equal | != |
Zが無効 |
GE |
greater or equal than |
>= (2の補数表現で) |
「VとNが有効」 または 「VとNが無効」 (つまりV==N ) |
LT |
lower than |
< (2の補数表現で) |
「Vが有効かつNが無効」 または 「Vが無効かつNが有効」 (つまりV!=N 。GE の逆。) |
GT |
greater than |
> (2の補数表現で) |
Zが無効 かつ N==V
|
LE |
lower or equal than |
<= (2の補数表現で) |
Zが有効 または N!=V ※ |
MI |
minus | 負 | Nが有効 |
PL |
plus | 正またはゼロ | Nがゼロ |
VS |
overflow set | - | Vが有効 |
VC |
overflow clear | - | Vが無効 |
HI |
higher | > |
Cが有効 かつ Zが無効 |
LS |
lower or same | <= |
Cが無効 または Zが有効 (HI の逆) |
CS/HS |
carry set/higher or same | >= |
Cが有効 |
CC/LO |
carry clear/lower | < |
Cが無効 |
※ 訳注:翻訳前の英語にはLE
のフラグの状態が「"Zが有効" または "N==V"」のように書かれていますが訳者が調べた所誤りであると確認できたので正しいものを載せています。
※ 訳注:「2の補数表現で」というのはC言語などで言う「符号付き整数」のことです。
b
命令に条件を表す文字列を組み合わせることで、新しい命令を作ることができます。例えば、beq
命令はZフラグが1なら分岐します。条件分岐命令の条件が満たされない場合、分岐は無視され後に続く命令が実行されます。条件分岐命令の前に条件コードフラグが適切にセットされているかを確認するのはプログラマーの仕事です。
/* -- compare01.s */
.text
.global main
main:
mov r1, #2 /* r1 ← 2 */
mov r2, #2 /* r2 ← 2 */
cmp r1, r2 /* update cpsr condition codes with the value of r1-r2 */
beq case_equal /* branch to case_equal only if Z = 1 */
case_different :
mov r0, #2 /* r0 ← 2 */
b end /* branch to end */
case_equal:
mov r0, #1 /* r0 ← 1 */
end:
bx lr
このプログラムを実行すると、r1
とr2
が等しいためエラーコードは1になります。では5行目のmov r1, #2
をmov r1, #3
に書き換えてみてください。返り値のエラーコードが2になるはずです。error_different
では、end
へ分岐するb
命令でcase_equal
をスキップするようにしています。これがないと、エラーコードは常時1になってしまうので注意です。
今日はここまで。