2015年9月8日火曜日

ARMのプログラム技法

1ヶ月ほど前のブログで「数式お絵書き」を書いたのは, 手元のRaspberry PiでMathematicaが動いていたからだ.

Mathematicaも面白いが, Raspberry PiはそのCPUがARMなので, アセンブリ言語でプログラムを書くと, ARMのアーキテクチャがよく分るではないかと, 最近ではもっぱらアセンブリ言語のプログラムを書いている.

「ARM」という単語を最初に聞いたのは, 1995年頃の英国ケンブリッジでであったと思う. ケンブリッジ大学の計算機研究所のWilkes先生を訪ねたのだが, もう大学にもオリベッティの研究所にも出勤されてはいなかったので, オリベッティ研究所の所長のAndy Hopperさんに会って雑談している時, ARMといういいCPUがあるよと言われた. (この訪問のことは, bit 1996年1月号のaleph zeroに書いた.)

昨年8月, 偶然みつけたウェブページのことをツィッターに書いた.
50年ほど前にBCPLを開発したMartin Richardsが「青少年のためのRaspberry Pi上のBCPLプログラミング入門」を書いている(http://goo.gl/rXOKy2 ). 1つの言語に半世紀も情熱を持ち続けるとは見上げたもの. そのうち読んでみたい.
(「青少年のための...入門」と書いたのは, 元の題が「Young Persons Guide to BCPL Programming on the Raspberry Pi」であり, Benjamin Brittenの曲「The Young Person's Guide to the Orchestra」が日本では「青少年のための管弦楽入門」といわれているからだ.)

このツィートの後, 研究所でRaspberry Piを何個か購入し, 使えるようになってきた. またARMそのものにも興味も出てきた.



ARMでプログラムを書くに際して, 最低限の知識は次のようだ.

ArmはRiscだが, 命令幅が32ビットで, 水平型マイクロプログラムの様相を持つ. 16ビット命令のモードもある.

記憶装置とのデータ転送はldr, strだけ.

CMPがCPSR(Current Program Status Register, NZCVビット)をセットするほか, 加減乗算命令でも, addsのようにsを付けてCPSRをセットできる.

命令には, eq, ne, pl, miなど条件を付け, 実行するしないが指定できる.

このようなことを念頭に置いてプログラムを書く. その前後に指定された個数の空白を置き, aからbまでの数字を順に出力するにはどうするか. これはカレンダーのプログラムを書くときに必要になる. 例えば今年の9月のカレンダーは, cal 9 2015 で見ると
% cal 9 2015
   September 2015
Su Mo Tu We Th Fr Sa
       1  2  3  4  5
 6  7  8  9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30

なら, 先頭に2個の空白と最後に3個の空白を置き, 1から30まで印字するプログラムを書けばよい. いきなりプログラムではなく, テストプログラムとしては, 例えば前に3個, 後に2個の空白を置き, 1から3まで出力するプログラムは普通ならこうなろう.
(do ((i 0 (+ i 1))) ((= i 3)) (display "   "))
(do ((i 1 (+ i 1))) ((> i 3)) (display i))
(do ((i 0 (+ i 1))) ((= i 2)) (display "   "))
ARMの機能を活用するとこうなる.
.data
        .balign 4      /*4バイト境界を合わせる*/
a:      .word -2       /*出力する整数の初期値 2*/
ret:    .word 0        /*帰り番地の退避場所*/
s0:     .asciz ". *"   /*printfの書式*/
s1:     .asciz ".%2d"
s2:     .asciz "\n"
.text
        .balign 4
.global main
main:   ldr r0,reta    /*帰り番地を一時退避*/
        str lr,[r0]
        ldr r0,aa
        ldr r5,[r0]    /*出力する整数をr5へ*/
l0:     cmp r5,#1      /*出力する最初の数a*/
        rsbgts r0,r5,#3/*出力する最後の数b*/
        mov r1,r5      /*出力する数をr1へ*/
        ldrpl r0,s1a   /*書式0を使う*/
        ldrmi r0,s0a   /*書式1を使う*/
        bl printf      /*printfで出力*/
        add r5,r5,#1
 cmp r5,#6      /*終りの空白が済んだときのr5の値*/
 bmi l0
 ldr r0,s2a
 bl printf      /*書式2で改行を出力*/
 mov r0,#1      /*プログラムの返り値*/
        ldr r1,reta
        ldr lr,[r1]    /*帰り番地を復活*/
        bx lr

aa:     .word a
s0a:    .word s0
s1a:    .word s1
s2a:    .word s2
reta:   .word ret
想定通りの出力が得られる. (空白の様子が分かるように.や*が入っている.)
. *. *. *. 1. 2. 3. *. *
上のプログラムのミソは
l0:     cmp r5,#1      /*出力する最初の数a*/
        rsbgts r0,r5,#3/*出力する最後の数b*/
        mov r1,r5      /*出力する数をr1へ*/
        ldrpl r0,s1a   /*書式0を使う*/
        ldrmi r0,s0a   /*書式1を使う*/
        bl printf      /*printfで出力*/
        add r5,r5,#1
 cmp r5,#6
 bmi l0
のところだ. ラベルがl0:の行ではr5と1を比較, つまりr5から1を引く. r5が-2から0までは結果は負である. 次の行はこの結果が>0ならrsbつまり逆減算で, 3からr5を引き, 最後のsで結果をCPSRに置く. r5が3を超えると負になるわけだ.

次の行でr5を出力パラメータとしてr1へ置き, 前の計算の正負に従って書式をr0に置き, printfを呼ぶ.

r5は最後の数3を超えても増え続け, つまり空白を出力し続け, 6になるとcmpの結果が負ではなくなり, ループから抜ける.

要するに初めの空白部は, r5との比較が負で判定し, 終りの空白はr5と3を逆に引くことで, 範囲外をともに負にしている.

こんなことが出来るのはARMの命令が多様だからであった.

0 件のコメント: