ARMアセンブリについてのメモ
ARM EABI(armel)についてのメモ。
ARMレジスタ
r0からr15までのレジスタとcpsrレジスタがあり、r11はフレームポインタ(fp)、r12はプロシージャ内呼び出しスクラッチレジスタ(ip; intra-procedure call scratch register)、r13はスタックレジスタ(sp)、r14はリンクレジスタ(lr)、r15はプログラムカウンタ(pc)として使われる。 cpsrレジスタは主にフラグレジスタの役割を持つ。 x86との比較でいえば、fpはebp、spはesp、lrはリターンアドレス、pcはeip、cpsrはeflagsにそれぞれ対応する。 ただし、 pcは演算の際、現在実行している命令のアドレス+8として評価される ことに注意。 また、objdumpコマンドによるディスアセンブル結果ではfp/ip、gdbのディスアセンブル結果ではr11/r12と、表示のされ方が異なることに注意。
ipレジスタの主な用途として、link時に解決される関数アドレスの呼び出しがある。 具体的には、ipレジスタを利用してGOTのアドレスを計算し、そのアドレスの間接参照によりライブラリ関数の呼び出しを行う箇所が挙げられる。
次に述べるThumbステートでは、大半の命令がr0からr7にしかアクセスできない。 r0からr7はLoレジスタ、r8からr15はHiレジスタと呼ばれる。 gccではThumbステートにおけるフレームポインタとして、fp(r11)の代わりにr7が用いられる。
ARM/Thumbステートとcpsrレジスタ
命令セットとして4バイト固定長のARMステートと基本2バイト固定長のThumbステートがある。
これらは実行中にbx命令などにより切り替えられる。
cpsrレジスタの下位5ビット目が1であればThumbステート、0であればARMステートを表す。
言い換えれば、cpsr & 0x20 > 0ならThumbステート、0ならARMステートとなる。
幅指定子
Thumbステート中における4バイト命令にはMOV.Wのように.Wがつく。
2バイト命令の場合はMOV.Nのように.Nがつけられるが、Thumbステート中では基本的に2バイト命令となる。
分岐命令
B命令がx86におけるjmp命令、BL命令がx86におけるcall命令に対応する。 BL命令では、ジャンプと同時にBL命令の次のアドレス(要するにリターンアドレス)がlrレジスタにセットされる。
また、BX、BLX命令は合わせてARM/Thumbステートの切り替えも行う。 ジャンプ先アドレスの最下位ビットが1の場合はThumbステート、0の場合はARMステートに切り替わる。
呼び出し規約
r0~r3レジスタに第1~第4引数がセットされ、r0レジスタに戻り値がセットされる。 第5引数以降にはスタックが利用される。
条件実行
命令の後ろにEQなどをつけることで、条件を満たすときだけ実行できる。
EQ/NE/LT/GT/LE/GEあたりはx86とほぼ同じ。
x86におけるjeは、ARMではB命令と組み合わせてBEQとなる。
即値
#42、#0xDEADBEEFのように#をつけて書く。
フレキシブル第2オペランド
多くの算術・比較命令(正確には汎用データ処理命令)はADD r0, r1, r2のようにオペランドを取り、r0 <- r1 + r2のような意味になる。
ここで、r2の箇所にはr2 LSL 2のような指定を行うこともでき、この場合はr2の値を2ビット論理左シフトした値、すなわちr2 * 4となる。
x86におけるlea eax, [ebx+ecx*4]のような計算に近いが、ARMの場合は論理左シフトのほかに算術右シフト(ASR)、論理右シフト(LSR)、右ローテート(ROR)もできる。
ビット演算
x86におけるand/or/xorは、ARMではAND/ORR/EORとなり微妙に異なる。
また、x86における否定not raxは、ARMではMVN r0, r0のようにmove notとして表される。
BIC r0, r1, r2はr0 <- r1 & ~r2、すなわちビットクリアを行う。
たとえば、BIC r0, r1, #0xFFはr1の下位8ビットを0にした値をr0に代入する。
逆減算、積和演算
RSB r0, r1, r2でr0 <- r2 - r1が計算できる。
SUB命令では第一オペランドに即値を取れないが、RSB命令を使えば即値からレジスタの値を減算することができる。
また、MLA r0, r1, r2, r3でr0 <- r1 + r2*r3が計算できる。
MOV/MOVT命令とMOV32疑似命令
レジスタからレジスタへの値の代入はMOV r0, r1のように書く。この場合はr0 <- r1となる。
一方、レジスタに32ビット即値を代入する場合は、下位16ビットをMOV、上位16ビットをMOVTといったように二つの命令に分けて代入する必要がある。
MOV32疑似命令を使うと、指定した32ビット即値をアセンブル時にMOVとMOVT命令に置き換えてくれる。
ADR命令
ラベルの位置のアドレスをレジスタにセットするにはADR r0, labelのように書く。
こう書くことで、アセンブル時にADD r0, [pc, #20]のような加算命令に置き換えられる。
LDR/STR命令
指定したアドレスにあるデータの内容をレジスタに読み込むにはLDR r0, [r1]のように書く。この場合はr0 <- [r1]、すなわちr1のアドレスにある4バイトをr0に読み込む。
逆に、レジスタの値を指定したアドレスに書き込むにはSTR r0, [r1]のように書く。この場合はr0 -> [r1]、すなわちr0の値をr1のアドレスに書き込む。
STRの場合矢印の向きが逆になることに注意。
また、LDR r0, [r1, #4]のように書くことでr1にオフセットを加えたアドレスを指定することができる。この場合はr0 <- [r1+4]となる。
リテラルプール
LDR命令はLDR r0, =0xBEEFのように書くことで定数の代入にも使える。
また、ラベルを用いてLDR r0, =labelのように書くことでラベルの位置のアドレスを代入することもできる。
このとき、代入される値はアセンブル時にプログラム末尾にデータとして追記され、このデータを用いたLDR命令に書き換えられる。
このデータ部はリテラルプールと呼ばれる。
多重レジスタロード/ストア
LDM/STM命令を使うと、あるアドレスを起点としたワード列を複数のレジスタに読み込み/書き込みできる。
たとえば、LDM r7, {r0-r3}と書くと、r7レジスタに入っているアドレスを起点として、4ワードがr0からr3レジスタの順に読み込まれる。
逆に、STM r7, {r0-r3}と書くと、r7レジスタに入っているアドレスを起点として、r0からr3レジスタの順に4ワード値が書き込まれる。
LDR/STRの場合と異なり、起点とするアドレスを先に書くことに注意。
アドレッシングモード
LDM/STM命令では、起点とするアドレスからアドレスを増やしていくか減らしていくか、また、アドレスを処理前に変えるか処理後に変えるかも選ぶことができる。
たとえば、LDMIAはincrement after、すなわちロード後にアドレスが1ワード分ずつ増えていく。
一方、STMDBはdecrement before、すなわちストア前にアドレスが1ワード分ずつ減っていく。
なお、単にLDM/STMと書いた場合はLDMIA/STMIAとなる。
IA/IB/DA/DBのほかに、スタックの動きをもとにした指定方法もある。
ロードするたびにアドレスが増えていくタイプのスタックを「下降」、減っていくタイプを「上昇」と表現する。
また、スタックレジスタが指すワードからロードするものを「フル」、スタックレジスタが指すアドレスの次のワードからロードするものを「空き」と表現する。
これらの表現をもとに、スタックの動きとして「フル下降」「フル上昇」「空き下降」「空き上昇」の4種類が指定できる。
それぞれLDM/STMの末尾にFD/FA/ED/EAをつけることで、これらの動きに合わせたIA/DBなどの代わりとできる。
これはスタック指向接尾文字と呼ばれる。
ライトバック
LDM r7!, {r0-r3}のように起点となるレジスタに!をつけると、命令実行後にr7の値が4ワード分増えた値に更新される。
なお、ThumbステートではライトバックつきのLDM/STM命令しか使用できない。
PUSH/POP命令
PUSH/POP命令は、起点となるレジスタをsp!とした場合のSTMDB/LDRIA命令に相当する。
LDR/STM命令同様、PUSH {r0, r1, r2}のように書くこともできるが、必ず番号の小さいレジスタがスタックの上に来るようにpushされる。
popの場合も番号の小さいレジスタから順にpopされる。
すなわち、一つの命令でr2、r1、r0の順にpush/popすることはできず、このような場合には複数の命令に分けて書く必要がある。
SVC命令
システムコール実行にはSVC命令を使う。
SVC命令はSVC 0のようにオペランドを一つ取るが、この値は基本的に無視される。
なお、Linuxではシステムコール番号にr7レジスタ、引数にr0~r6レジスタを使い、戻り値はr0レジスタにセットされる。