算術命令
算術命令を使ったプログラムを作る.第I部と同じように,まずは計算結果をプログラムの終了コードにして,echo $?
で確認することにする.
exitシステムコール
i386でもARMでも,Linux上でプログラムを実行する限り,プログラムを終了させるにはexit
システムコールを使う.
では,開始早々にexit
システムコールを実行してプログラムを終了するだけのプログラム作ってみよう.次のプログラムを入力し,123.s
というファイル名で保存しなさい.
@ 123を終了コードとして出力するあまり意味のないプログラム
.section .text
.global _start
_start:
mov r7, #1 @ exitのシステムコール番号
mov r0, #123 @ 終了コード
swi #0 @ システムコールの発行
最初の行は,このプログラムが何をするのかを簡潔に書いたコメントである.ARMのアセンブラでは,@
以降がコメントとして扱われる.
次の3行はi386アセンブラのときと同じ「決まり文句」だ.
.section .text
は,以降が機械語命令であることを表す..global _start
は,ラベル_start
を外部から参照可能にすることを表す._start:
はこの行にラベル_start
を設置している._start
というラベルはプログラムの開始位置を表す.
続く3行でexit
システムコールを発行している.正確には,最後のswi #0
がexit
システムコールの発行で,直前の2行はその準備である.
ARM Linuxではシステムコールを発行するときに,r7
レジスタにシステムコール番号を,r0
レジスタから順にパラメタを格納しておくことになっている.
exit
のシステムコール番号は1である.exit
システムコールはパラメタとして終了コードだけを受け取るので,r0
に終了コードの123を格納する.
ARMのアセンブラでは定数に#
を付けることになっている.
ではこのプログラムを実行してみよう.
$ arm-none-eabi-as 123.s -o 123.o
$ arm-none-eabi-ld 123.o -T ../raspbian.lds -m armelf -o 123
$ scp 123 <Raspberry PiのIPアドレス>:
Raspberry Piの端末で,
> ./123
> echo $?
123
即値のロード命令
レジスタに値を格納することをロードすると言う.
先程のプログラムではr0
レジスタに123をロードするためにMOV命令を使って
mov r0, #123
としていた.では12345678をロードしてみよう.123.sのMOV命令の行を変更し,
mov r0, #12345678
とすると何が起こるだろうか.
実は,MOV命令でロードできる即値には制限がある.その制限を超える定数のロードにはLDR命令を使う.
ldr r0, =12345678
LDR命令を使うときは定数の前に=
を付ける.
オペランドにラベルを指定するとアセンブラがそれを値に置換する.ラベルの値はそのラベルが付けられた命令やデータの番地 (address) である.この番地がいくらになるかはプログラムを書く時点では分からないので,(MOV命令の制限に引っかからないよう)用心のためにLDR命令を使う.例えば,hello.sでは
ldr r0, =msg
のように書いていた.
データ転送命令
定数のロードを含めたデータ転送命令を示す.これ以降の命令の凡例中の「*」が付いたオペランドは,レジスタだけでなく,先頭に # を付けた定数も指定できることを表している(ただし,ある制限に引っかからない定数に限る.この制限は他の命令でも同じである).「*」が付いていないオペランドにはレジスタしか指定できない.
- MOV dst, src*
- srcの値をレジスタdstにコピーする.srcにはレジスタだけでなく,先頭に # を付けた定数(ただし制限有り)も指定できる.
- C言語風に書くと: dst = src
- LDR dst, =imm
- 定数immをレジスタdstにロードする.定数immには先頭に = を付ける.
- C言語風に書くと: dst = imm
演習2.2-1(個人課題: この課題のレポートは各自が書くこと) MOV命令でロードできる即値の制限について調べ,実験レポートにまとめなさい.
- 調査は班で協力して行いなさい.
- 調査のための実験方法は各班で工夫して設計しなさい.
- 制限はARMプロセッサのバージョンや動作モードによって異なる.本科目 第II部で使っている実行モード(Raspberry Pi 3 Model Bの上のRaspbian上でプログラムを実行開始したときの実行モード)での制限を調べなさい.
- 書籍やオンラインドキュメント等を参照してもよいが,それによって得た答えが正しいことを実験によって確かめること.
- 実験レポートの書き方に従って,目的,方法,結果,考察を書きなさい(生物や自然現象を調べるのと同じように,MOV命令を観察対象だと思って実験や調査を行い,結果を実験レポートにまとめなさい).ここでは,レポートの体裁が整っている前提で,次のことが特に評価の観点となる.
- 目的に対して適切な実験であるか.
- 読者が再現可能なくらい明確に実験方法が述べられているか.
- 実験の結果から論理的な考察により結論が導き出されているか.
- 目的と結論が整合しているか.
(補足1)
- 実験の目的は以下のように表せる:
ARMプロセッサのMOV命令でロードできる即値の範囲を表す規則を明らかにする. - 「具体的な規則を仮説として先に述べ,実験によって確かめる」という書き方でも「実験の結果から具体的な規則を推定する」という書き方でも良いが,いずれにせよその規則の正しさが実験結果から納得できることが重要(そのように実験を設計することが重要).
- MOV命令でロードできる即値の範囲を表す規則として必要十分な規則を示すことが望ましい.つまり「その範囲はロードできる,かつ,それ以外の範囲はロードできない」ような範囲を示すことが望ましい.
(補足2)
- さらに,次の項目も「実験の目的」に加えて,結果を述べるとなお良い(オプション項目):
ARMプロセッサのMOV命令がなぜそのような設計になっているのか(ある程度)明らかにする. - もちろん何かを測定して設計理由がわかるわけではなく,上記の実験や調査によって得られた様々な事実から,妥当と考えられる設計理由を推定する,ということ(観察結果から背後の法則や機構(「なぜそうなったのか」)を推定することに当たる).
(補足3)
- 問いは「MOV命令でロードできる即値の制限」であって, 「exitシステムコールで出力できる値の範囲」ではない. Linuxのexitシステムコールで出力できる値は,CPUによらず,0〜255である.
- 「MOV命令でロードできない即値」とは,
mov r0, x
という記述に対応するARMの機械語が存在しないような即値 x のことである. - 「ある数 x は指定できて x + 1 は指定できない」からと言って, 「x 以下の即値を指定できる」と結論付けるのは早計である. 実際,「MOV命令でロードできる即値の範囲を表す規則」はもっと複雑である.
加算命令
次は123 + 45を計算するプログラムを作ろう.
@ 123 + 45を計算して終了コードとして出力するプログラム
.section .text
.global _start
_start:
mov r1, #123 @ r1 = 123
add r0, r1, #45 @ r0 = r1 + 45
mov r7, #1
swi #0
123 + 45 を計算しているのは,
mov r1, #123
add r0, r1, #45
の2行である.まず
- MOVは,第1オペランドのレジスタに第2オペランドの値を代入する命令である.
したがって,r1
に123が格納される.ここまではi386のアセンブラと変わらない.次のADDは様子が異なる.i386のアセンブラでは,
add ebx, 45
のように,ADDのオペランドが2つだった.一方,ARMアセンブラではオペランドが3つある3オペランド命令になっている.
- ADDは第1オペランドに第2オペランドと第3オペランドの値の和を格納する命令である.
複数のオペランドに同じレジスタを指定することもできる.例えば,次のプログラムではr1
に45が加算される.
add r1, r1, #45
それでは,123.s
を上記のように変更して,アセンブルして実行してみよう.
$ arm-none-eabi-as 123.s -o 123.o
$ arm-none-eabi-ld 123.o -T ../raspbian.lds -m armelf -o 123
$ scp 123 <Raspberry PiのIPアドレス>:
> ./123
> echo $?
レジスタ
以下の演習で123+45−67+8−9の計算をするのだが,そのためには中間結果を保存するレジスタが必要になる.ARMでは16本の汎用レジスタを使うことができる.それぞれのレジスタにはr0
からr15
というレジスタ名が付いている.中には特定の用途に利用することに決められているレジスタもあり,それらには別名が与えられている.関数呼び出しをしたり,C言語のプログラムと結合したりするときには,レジスタの用途が重要になるが,今のところはr0
からr14
を自由に使ってよい.r15
はプログラムカウンタ(実行中の命令が置かれた番地を保持するレジスタ.i386のEIPに当たる)なので使ってはいけない.
加減算命令一覧
主な加減算命令を示す.表中の * はフレキシブル第2オペランド(後述)を表す. src1 はレジスタしか指定できない. src2 は即値も指定できるが,即値の場合はMOV命令の第2オペランドと同じ制約を受ける.
- ADD dst, src1, src2*
- src1とsrc2の和をdstに格納する.
- C言語風に書くと: dst = src1 + src2
- ADC dst, src1, src2*
- src1とsrc2とキャリーの和をdstに格納する.
- C言語風に書くと: dst = src1 + src2 + C
- SUB dst, src1, src2*
- src1からsrc2を引いた値をdstに格納する.
- C言語風に書くと: dst = src1 − src2
- RSB dst, src1, src2*
- src2からsrc1を引いた値をdstに格納する.
- C言語風に書くと: dst = src2 − src1
- SBC dst, src1, src2*
- src1からsrc2を引き,(1 − キャリー)を引いた値をdstに格納する.
- C言語風に書くと: dst = src1 − src2 − (1 − C)
- RSC dst, src1, src2*
- src2からsrc1を引き,(1 − キャリー)を引いた値をdstに格納する.
- C言語風に書くと: dst = src2 − src1 − (1 − C)
演習2.2-2 123+45−67+8−9を計算して結果を出力するアセンブリ言語のプログラムを作りなさい.ソースファイルは,サブディレクトリchap2の中に123.sという名前で作成すること.
乗除算命令
ARMには多彩な乗算関連の命令が用意されている.詳しくはマニュアルの4.4節にある. ここでは主なものだけを説明する.
乗除算命令一覧
乗除算命令のオペランドにはレジスタしか指定できない.
- MUL dst, src1, src2
- src1とsrc2の積の下位32ビットをdstに格納する.dstとsrc1には異なるレジスタを指定しなければならない.
- C言語風に書くと: dst = src1 * src2
- UMULL dstL, dstH, src1, src2
- src1とsrc2の積の上位32ビットをdstHに,下位32ビットをdstLに格納する.符号無し整数として計算する.dstHとdstLとsrc1にはすべて異なるレジスタを指定しなければならない.
- C言語風に書くと: (dstH, dstL) = src1 * src2
- SMULL dstL, dstH, src1, src2
- src1とsrc2の積の上位32ビットをdstHに,下位32ビットをdstLに格納する.符号付き整数として計算する.dstHとdstLとsrc1にはすべて異なるレジスタを指定しなければならない.
- C言語風に書くと: (dstH, dstL) = src1 * src2
- UDIV dst, src1, src2
- src1をsrc2で割った商をdstに格納する.符号無し整数として計算する.
- C言語風に書くと: dst = src1 / src2
- SDIV dst, src1, src2
- src1をsrc2で割った商をdstに格納する.符号付き整数として計算する.
- C言語風に書くと: dst = src1 / src2
演習2.2-3 12356秒が何時間何分何秒か計算し,時×10 + 分×5 + 秒 を計算して出力するアセンブリ言語プログラムを作りなさい。 ソースファイルは,サブディレクトリ chap2 の中に hms.s という名前で作成すること.
ヒント: A ÷ B = Q あまり R のとき, A = BQ + R なので, R = A − BQ である.
論理演算命令
本科目 第III部では,ARM CPUを使ってLEDやスピーカなどの外部機器を制御する. 外部機器は,メモリ中の特定のワードの中の,特定のビットを1にセットしたり0にクリアしたりして制御する. それには論理演算命令を用いる.
特定のビットを1にセットするには論理和を計算すればよい.例えば,r0
とr1
の論理和をr0
に代入すれば,r0
中のビットのうちr1
の中で1である箇所だけ1にセットしたことになる.
r0 = 01000101 00000100 00010001 11010000
r1 = 00001111 00000000 00000000 00000000
-----------------------------------------------
r0|r1 = 01001111 00000100 00010001 11010000
論理和の計算はORR命令で行う.上記の計算をして,結果をr0
に書き戻すには,
orr r0, r0, r1
とする.
逆に特定のビットをクリアするには,BIC命令を使うと便利だ.
i386では,特定のビットをクリアするためには
NOT命令とAND命令を組み合わせる必要があった.例えば,eax
中のビットのうち,ebx
の中で1である箇所だけ0にクリアするには,
not ebx
and eax, ebx ; eax = eax & ~ebx
のようにする必要があった.ARMでは,
bic r0, r0, r1
と1命令で書ける.
r0 = 01000101 00000100 00010001 11010000
r1 = 00001111 00000000 00000000 00000000
-----------------------------------------------
r0&~r1 = 01000000 00000100 00010001 11010000
論理演算命令一覧
- MVN dst, src*
- srcの全ビットを反転した値をdstに格納する
- C言語風に書くと: dst = ~src
- AND dst, src1, src2*
- src1とsrc2の論理積をdstに格納する.
- C言語風に書くと: dst = src1 & src2
- ORR dst, src1, src2*
- src1とsrc2の論理和をdstに格納する.
- C言語風に書くと: dst = src1 | src2
- EOR dst, src1, src2*
- src1とsrc2の排他的論理和をdstに格納する.
- C言語風に書くと: dst = src1 ^ src2
- BIC dst, src1, src2*
- src1からsrc2で1になっているビットを0にクリアした値をdstに格納する.
- C言語風に書くと: dst = src1 & ~src2
フレキシブル第2オペランド
ARMには単独のシフト命令は存在しない.実は,シフト命令は乗除算命令を除くほとんどの演算命令とMOV命令におまけで付いてくる.
これまでに示した命令一覧で「*」が付いたオペランドは,フレキシブル第2オペランドと呼ばれる. フレキシブル第2オペランドにレジスタを指定する場合,そのレジスタの中身をシフトして得られる値を被演算数にすることができる.
例えば,a
にx
の4倍を加えてp
に代入するというプログラムは,C言語では
p = a + (x << 2)
と書く.2ビット左シフトによって4倍を表している.
同様の計算で,r1
にr2
の4倍を加えr0
に代入するというプログラムは ARM アセンブラで
add r0, r1, r2, lsl #2
と書く.このr2, lsl #2
がフレキシブル第2オペランドである.
lsl #2
は2ビット左論理シフト (Logical Shift Left) せよという意味になっている.
この計算は配列アクセスでよく現れる.C言語の
a[x]
という配列アクセスでは,a
の要素がint
のように4バイトの型の場合,上記のような計算でアクセスする番地を求めることになる.
MOV命令のフレキシブル第2オペランドでシフトを行うと,ただのシフト命令として使うことができる.
例えば,r1
を3ビット算術右シフトした(8で割った)値をr0
に格納するには
mov r0, r1, asr #3
とする.lsl
やasr
の後ろにはシフト量を即値かレジスタで指定する.レジスタで指定する場合,
mov r0, r1, asr r3
のように書く.
まるでASR命令というシフト命令があるかのように
asr r0, r1, #3
とプログラムを書いても,アセンブラが気を利かせて,
mov r0, r1, asr #3
と同じ機械語にアセンブルしてくれる.しかし,ASRの第3オペランドは普通のオペランドとは異なることに注意しておく必要がある.
フレキシブル第2オペランドで指定できるシフト命令
ニモニック | 意味 |
---|---|
LSL #n | nビット左シフトする.nには1から31までの整数が指定できる. |
LSL r | rレジスタの下位8ビットで表されるビット数だけ左シフトする. |
LSR #n | nビット論理右シフトする.nには1から32までの整数が指定できる. |
LSR r | rレジスタの下位8ビットで表されるビット数だけ論理右シフトする. |
ASR #n | nビット算術右シフトする.nには1から32までの整数が指定できる. |
ASR r | rレジスタの下位8ビットで表されるビット数だけ算術右シフトする. |
ROR #n | nビット右ローテートする.nには1から31までの整数が指定できる. |
ROR r | rレジスタの下位8ビットで表されるビット数だけ右ローテートする. |
RRX | ローテート対象のレジスタとキャリーフラグをつなげて33ビットの値とみなし,1ビット右ローテートする. |
左シフトでは最下位ビットに0が補充される.右シフトでは,論理右シフトのときは0が補充され,算術右シフトでは符号桁と同じ値が補充される.
1ビット右ローテートとは,1ビット右シフトして,右にあふれたビットを左端のビットとする処理のことである. 複数ビット右ローテートは,1ビット右ローテートを繰り返した結果になる演算である.
演習2.2-4 プログラムの1行目に.equ
疑似命令を使って名前付き定数N
を
.equ N, -5
のように定義し,N
の絶対値を計算して出力するアセンブリ言語プログラムを作りなさい.プログラムは次の条件を満たすこと.
- (条件1)
N
の定義を −255 以上 255 以下のどんな整数に書き換えても正しく計算できること. - (条件2) 条件分岐命令(次章で扱う)は使わないこと.
- (条件3) exitシステムコール実行時の
r0
の値(下位8ビットだけでなくr0
全体)がN
の絶対値に等しいこと.
ソースファイルは,サブディレクトリchap2の中にabs.sという名前で作成しなさい. (オプション項目: 5命令以下のプログラムであればより良い.)
ヒント:2の補数による負数の表現には次のような特徴がある.
- 最上位ビットが符号ビットになっており,正の数または0では最上位ビットが0,負の数では最上位ビットが1になっている.
- 整数nを2の補数により表現したビット列に対し,各ビットの0と1を反転したビット列が表す整数は,−n − 1になる.
(例)
5 = 00000000 00000000 00000000 00000101
-6 = 11111111 11111111 11111111 11111010
演習 2.2-5 サブディレクトリchap2の中に,演習2.2-2〜2.2-4で作成したプログラムをビルドするためのMakefileを作りなさい.ただし,以下を満たすように記述すること.
- 各演習問題においてソースファイル名が xxx.s と指定されている場合,
make xxx
を実行すると,xxx.s から(xxx.oを経由して)ビルドされた実行可能ファイル xxx が生成される.make xxx.o
を実行すると,xxx.s をアセンブルした結果であるファイル xxx.o が生成される.
- 上記のいずれにおいても,Raspberry Piへの実行可能ファイルの転送は行わない(実行可能ファイルの転送がしたい場合は,上記のいずれとも異なるターゲット名を付ける).
make clean
を実行すると,実行可能ファイルや .o ファイル等が削除される.