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へ。但しリファレンスマニュアルは英語です。


ショートカット

レジスタ

条件フラグ

条件フラグをセットする命令

シフト/ローテイト命令

加算命令

減算命令

比較命令

論理演算命令

格納命令

積算命令

符号反転命令

ストア命令

ロード命令

PUSH/POP命令

複数のレジスタのロード、ストア命令

NOP命令

条件分岐命令

無条件分岐命令

ソフトウェア割り込み命令

ブレークポイント命令

条件ループの例


レジスタ

レジスタは以下の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(カレントステータスレジスタ)の条件フラグにセットします。

    命令の詳しいフォーマットは下に記述してあります。


シフト命令(Shift/Rotate)

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)

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の上位レジスタも使用することが可能です。


減算命令(Subtract)

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

補足:物を買ってもお金減らない等はこの減算命令を潰すことにより可能。逆にお金増える等は加算命令に書き換えることで

    可能になる。


比較命令(Compare)

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と同じ働きをする。


論理演算(Logical)

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)に固定されているためサイクルの違いはあれどあまり意味がないと思われる・・・(汗)。


格納命令(Move)

MOV Rd,Rs RdにRsの値を格納
MOV Rd,#Offset8 8ビット長で表される値をRdに格納

例:MOV R0,$0x63

意味:R0 = 99

補足:MOV命令はR8~R15の上位レジスタも使用することが可能です。


積算命令(Multiply)

MUL Rd,Rs RdをRsの値で積算

例:MUL R0,R1

意味:R0 = R0 * R1


符号反転命令(Negate)

NEG Rd,Rs RdにRsの符号反転した値を入れる

例:NEG Rd,Rs

意味:R0 = -R1

補足:元の値が負の場合は、反転して正になります。


書き込み(ストア)命令(Store)

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・・・)。


読み込み(ロード)命令(Load)

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/POP命令

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を実行すると、メモリ間の転送(コピー)が可能です。


ノーオペレーション命令(No Operation)

NOP (MOV R8,R8) 何も変化しない命令を実行する

例:MOV R8,R8

意味:ノーオペレーション(NOP)

補足:ARM系プロセッサでは上記の例がNOPとして扱われます。重要なのは無実行ではなくちゃんと命令を実行しているのでサイクルがあります。

    これがリファレンスのNOP命令なので処理を潰す場合はなるべくこの形を取ってください。

    ARMではMOV命令で代用してますが、PS2ではZEROレジスタを0回シフトを実行していますね。

    注意点としてはNOPが連続で並んでいる場所などは命令の処理待ちの可能性があることです。その様な場合はむやみに変更す

    ると予期せぬ結果が得られることがあります。


条件分岐命令(Branch)

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等と考えてください。


無条件分岐命令(Branch)

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でプログラム改造する場合、まず使わない命令です(笑)。


ブレークポイント(Breakpoint)

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に条件フラグをセットしています。