Edited at

[アセンブラ] ARMの仕様を見てみる

More than 3 years have passed since last update.

前回の記事(アセンブラに手を出してみる)でx86、x64のアーキテクチャのアセンブラについて簡単にまとめました。

今回はARM向けの仕様を見てみようと思います。

(ちなみに最新のiPhoneとかではarm64プロセッサを搭載しているので、ここでまとめたのとは若干異なると思います)

つらつらとデバッグ時に遭遇したものとかをメモしていくので随時更新です。


汎用レジスタ

なにはともあれ、レジスタの状況を見ないとなにも始まらないのでまずはレジスタです。

ARMプロセッサはr0r15の16本、32bitレジスタを持っています。

このうち、r15は「プログラムカウンタ」で、実行中のメモリアドレスを保持します。

(x86とかではIPレジスタあたりが似たものでしょうか)

r14はリンクレジスタでBL命令(サブルーチンコール)で分岐した場合の戻りアドレスを保持します。

(x86ではbpとかかな?(こっちのbpの認識も怪しいけど・・))

x86と似たように、このレジスタも関数コール時にスタックに積まれるようです。(ひとつの戻りアドレスしか保持できないため)

(なお、通常はr13がスタック領域のレジスタの模様)


名称付き汎用レジスタ

ちなみに上記の役割があるレジスタはそれぞれ、以下のように呼ばれることもあるようです。(数字で表されるより分かりやすい)

レジスタ
別名
_
説明

r0
a1
-
汎用レジスタ

r1
a2
-
汎用レジスタ

r2
a3
-
汎用レジスタ

r3
a4
-
汎用レジスタ

r4
v1
-
汎用レジスタ

r5
v2
-
汎用レジスタ

r6
v3
-
汎用レジスタ

r7
v4
wr
汎用レジスタ

r8
v5
-
汎用レジスタ

r9
v6
sb
汎用レジスタ

r10
v7
sl
汎用レジスタ

r11
v8
fp
汎用レジスタ

r12
ip
-
汎用レジスタ

r13
sp
-
汎用レジスタ、スタックポインタ

r14
lr
-
リンクレジスタ

r15
pc
-
プログラムカウンタ

spなどはそれぞれ名称の頭文字ですね。


W**, X**

Xcodeでデバッグしていると見るこんな命令。

stp    x29, x30, [sp, #-16]!

STP命令は「レジスタペアストア」ということのよう。

ドキュメント

ドキュメントの一部を引用すると以下の感じ。

STP  Wt1, Wt2, [Xn|SP], #imm    ; 32 ビット汎用レジスタ、ポストインデクス

STP Xt1, Xt2, [Xn|SP], #imm ; 64 ビット汎用レジスタ、ポストインデクス

Xt1
転送される最初の汎用レジスタ(64 ビット)名を 0 ~ 31 の範囲内で指定します。


コメント

「ARM」では@以降がコメントとして扱われます。


条件指定実行

ARMの仕様として、演算命令に対して条件指定することができるようです。

例えば以下。

SUBEQ a, b, c

SUBは減算命令ですね。続くEQはEqualのEQです。

つまり、所定のフラグがEqualの場合に実行する、という意味になります。


ステータスレジスタ

bit
フラグ
意味
備考

31
N
ネガティブ
2の補数表現で負なら1

30
Z
ゼロ
ゼロなら1

29
C
キャリー/ボロー
無符号加減算の桁あふれの時1

28
V
オーバーフロー
符号付加減算の桁あふれの時1

27
Q
オーバーフロー
オーバーフロー、飽和(DSP)の時1

条件 ()
意味
フラグ条件

AL
無条件に実行
無条件

EQ
等しい
Zセット

NE
等しくない
Zクリア

MI

Nセット

PL
正かゼロ
Nクリア

VS
オーバフロー
Vセット

VC
オーバフローでない
Vクリア

CS/HS
≧ 大きいか等しい (符号無し)
Cセット

CC/LO
< 小さい (符号無し)
Cクリア

HI
> 大きい (符号無し)
CセットかつZクリア

LS
≦ 小さいか等しい (符号無し)
CクリアまたはZセット

GE
≧ 大きいか等しい (符号付き)
NとVが同じ

LT
< 小さい (符号付き)
NとVが異なる

GT
> 大きい (符号付き)
GEの条件かつZクリア

LE
≦ 小さいか等しい (符号付き)
LTの条件かつZクリア



演算命令

add

add r1, r2, r3 @ r1 = r2 + r3

addには3つのレジスタが指定できます。(3番目は定数なども指定できます)

ここで、r1はディスティネーションレジスタで、最終的な演算結果が代入されます。

r2は第1ソースオペランドレジスタ、r3は第2オペランドと呼びます。

前述の通り、r3は「レジスタ」、「メモリアドレス」、「定数」を指定することができます。

また以下のように、シフト演算も含めて指定することができるようです。

add r1, r2, r3 LSL #20 @ r1 = r2 + (r3 << 20)

シフト演算の命令は以下のようになります。

命令
意味

LSL
(Logical Shift Left) 左シフト

LSR
(Logical Shift Right) 右シフト

ASR
(Arithmetic Shift Right) 算術右シフト(符号を保存)

ROR
(Rotate Right) 右ローテイト

RRX
(Rotate Right with extend) 右ローテイト(含キャリーフラグ)

ループで10を加算するプログラム例

    MOV r0, #0 @ r0 = 0

Next: BL Inc @ 戻りアドレスをLRに入れ、Incに分岐
CMP r0, #10 @ r0と10を比較
BNE Next @ r0が10でなければNextへ
B End @ 無条件にEndへ分岐
Inc: ADD r0, r0, #1 @ r0 = r0 + 1
MOV PC, LR @ LRをPCに代入(つまりリターン)
End:


ロード/ストア命令

レジスタからメモリへデータを転送することをストア、メモリからレジスタへデータを転送することをロード、と呼びます。

(ちなみにレジスタからレジスタへの移動をムーブ(MOV)と呼びます)

それぞれの命令は以下のようになります。

動作
命令

レジスタ → レジスタ
MOV

メモリ→ レジスタ
LDR

レジスタ→メモリ
STR


プログラム例

こちらの記事から引用させてもらいます。


arm

#-------------------------------------

# プログラム例 list01.s
#-------------------------------------

#-------------------------------------
# text セクション
#-------------------------------------
.text
.align 2
.global _start @ 必須

/* 定数に名前をつける */

sys_write = 0x900004 @ write システムコール
sys_exit = 0x900001 @ exit システムコール

/* ここからプログラムを記述 */

_start: /* 常にこのラベルから実行開始 */

mov r3, #5 @ 5回繰り返す
1: ldr r1, msg @ 文字列先頭アドレス
mov r0, #1 @ 標準出力(fd=1)を指定
ldr r2, msg_sz @ 出力データの長さ(バイト)
swi #sys_write
subs r3, r3, #1 @ カウンタを更新
bne 1b @ 後方のローカルラベルへ

/* プログラム終了時に実行する */

mov r0, #0 @ 終了コードを0
swi #sys_exit @ プログラム終了

/* 書き換え不要な定数は text セクションに置く */

msg: .long msg0 @ 文字列格納アドレス
msg_sz: .long msg_sz0 @ 文字列の長さ格納アドレス

#-------------------------------------
# data セクション
#-------------------------------------
.data
.align 2
/* ここは初期化が必要なデータを置く *
* プログラムサイズに影響する */

msg0: .asciz "hello, world\n"

.align 2 @ 4バイト境界に設定
msg_sz0 = . - msg0 @ 文字列の長さを計算

data1: .hword 12345

#-------------------------------------
# bss セクション
#-------------------------------------
.bss
.align 2
/* ここは初期化が不要なデータを置く *
* プログラムサイズには影響しない */

data: .long 0 @ この例では使っていない
#-------------------------------------


どうやら最終行は空行が必要なようです。(ないとエラー)

ざっと見てみると、だいぶx86のアセンブラと似ている感じです。

.textセクションがいわゆるプログラムコード、.dataセクションが変数などのデータですね。

なお、.dataは、


C言語では、「0以外の初期値を持つ大域変数」「0以外の初期値を持つ静的局所変数」がここに置かれます。


ということで、初期値を持つものが置かれる領域のようです。

一方、初期値を持たないものは.bssセクションに置かれます。

また、.rodataという「定数」を置くセクションもあるようです。

msg0: .asciz "hello, world\n"

このあたりが文字列定数の宣言ですね。

続く以下のものはアドレスから文字列長を計算している?

msg_sz0 = . - msg0

.は現在のアドレスで、そこからmsg0分引くと差分は文字列長、ということだろうか?

x86のところでも似たような記述がありました。

こんな感じの。


x86

.len: equ $ - msg


$が現在のアドレスを表し、そこからmsgのアドレスを引く、という計算です。