前回は、.syntaxとかコメントとか、Armのアセンブリ言語をgasの上で書くときにちょっと「引っかかった」あれこれをいくつか投稿いたしました。今回は、Armのアセンブリ言語命令そのもので、ちょっと独特だな~と思った部分をメモっておきたいと思います。
※「ぐだぐだ低レベルプログラミング」投稿順indexはこちら
CPUは数あれど、基本的な命令セットは皆似たようなものではないかと思います。大雑把に分けたら、ザックリこんな感じですかね。
- 転送命令(ロード、ストア命令なども含む)
- 算術演算命令、論理演算命令
- 分岐命令(制御転送命令)
- その他制御など
この中でCPU毎の「色」が出やすい部分としては、以下の部分でしょうか。
- レジスタの本数と使い方
- メモリに対するアドレシング
- フラグ操作や使い方
ミソ汁はミソ汁なんだけれども、油揚げが入っているのか、大根が入っているのか程度の違い(かえって分からないか?)で差をつけてきている感じ。遠くから見たら50歩、100歩の違いにみえる各社アセンブリ言語の違いの中では、Armのアセンブリ言語は結構独特な「性格」が見え隠れしている部類ではないかと思われます。前回も例につかった16進文字列数値変換ルーチンを例に
Armっぽいな
と思われる命令のいくつかを取り上げてみます。
まずは、例にあげたルーチンの「仕様」から。C言語では strtol関数のような変換関数がありますが、そのような汎用なものではありませぬ。
- 変換対象文字列は、アスキー文字0~9、A~F、a~fからなるASCIIZ文字列
- 変換結果は符号なし32ビット整数
- 第1引数は変換対象文字列を指すポインタ
- 第2引数は変換結果を格納するunsigned int型変数を指すポインタ
- 変換成功すれば戻り値0を返す。変換対象文字列の長さが1文字から8文字でない場合、変換不能文字を含む場合はエラーとして0以上の数値を返す。また、その数値はエラーの発生した文字列の文字位置(先頭文字を1文字と数える)を示す。なお、エラー位置が2以上の場合、そこまでに変換可能であった部分が第2引数のポイントする先に数値として書き込まれる。エラー位置が先頭の場合は不定。
C言語からは以下のような関数プロトタイプを用いて呼び出すことが可能です。(コンパイラはEABI準拠のこと)
unsigned int hex(const char* hexstr, unsigned int* result);
前置きが大分長くなりましたが、該当の関数のソースコードがこちら(急いで書いたので、汚いところはご勘弁を)前回記したとおり、.syntax unified しているので即値に#を付けていない書式なのでご注意を。
.syntax unified .global hex .text .align 4 hex: push {r2-r4} mov r4, r0 mov r0, 1 mov r3, 0 1: ldrb r2, [r4] cmp r2, 0 beq 6f cmp r0, 9 beq 5f @ Error # Processing cmp r2, 0x66 @ f bhi 5f @ Error cmp r2, 0x60 @ ` bhi 2f @ a-f cmp r2, 0x46 @ F bhi 5f @ Error cmp r2, 0x40 @ @ bhi 3f @ A-F cmp r2, 0x39 @ 9 bhi 5f @ Error cmp r2, 0x2F @ / bhi 4f @ 0-9 b 5f @ Error # 0-9 4: sub r2, 0x30 b 8f # A-F 3: sub r2, 0x37 b 8f # a-F 2: sub r2, 0x57 # next 8: add r3, r2, r3, lsl 4 add r0, r0, 1 add r4, r4, 1 b 1b 6: # Check normal end. cmp r0, 1 beq 9f @ First Char is null. # Normal end. mov r0, 0 5: @ Error end. str r3, [r1] 9: pop {r2-r4} bx lr
さて、Armらしい命令といえば、先頭の
push {r2-r4}
からしてArmらしいのではないでしょうか。関数の中で書き換えてしまうr2, r3, r4のレジスタをスタックに退避しているところ。この関数は内部で他の関数を呼び出さないですが、該当の関数の中で関数を呼び出す場合は
push {r2-r4, lr}
のように lr(リンク・レジスタ、関数からの戻り番地が格納されている)を含めるのが一般的かと思います。当然、関数末尾の bx lr (リンクレジスタの番地に分岐。x86のRET命令相当)の前にも対になる pop 命令が置かれています。
昔風のRISCプロセッサではそもそも push/pop などない
ものもある中で、ArmはCISC風にpush/popが使えるだけでなく、何個もpush/popを並べることなく、必要なレジスタを列挙するだけで一命令で書けてしまいます。まあ、最近のプロセッサではこれに近い命令を持っているものがあるのでArmが唯一というわけではないですが、かなり便利。ただし、対象になるレジスタはモードにより変化します(Thumbはレジスタ8本だし)。
次にArmらしいアセンブリ言語命令と言えば、ローカルラベル 8:の直下にある
add r3, r2, r3, lsl 4
でしょうか。やたらとオペランドが4個もあるように見える書式。
- ニーモニック add。言わずと知れた、算術加算命令
- 一番左の r3 は、演算結果を格納するデスティネーション
- 次の r2 は、加算の一方のソースレジスタ
- その次のr3は、加算のもう一方のソースレジスタ
つまりここまでであれば、r2の内容とr3の内容を足した結果をr3に書き戻すという操作です。しかし、最後のオペランド
lsl 4
が、結構Armにユニークなもの。ソースの一方に対する「修飾」で、Arm社が
フレキシブル第2オペランド
と呼んでいる記法の一部。この場合、ソースレジスタ指定の2つ目のr3の内容を先に lsl 4 = logical shift left 4bit してから足し算に使うという意味。他の多くのプロセッサであれば、r3をまず4ビット左シフトする命令を実行してから、次に加算命令を発行するところを1命令で始末しています。近代的なプロセッサでは、一命令にまとめたからと言って2命令より速いかどうかは分かりませんが、すくなくともマシンコードのバイト数は短くなる筈。初期のArmは組み込み用途のためにオブジェクトのフットプリントをともかく小さくすることにフォーカスしていたのでこのような命令を備えるにいたったのだと想像します。最近のプロセッサ設計上は都合の良い命令とも思えませんが、ともかく使える場合には使ってしまいます。勿論使えるのは左シフトに限りません。