ARM7TDMI
GBAに搭載されているCPUのARM7TDMIの命令を簡単に説明します。
ARM系CPUにはARM命令(32bit)とThumb命令(16bit)がありますが、GBAが携帯ゲーム機ということから
Thumb命令より消費電力が大きいARM命令は、ゲーム上のステータスの値やフラグの値等の処理にはほ
とんど使われません。ということでThumb命令のみ説明することにします。
説明の前に
※基本的な改造の知識を持っていることが条件です。
※今回のページを作成するに当たって、[ARM MEMO]、[リファレンスマニュアル]を参考にさせてもらいました。
※見にくいのは、ご了承ください。
※命令を説明するの際に使われているRd,Rs,RnはR0~R7まで、Hd,HsはR8~R15までの汎用レジスタが使用可能。
※$0x??は16進数、#???は10進数を表します。
※#Offset5、#Offset8は5、8ビット長のオフセットを表します。
※immは数値です(10進数&16進数両方の形式で記述可能)。今回の説明では基本的には10進数として扱っていきます。
※論理演算についてわからなかったら、他のところで勉強してください。
※ネット上にはGBA(ARM)用のアセンブラがありますので、バイナリエンコーディングは省きます。
※本来ならばもっと詳細に説明をしなければいけないと思いますが、ARM系のCPUでGBAのプログラムを組むのではなく、
あくまでも GBA上でちょっとしたプログラム改造やパラサイトルーチン等を作ることを目的としているので省きます。
※本当に適当に書いた物なので、これ以上詳しい情報が知りたければARMへ。但しリファレンスマニュアルは英語です。
ショートカット
レジスタは以下の32bitレジスタが使用できます。
R0 | 32bit汎用レジスタ |
R1 | 32bit汎用レジスタ |
R2 | 32bit汎用レジスタ |
R3 | 32bit汎用レジスタ |
R4 | 32bit汎用レジスタ |
R5 | 32bit汎用レジスタ |
R6 | 32bit汎用レジスタ |
R7 | 32bit汎用レジスタ |
R8 | 32bit汎用レジスタ |
R9 | 32bit汎用レジスタ |
R10 | 32bit汎用レジスタ |
R11 | 32bit汎用レジスタ |
R12 | 32bit汎用レジスタ |
R13(SP) | スタックポインタ |
R14(LR) | リンクレジスタ |
R15(PC) | プログラムカウンタ |
CPSR | カレントステータスレジスタ |
※灰色で示されているのがThumbステート時に通常使わないレジスタです。
※Thumbステート時にはR8~R15は四則演算等、汎用レジスタとして使うことはできないが、限定的な手段でアクセスが可能。
※他にもレジスタは存在しますが、ユーザー・レベルプログラムではこの程度しか使わないと判断しました。
スーパーバイザー、システムレベルでは他のレジスタも使えます。
※R0~R7までをLoレジスタ(下位レジスタ)、R8~R15までをHiレジスタ(上位レジスタ)と呼びます。
※R13(SP)のスタックポインタはPOP/PUSH命令で一時的に数値を格納しておくレジスタです。
※R14(LR)のリンクレジスタはBL命令等でサブルーチンに飛ぶ場合に、その次のアドレスを格納しておくレジスタです。
[0x8000000]からサブルーチンに飛ぶ場合、次の[0x8000002]を格納します。(ARM State時の場合は、[0x8000000]であれば、[0x8000004]を示します)
※R15(PC)のプログラムカウンタは、次に実行される命令のアドレスを格納しておくレジスタです。
現在実行されているのが[0x8000000]であれば、プログラムカウンタは[0x8000002]を示します。[0x8000002]だったら、[0x8000004]という感じです。
(ARM State時の場合は、[0x8000000]であれば、[0x8000004]を示します)
BL命令でサブルーチンに飛び、元のアドレスに復帰したい場合は、[MOV PC,LR]と記述してやると良いです。
N | Negative/Less Than |
Z | Zero/Equal |
C | Carry/Borrow/Extend |
V | Overflow |
I | IRQ Disable |
F | FIQ Disable |
T | THUMB State |
補足:条件フラグとはALU演算の結果でCPSR(カレントステータスレジスタの)にON(1)、OFF(0)にされるもののことです。条件命令の時に判定として使われます。
N、Z、C、Vフラグはコンディションコードに保存されます。コンディションコードとは演算の結果により変化し、その後の命令を実行するか否かを決定する
のに使用されます。Thumb命令でこれの影響を受けるのはBranch命令だけです。
I、F、Tフラグについてはプログラム改造レベルで扱うフラグではありません。ちなみにTはThumb State時にはずっと1(ON)になってます。
これらの命令はR0~R7レジスタ間でALU演算(演算結果によってCPSRに条件フラグをセットする命令)を行う命令です。
AND |
論理積 |
EOR | 排他的論理和 |
LSL | 論理左シフト |
LSR | 論理右シフト |
ASR | 算術右シフト |
ADC | キャリー付き加算 |
SBC | キャリー付き減算 |
ROR | 右ローテイト |
TST | ビットテスト |
NEG | 符号反転 |
CMP | 比較 |
CMN | 逆比較 |
ORR | 論理和 |
MUL | 積算 |
BIC | ビットクリア |
MVN | ビット反転 |
その他
ADD |
加算 |
SUB | 減算 |
補足:これらの演算は結果をCPSR(カレントステータスレジスタ)の条件フラグにセットします。
命令の詳しいフォーマットは下に記述してあります。
LSL Rd,Rs,#Offset5 |
Rsを5ビット長で表される値だけ論理左シフトしRdに格納 |
LSL Rd,Rs |
RdをRsの値の分だけ論理左シフト |
LSR Rd,Rs,#Offset5 |
Rsを5ビット長で表される値だけ論理右シフトしRdに格納 |
LSR Rd,Rs | RdをRsの値の分だけ論理右シフト |
ASR Rd,Rs,#Offset5 | Rsを5ビット長で表される値だけ算術右シフトしRdに格納 |
ASR Rd,Rs | RdをRsの値の分だけ算術右シフト |
ROR Rd,Rs | RdをRsの値の分だけ右ローテイト |
例:LSL R0,R1,#1
意味:R1の内容を1bit左に論理シフト、その結果をR0に代入。
補足:左に○bitシフトすることにより元の値が2の○乗されていく。右の場合は2の-○乗。
よくある経験値n倍、1/n倍等はこのシフト命令を用いて作られる。
ADD Rd,Rs,Rn |
RsとRnを加算しRdに格納 |
ADD Rd,Rs,#Offset3 |
Rsと3ビット長で表される値を加算しRdに格納 |
ADD Rd,#Offset8 | Rdに8ビット長で表される値を加算しRdに格納 |
ADD Rd,SP,#imm | SPに#immを足したアドレスをRdにロード |
ADD Rd,PC,#imm | PCに#immを足したアドレスをRdにロード |
ADD SP,#±imm | SPに±#imm(±508まで)を加算。但しオフセットは下位2bitが0であることが条件 |
ADC Rd,Rs | RsとRdをキャリー付き加算 |
例:ADD R0,R1,#99
意味:R0 = R1 + 99
補足:[ADD Rd,SP,#imm]と[ADD Rd,PC,#imm]は加算と言うよりSP、PCを加算して得られたアドレスをロードする命令です。
ADD命令はR8~R15の上位レジスタも使用することが可能です。
SUB Rd,Rs,Rn |
RsからRnを減算しRdに格納 |
SUB Rd,Rs,#Offset3 |
Rsから3ビット長で表される値を減算しRdに格納 |
SUB Rd,#Offset8 | Rdから8ビット長で表される値を減算しRdに格納 |
SBC Rd,Rs | RsからRdを NOT キャリー付き減算 |
例:SUB R0,$0x63
意味:R0 = R0 - 99
補足:物を買ってもお金減らない等はこの減算命令を潰すことにより可能。逆にお金増える等は加算命令に書き換えることで
可能になる。
CMP Rd,Rs | Rdの内容とRsの内容を比較 |
CMP Rd,#Offset8 | Rdの内容を8ビット長で表される値を比較 |
CMN Rd,Rs | Rdの内容とRsの内容を比較(逆比較) |
例:CMP Rd,$0x98967F
意味:Rd - 9999999
補足:CMPはSUBとまったく同じ動作をするが、違いは演算結果がレジスタに反映されない点。結果のみが条件フラグに反映される。
CMNはCMPと逆でADDと同じ働きをする。
AND Rd,Rs | RdをRsで論理積 |
ORR Rd,Rs | RdをRsで論理和 |
EOR Rd,Rs | RdをRsで排他的論理和 |
BIC Rd,Rs | RdをNOT Rsで論理和する |
MVN Rd,Rs | RdにNOT Rsした値を格納 |
TST Rd,Rd | RdをRsで論理積(ただし、比較命令と同じでレジスタに反映しない) |
あまり良い例が思いつきませんが・・・レジスタを初期化する基本的な命令
例:EOR R0,R0
意味:R0 = R0 EOR R0
排他的論理和の例:X(=11111111)とY(=11110000)とします。
11111111
EOR 11110000 ;XとYの値が違う場合は真、同じ場合は偽という動作をします。排他とは他と同じにならないという意味です。
00001111 ;論理式で書くと・・・NOT(X)・Y+X・NOT(Y)。ベン図は省略(爆)。
;上位4bit(1111+1111)は入力される数がともに1で同じことから偽となり0になります。
;下位4bit(1111+0000)は入力される数が1と0で違うので真となり1になります。
;単純な数での排他的論理和をしてみましたが、複雑な数だとどうなるか等いろいろテストしてみると
;理解が深まるかと思います。テストにはWINDOWS付属の関数電卓で十分できます。
補足:R0をR0で排他的論理和することにより0にすることができる。x86系[XOR EAX,EAX]等ではメモリの使用量が2Byteで済
むため[MOV EAX,0]の4Byteより少ないメモリの使用量で実行できる、かつ高速なことから初期化として使われることが多
いがARM系では命令長がARM(4Byte)、Thumb(2Byte)に固定されているためサイクルの違いはあれどあまり意味がないと思われる・・・(汗)。
MOV Rd,Rs | RdにRsの値を格納 |
MOV Rd,#Offset8 | 8ビット長で表される値をRdに格納 |
例:MOV R0,$0x63
意味:R0 = 99
補足:MOV命令はR8~R15の上位レジスタも使用することが可能です。
MUL Rd,Rs | RdをRsの値で積算 |
例:MUL R0,R1
意味:R0 = R0 * R1
NEG Rd,Rs | RdにRsの符号反転した値を入れる |
例:NEG Rd,Rs
意味:R0 = -R1
補足:元の値が負の場合は、反転して正になります。
STR Rd,[Rs,Rn] | RsとRnを足したメモリアドレスにRdをワードストア(4Byte) |
STRH Rd,[Rs,Rn] | RsとRnを足したメモリアドレスにRdをハーフワードストア(2Byte) |
STRB Rd,[Rs,Rn] | RsとRnを足したメモリアドレスにRdをバイトストア(1Byte) |
STR Rd,[Rs,#imm] | Rsに符号無し#imm(7bit = 124まで)を足したメモリアドレスにRdをワードストア |
STRH Rd,[Rs,#imm] | Rsに符号無し#imm(6bit = 62まで)を足したメモリアドレスにRdをハーフワードストア |
STRB Rd,[Rs,#imm] | Rsに符号無し#imm(5bit = 31まで)を足したメモリアドレスにRdをバイトストア |
STR Rd,[SP,#imm] | SPに符号無し#imm(最大1020バイト)を 足したメモリアドレスにRdをワードストア |
例:STRB R0,[R1,#20]
意味:[R1 + $0x14] = R0
補足:#immの値は命令の書き込む長さに合わせてください。STRだったら4バイトずつ(10進だと4,8,12,16…)、STRHは2バイトずつ(10進だと2,4,6,8・・・)。
LDR Rd,[Rs,Rn] | RsとRnを足したメモリアドレスの値をRdにワードロード(4Byte) |
LDRH Rd,[Rs,Rn] | RsとRnを足したメモリアドレスの値をRdにハーフワードロード(2Byte) |
LDRB Rd,[Rs,Rn] | RsとRnを足したメモリアドレスの値をRdにバイトロード(1Byte) |
LDR Rd,[Rs,#imm] | Rsに符号無し#imm(7bit = 124まで)を足したメモリアドレスの値をRdにワードロード |
LDRH Rd,[Rs,#imm] | Rsに符号無し#imm(6bit = 62まで)を足したメモリアドレスの値をRdにハーフワードロード |
LDRB Rd,[Rs,#imm] | Rsに符号無し#imm(5bit = 31まで)を足したメモリアドレスの値をRdにバイトロード |
LDSH Rd,[Rs,Rn] | RsにRnを足したメモリアドレスの値をRdにハーフワードロード、 Rdの上位16bitには15bit目の値がコピーされる |
LDSB Rd,[Rs,Rn] | RsにRnを足したメモリアドレスの値をRdにバイトロード、 Rdの上位24bitには7bit目の値がコピーされる |
LDR Rd,[SP,#imm] | SPに符号無し#imm(1020バイトまで)を足したメモリアドレスの値にRdをワードロード |
LDR Rd,[PC,#imm] | PCに符号無し#imm(1020バイトまで)を足したメモリアドレスの値をRdにワードロード |
例:LDRB R0,[R1,#20]
意味:R0 = [R1 + $0x14]
補足:ストアと同じく#immの値の設定には気をつけてください。
PUSH {Rlist} | レジスタリストRlistにあるレジスタをスタックにPUSHし、 スタックポインタを更新する |
PUSH {Rlist,LR} | レジスタリストRlistにあるレジスタとリンクレジスタを スタックにPUSHし、スタックポインタを更新する |
POP {Rlist} | スタックからレジスタリストRlistにあるレジスタをPOPし、 スタックポインタを更新する |
POP {Rlist,PC} | スタックからレジスタリストRlistにあるレジスタとPCを POPし、スタックポインタを更新する. |
例:PUSH {R0-R7,LR}
意味:R0~R7、リンクレジスタをスタックに一時保存し、スタックポインタを更新。
補足:上記の例はサブルーチンの手前で実行するとリターンアドレスとその時のレジスタの状態を保存できる
STMIA Rd!,{Rlist} | レジスタリストRlistにあるレジスタを、ベースレジスタRdで示されるメモリアドレスにワードストアし、Rdを更新する |
LDMIA Rd!,{Rlist} | ベースレジスタRdで示されるメモリアドレスから レジスタリストRlistにあるレジスタをワードロードし、 Rdを更新する |
例:STMIA R0!,{R1-R7}
意味:R1~R7をR0で示されるメモリアドレスに、R0を順にインクリメントしながらワードストアし1回ごとにR0にはインクリメントした値が書き戻される
別解:上の例はこのような動作を1命令分で行います。
STR R1,[R0]
ADD R0,#4
STR R2,[R0]
ADD R0,#4
・
・
・
STR R7,[R0]
ADD R0,#4
例:LDMIA R0!,{R1-R7}
意味:R1~R7にR0で示されるメモリアドレスからR0を順にインクリメントしながらワードロードし1回ごとにR0にはインクリメントした値が書き戻される
別解:上の例はこのような動作を1命令分で行います。
LDR R1,[R0]
ADD R0,#4
LDR R2,[R0]
ADD R0,#4
・
・
・
LDR R7,[R0]
ADD R0,#4
補足:インクリメントと聞くとプログラムやってる人は+1と想像してしまうでしょうが、レジスタが32bit(4Byte)なので、実際は4Byte
ずつ足されていきます。C/C++をやっている人は、アドレスを保持しているポインタ変数をインクリメントしていると考えてもらって結構です。
データの並びによって使う場面が限定されそうですが、アイテムの個数を全て最大にする等の処理ができそうです。
また、LDMIAを実行した後、ベースレジスタの値を変更してSTMIAを実行すると、メモリ間の転送(コピー)が可能です。
NOP (MOV R8,R8) | 何も変化しない命令を実行する |
例:MOV R8,R8
意味:ノーオペレーション(NOP)
補足:ARM系プロセッサでは上記の例がNOPとして扱われます。重要なのは無実行ではなくちゃんと命令を実行しているのでサイクルがあります。
これがリファレンスのNOP命令なので処理を潰す場合はなるべくこの形を取ってください。
ARMではMOV命令で代用してますが、PS2ではZEROレジスタを0回シフトを実行していますね。
注意点としてはNOPが連続で並んでいる場所などは命令の処理待ちの可能性があることです。その様な場合はむやみに変更す
ると予期せぬ結果が得られることがあります。
BEQ [address](label) | 等しい | 比較結果が等しいかゼロ | Z = 1 |
BNE [address](label) | 等しくない | 比較結果が等しくないかゼロでない | Z = 0 |
BCS [address](label) | キャリー・セット | 算術演算の結果が桁上がりする | C = 1 |
BCC [address](label) | キャリー・クリア | 算術演算の結果が桁上がりしない | C = 0 |
BMI [address](label) | マイナス | 結果が負または否定 | N = 1 |
BPL [address](label) | プラス | 結果が正またはゼロ | N = 0 |
BVS [address](label) | オーバーフローのセット | 符号付き整数演算でオーバーフローが起きる | V = 1 |
BVC [address](label) | オーバーフローのクリア | 符号付き整数演算でオーバーフローがない | V = 0 |
BHI [address](label) | 大きい | 符号無し比較で大きい | C = 1 , Z = 0 |
BLS [address](label) | 小さいか同じ | 符号無し比較で小さいか等しい | C = 0 , Z = 1 |
BGE [address](label) | 大きいか等しい | 符号付き整数演算でより大きいか等しい | N = V |
BLT [address](label) | 小さい | 符号付き整数演算でより小さい | N != V |
BGT [address](label) | 大きい | 符号付き整数演算でより大きい | Z = 0 and N = V |
BLE [address](label) | 小さいか等しい | 符号付き整数演算でより小さいか等しい | Z = 1 or N != V |
補足:まず補足。Z,C,N,V等は条件フラグと呼ばれるもので、これが0(OFF)か1(ON)かを判別して条件分岐しています。
上記の条件フラグはThumb命令において殆どの命令がこの条件フラグの状態を変化させています。ARM命令では
全ての命令に対して条件(EQ、NE等)をつけられますが、Thumb命令ではB(Branch)命令にしか付加できないので
この命令しか条件フラグの影響は受けないと考えてもらって結構です。因みにこの命令はよくCMP命令とペアで使わ
れることが多いです。では下に例を示します。
例: CMP R0,#99
BLT JUMP
MOV R1,#99
STRB R1,[R2]
JUMP
ADD R1,#1
STRB R1,[R2]
意味:1行目・・・R0-99
2行目・・・比較の結果でR0が99より小さい場合はラベルJUMPに分岐する
3行目・・・R1に99を格納する
4行目・・・R2で示されるメモリアドレスにR1の値をバイトストアする
5行目・・・ラベル
6行目・・・R1に1を足す
7行目・・・4行目と同じ
補足:ほとんどの場合がCMPとペアなのでCMPの引き算の結果が等しいだったらBEQ、等しくなかったらBNE等と考えてください。
B [address](label) | [PC±2KB] に強制分岐。今実行してるプログラムのアドレスから±2048Byteまで |
BL [address](label) | [PC±4MB]に強制分岐。今実行してるプログラムのアドレスから±409600Byteまで |
BX Rd(Hs) | R0~R15が示すアドレスに分岐、同時にステート変更(第0bitが0の場合はARMステート、1の場合はThumbステートに移行) |
例:B(L) JUMP
意味:ラベルJUMPに強制分岐
補足:BL命令(Long Branch)だけはThumb命令の中で唯一、1命令で2命令分(4Byte)使います。よくWRAMやIRAMに飛ばして処理しようと
研究してる人がいますが、距離が足りないので意味がないです。(私も一時期、研究しましたがとてもじゃないですけど実用的ではないです)
BX命令はARM命令とThumb命令の切り替えを行う命令です。まず使わないと思ってください。使えば、16bitから
32bitに広がるので扱える数の制約が緩んでより自由なプログラムが組めますが、改造コードがものすごく長くなります・・・(汗)。
ソフトウェア割り込み(Software Interrupt)
SWI #imm | 次の命令のアドレスをLRにコピー、CPSRの内容を SPSR_svcにコピー、SWIベクタアドレスをPCにコピーして、ARMステートに切り替え、SVCモードに入る |
例:SWI 10
意味:ソフトウェア割り込み例外を発生させる。処理番号10をSWIハンドラに渡す。
補足:BIOSのファンクションコールなので使いどころがないです。GBAでプログラム改造する場合、まず使わない命令です(笑)。
BKPT #imm | ブレークポイントを設置する。#immの値は(8bit=255)まで |
例:BKPT #1
意味:ブレークポイントを設置。設置番号は1
補足:この命令もまず使わないです。デバッガ側が勝手に設定してくれます。デバッガ側の動作を見てみるとどうやら#immの値は
設置番号のよう。1番目のブレークポイントには1が2番目には2という感じ。この命令をCPUに対して実行できるということは、
GBAのCPUはARMアーキテクチャversion5Tを実装してる模様。そうすると無条件分岐命令の[BLX]も使えそうな予感だが・・・。
条件ループの例を簡単に説明します。この時、[R2=0x2000000]とします。このループはメモリアドレス0x2000000から+2hずつアドレスを
足していきながらR1とR3の値を書き込みをするプログラムです。結果として0163、0263、0363、0463、0563・・・FF63まで書き込みを繰り返します。
このようなループはアイテム番号と個数が連続して並んでいるデータ等に対して有用なループだと思われます。GBAではありませんが実際ファイナ
ルファンタジー4はこの形式でした。しかしPARやX-TAには連続書き込みコードがあるので、ほとんど使わないかも(汗)。
CMP命令を使う条件ループの例:
MOV R0,#255 ;R0 = 255 (カウント用の値255を格納)
MOV R1,#1 ;R1 = 1 (アイテム番号の初期値1を格納)
MOV R3,#99 ;R3 = 99 (アイテムの最大個数99を格納)
LOOP
STRB R1,[R2] ;[R2] = R1 (R2の値が示すメモリアドレスにR1の値をバイトストア)
STRB R3,[R2,#1] ;[R2 + 1] = R3 (R2に1を足した値が示すメモリアドレスにR3の値をバイトストア)
ADD R2,#2 ;R2 = R2 + 2 (R2に2足す)
ADD R1,#1 ;R1 = R1 + 1 (R1に1足す)
SUB R0,#1 ;R0 = R0 - 1 (R0から1引く)
CMP R0,#0 ;R0 = R0 - 0 (比較演算する結果はCPSRへ格納される)
BNE LOOP ;(R0 - 0が等しくない (R0 > 0 or R0 < 0) 場合LOOPに分岐)
;(逆にR0 = 0であったなら分岐せずに素通り)
CMPを使わない条件ループの例:
MOV R0,#255 ;R0 = 255 (カウント用の値255を格納)
MOV R1,#1 ;R1 = 1 (アイテム番号の初期値1を格納)
MOV R3,#99 ;R3 = 99 (アイテムの最大個数99を格納)
LOOP
STRB R1,[R2] ;[R2] = R1 (R2の値が示すメモリアドレスにR1の値をバイトストア)
STRB R3,[R2,#1] ;[R2 + 1] = R3 (R2に1を足した値が示すメモリアドレスにR3の値をバイトストア)
ADD R2,#2 ;R2 = R2 + 2 (R2に2足す)
ADD R1,#1 ;R1 = R1 + 1 (R1に1足す)
SUB R0,#1 ;R0 = R0 - 1 (R0から1引く。この時、演算結果がCPSRの 条件フラグにセット される)
BGT LOOP ;( (R0 = R0 - 1) が 0以上 ならLOOPに分岐する)
;(符号付き整数演算の結果が 0より小さいなら 分岐せずに素通り)
補足:まぁ1行減っただけですが、CMP使わないほうが1命令分省けます。慣れてる方はこっちの方がいいかもしれません。
PAR用に直すと1行分省けますからね。ちなみに書いてませんがADD命令もCPSRに条件フラグをセットしています。