前回はArmの32ビットと64ビットのモードと命令セット(機械語命令の幅は、どちらも32ビット)の件でした。Arm(腕)には、Thumb(親指)あり、今回は、32ビットモードのArmプロセッサの多くが備えるThumbという名の16ビット幅の機械語命令セットにどうやって切り替えるのか、というところを実際に動かして確認してみたいと思います。
※「ぐだぐだ低レベルプログラミング」投稿順indexはこちら
さて、「またもや」というべきなのでしょうが、Thumb命令セットについても歴史があり、いくつかその意味する範囲が異なる Thumb が存在します。少なくとも、以下の3つを識別しておくべきかと思います。
- Thumb
- Thumb 2
- Arm
最後のArmは、Armの「根本を成す筈」の32ビット幅の命令セットです。32ビット幅16本の汎用レジスタと使うことができるもの。それに対して、最初のThumbは、ARM7TDMIの時代に導入された16ビット幅の命令セット(データは32ビット幅を扱える)であり、命令幅を狭くした分、レジスタ16本を自由に使えるというわけにはいかず、最初の8本は自由に、その他は「不自由に」使えるものです。当時を考えると、メモリへのバス幅が狭かったり、使えるメモリ容量が小さかったりしたために、32ビット幅のゴージャスな命令セットでなく、Thumbのような命令を使ってプログラム容量を小さく、狭いバスでもフェッチしやすくしたんだと思います。ArmとThumbの切り替えは今回ちょっとやってみますが、ユーザモードの中で可能な「モード切替」が必要です。
これに対してThumb 2は、16bit幅のThumb命令2個分を使って32ビット1命令分をコードするような拡張をThumb命令に追加したものです。Thumb命令セットが拡張されているので、モードの切り替えなど不要。Thumbの16ビット幅命令と「Thumbの32ビット幅命令」を混在可能。ある意味、ピュアなThumbでは、どうしてもArmコードに戻って処理したくなるような処理をThumbの中にいたままで出来るようにした拡張と言えます。おかげで、最近の組み込み系Armプロセッサでは、基本である筈の32ビット幅のArm命令セットを持たず、Thumb 2命令のみを実装したタイプが主流です。それに対して、ArmとThumb(Thumb 2)の両方を実装した機種では、ArmとThumbモード間の遷移のために「モード切替」を必要とします。ラズパイはこちら。
なお、Thumb 2といっても「ピンからキリ」か「松竹梅」か、どれだけの命令が拡張してあるのかは様々なようです。ざっと見たところでは、v7系のアーキテクチャになると、Arm命令など無くてもいいくらい拡張されているThumb 2になるようです。それ以前だと、Thumb 2といっても必要最小限の Thumb 2命令のみ実装されている感じ。
さらにまた、
ThumbEE
というThumbも存在します。基本 Thumb 2のさらなる拡張で動的なコンパイレーション向け。当然フラッシュにコードを焼いて固定してしまうようなCortex-M系には無縁な筈。ThumbといってもCortex-AかR系向けだと思います。
前置きが随分長くなりましたが、実際に Thumb へ 遷移するコード例を書いてみます。ベースにしたのは、前々回、Raspberry Pi 3上でCのメインルーチンからアセンブラのサブルーチンを呼び出すときに使ったもの。単に定数ロードするだけのサンプル。これのアセンブラの部分のみを書き換えました。
前々回は、サブルーチンの中で ldrして値5をロードしておしまいにしていましたが、今回は、Thumbコードのサブルーチンを呼び出し、その中でldrしています。呼び出し先の関数
get5_thumb
の前に、疑似命令が2つ。.thumbは以降、thumbコードを生成せよ、という指令で、.thumb_funcは次のラベル(関数)がthumbで書かれた関数だという宣言です。通常、ArmコードからThumbコードへ遷移するには、
飛び先のThumbコードのアドレス+1番地
へ分岐するのです。元々Thumbコードは16ビットアライメントなので、偶数番地に置かれ、アドレスの最下位は0の筈.(Armコードは32ビットアライメントなのでやはり最下位は0に変わりない。)この「使われていないビット」を立ててジャンプすることでThumbに遷移するのです。個人的には、合理的だが、普通のアセンブラからしたらちょっと気持ち悪い書き方だと思いました。間接分岐などする場合には最下位ビットに1をたてるという操作が必須になりますが、ラベルをコールする場合は、
blx命令
が使えます。これを使えば、ラベルに+1するような気持ちの悪い書き方をせずに普通に関数コール(リンクレジスタに戻り番地をストアした上で分岐)することで、Thumbに遷移することができます。また、
bx lr
すればArmモードに復帰して戻れます。
実際に上のコードをアセンブルしたときに生成される命令コードをobjdumpで調べてみると、32ビット幅の命令から、16ビット幅の命令に変化していることが分かります。
00010450 <get5>: 10450: e52de004 push {lr} ; (str lr, [sp, #-4]!) 10454: fa000001 blx 10460 <get5_thumb> 10458: e49de004 pop {lr} ; (ldr lr, [sp], #4) 1045c: e12fff1e bx lr 00010460 <get5_thumb>: 10460: 46c0 nop ; (mov r8, r8) 10462: 4801 ldr r0, [pc, #4] ; (10468 <get5_thumb+0x8>) 10464: 46c0 nop ; (mov r8, r8) 10466: 4770 bx lr 10468: 00000005 .word 0x00000005
ここでポイントは、前々回も今回も、戻り値レジスタであるr0に5を立てるのにアセンブラ表記上は以下の、
ldr r0, =5
と同じ表現を書いているのですが、実際に生成されたオブジェクトコードを見ると、今回はPC相対でメモリから値を呼び出す命令が生成されています。
ldr r0, [pc, #4]
ところが、前々回のように Arm コードとして同じアセンブラ命令をアセンブルさせると、生成されるのは以下のようなmov命令でした。
mov r0, #5
Arm命令とThumb命令(昔風の)の差が見えています。
また、このコードをgdb使って 機械語命令レベルで step実行してみると
(gdb) stepi get5 () at sample001.s:6 6 push { lr } (gdb) stepi 7 blx get5_thumb (gdb) stepi get5_thumb () at sample001.s:13 13 nop (gdb) info reg r0 0x1 1 r1 0x7efff5a4 2130703780 r2 0x7efff5ac 2130703788 r3 0x10410 66576 r4 0x0 0 r5 0x1046c 66668 r6 0x10320 66336 r7 0x0 0 r8 0x0 0 r9 0x0 0 r10 0x76fff000 1996484608 r11 0x7efff454 2130703444 r12 0x7efff4d0 2130703568 sp 0x7efff444 0x7efff444 lr 0x10458 66648 pc 0x10460 0x10460 <get5_thumb> cpsr 0x60000030 1610612784 fpscr 0x0 0 (gdb) stepi 14 ldr r0, =5 (gdb) stepi 15 nop (gdb) info reg $r0 r0 0x5 5 (gdb) stepi 16 bx lr (gdb) stepi get5 () at sample001.s:8 8 pop { lr } (gdb) info reg cpsr cpsr 0x60000010 1610612752
ここでのポイントは、cpsr(ステータスレジスタ)の値です。Thumbコードの中にいるときは、
0x60000030
と、ビット5に1が立っていますが、Thumbコードから戻ると
0x60000010
と、ビット5が0になっています。ビット5はThumbか否かを示すTビットなので、ここ見ればThumbコードを実行中であることが分かるわけです。