ぐだぐだ低レベルプログラミング(4) ARM, THUMB切り替え

JosephHalfmoon

前回はArmの32ビットと64ビットのモードと命令セット(機械語命令の幅は、どちらも32ビット)の件でした。Arm(腕)には、Thumb(親指)あり、今回は、32ビットモードのArmプロセッサの多くが備えるThumbという名の16ビット幅の機械語命令セットにどうやって切り替えるのか、というところを実際に動かして確認してみたいと思います。

さて、「またもや」というべきなのでしょうが、Thumb命令セットについても歴史があり、いくつかその意味する範囲が異なる Thumb が存在します。少なくとも、以下の3つを識別しておくべきかと思います。

  1. Thumb
  2. Thumb 2
  3. 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コードを実行中であることが分かるわけです。

ぐだぐだ低レベルプログラミング(3) Arm 32ビット、64ビットへ戻る

ぐだぐだ低レベルプログラミング(5) Arm 組み込みアセンブラとインラインアセンブラ へ進む