日記からの寄せ集めです. ARM Thumb 命令セット †ARM の Thumb 命令セットの特徴をざっくり書くと
といったところかな. プログラムサイズが小さくなる †命令長が短いことから,高密度のコードを生成できることが期待されるのだけれど,命令長が短いがために 1 命令あたりでできることは少なくなります. ARM 社の説明によると,それでもトータルで 30% 程度コード量が小さくなるそうです. 実行速度は遅くなる †Thumb 命令は CPU 内部では ARM 命令に展開されて実行されるそうです. 1 命令でできることは ARM 命令よりも少ないので,プログラムの速度は ARM 命令よりも遅くなります. ということで †ARM Linux 上で Thumb 命令はどれだけ使えるのか,というあたりをつついていこうかと思ってます. どこまで突っ込んで見れるかはわからないけど. (追記) †そういえば,Thumb-2 命令セット,なんてのもあるけど,今回はこっちの話は無し. だって実行環境持ってないんだもん. ARM Thumb 命令セット 文献 †ざっくりと挙げておきます. ARM Architecture Reference Manual †ARM 社が出している ARMv5 アーキテクチャのリファレンスマニュアルです. 改訂 ARMプロセッサ―32ビットRISCのシステム・アーキテクチャ (Design Wave BOOKS) †いきなり英語はつらい方は,こちらで予習してから臨んだほうがよいでしょう. 大学の講義を前提にして書かれた本のようで,「ARM を例に CPU アーキテクチャの勉強をする」というスタンスで書かれています. 「そんなのはもう知ってるから,オレは ARM アーキテクチャについて知りたいんだ」という場合には正直うざいところがあります. gcc のマニュアル †わざわざ web で読まなくても,環境によっては info コマンドで読める場合があります. ARM Thumb 命令セット 状態遷移 †もう少し復習めいた話が続きます. 32bit ARM 命令のモードと Thumb 命令のモード間の遷移は下図のようになります. ARM --(BX)--> Thumb <--(BX)-- <--(interrupt/exception)-- BX 命令でブランチすることにより,プログラム中で明示的に命令モードを変更することができます. 暗黙の遷移として,割り込みや例外などが発生した場合,Thumb モードから強制的に ARM モードへと切り替わります. この「割り込み」には「ソフトウェア割り込み」も含まれます. つまり,Linux 上でのシステムコールでもこの遷移が発生することになります. ARM Thumb 命令セット Linux カーネル †Thumb 命令セットの Linux カーネル側の対応について. Linux カーネル自身が Thumb コードでコンパイルされるわけではなく,Thumb ユーザプログラムが動くようにするための話です. menuconfig 設定項目 †CONFIG_ARM_THUMB という項目があります. この項目の help を見ると Say Y if you want to include kernel support for running user space Thumb binaries. The Thumb instruction set is a compressed form of the standard ARM instruction set resulting in smaller binaries at the expense of slightly less efficient code. If you don't know what this all is, saying Y is a safe choice. とあります. 実際のコード †CONFIG_ARM_THUMB で grep かけてみると,このマクロの影響する部分はちょろちょろとあります. 例えば linux/arch/arm/kernel/entry-armv.S の中です. #ifdef CONFIG_ARM_THUMB bx \reg #else mov pc, \reg #endif カーネルモードからユーザモードにジャンプするコードです. (マクロ定義の中なので,レジスタ名の部分が \reg になっちゃってます) bx 命令は Thumb 命令セットを持つ CPU でしか使えないので,こういうことになってるのでしょう. カーネル空間では ARM 命令セットで動作しているのでユーザ空間に戻るときはモードを元に戻す必要があり,ここで bx 命令が使われるわけです. ARM Thumb 命令セット gcc サポート †gcc 上での Thumb 命令サポートについて. コンパイルオプション †gcc のドキュメントから Thumb 命令に関するオプションを拾ってみると
というあたりかな. ざっくり分類すると,こんなところかな.
Thumb Interwork †-mthumb-interwork の説明によると Generate code which supports calling between the ARM and Thumb instruction sets. Without this option the two instruction sets cannot be reliably used inside one program. The default is `-mno-thumb-interwork', since slightly larger code is generated when `-mthumb-interwork' is specified. ARM 命令セットと Thumb 命令セットをチャンポンに使ってプログラムをリンクできる枠組みのようです. Super Interworking †-mcallee-super-interworking の説明によると Gives all externally visible functions in the file being compiled an ARM instruction set header which switches to Thumb mode before executing the rest of the function. This allows these functions to be called from non-interworking code. 上記の Thumb Interwork の枠組みを使わずに呼び出し元 or 呼び出し先でモード切り替えを行うことで,非 Thumb Interwork コードとインターフェースしよう,ということのようですね. ARM Thumb 命令セット Thumb Interwork †gcc での Thumb 命令サポートでの Thumb Interwork とよばれてるものについて. 環境 †今回は old ABI の環境でいじりました.
ソース †例としてこんなソースを用意します.
コンパイルすることを考えてます. 関数の呼び出しはこんなところ. ARM → Thumb と Thumb → ARM の両パターンが入るようにしています. main -+-> arm_caller -> thumb_callee |-> thumb_caller -> arm_callee arm.c extern int thumb_callee ( int ); int arm_caller ( int a ) { return a + thumb_callee ( a ); } int arm_callee ( int a ) { return 1 + a; } thumb.c extern int arm_callee ( int ); int thumb_caller ( int a ) { return a + arm_callee ( a ); } int thumb_callee ( int a ) { return 1 + a; } main.c #include <stdio.h> extern int arm_caller ( int ); extern int thumb_caller ( int ); int main ( void ) { printf ( "%d %d\n", arm_caller(1), thumb_caller(1) ); return 0; } コンパイル †とりあえずコンパイルしてみます.-mthumb-interwork というオプションで Thumb Interwork を使うことを指示しています. $ gcc -mthumb-interwork main.c -c $ gcc -mthumb-interwork arm.c -c $ gcc -mthumb-interwork -mthumb thumb.c -c ELF ヘッダ †生成されたオブジェクトファイルの ELF ヘッダを見てみます. $ readelf -h arm.o ELF ヘッダ: マジック: 7f 45 4c 46 01 01 01 61 00 00 00 00 00 00 00 00 クラス: ELF32 データ: 2 の補数、リトルエンディアン バージョン: 1 (current) OS/ABI: ARM ABI バージョン: 0 タイプ: REL (再配置可能ファイル) マシン: ARM バージョン: 0x1 エントリポイントアドレス: 0x0 プログラムの開始ヘッダ: 0 (バイト) セクションヘッダ始点: 268 (バイト) フラグ: 0x4, GNU EABI, interworking enabled このヘッダのサイズ: 52 (バイト) プログラムヘッダサイズ: 0 (バイト) プログラムヘッダ数: 0 セクションヘッダ: 40 (バイト) Number of section headers: 9 Section header string table index: 6 「フラグ」のところに interworking enabled というフラグが立っており,通常の ARM 命令の ELF ファイルとは区別されるようになってます. オブジェクトファイルのディスアセンブル †ディスアセンブルしてみます. $ objdump -d arm.o arm.o: ファイル形式 elf32-littlearm セクション .text の逆アセンブル: 00000000 <arm_caller>: 0: e1a0c00d mov ip, sp 4: e92dd800 stmdb sp!, {fp, ip, lr, pc} 8: e24cb004 sub fp, ip, #4 ; 0x4 c: e24dd004 sub sp, sp, #4 ; 0x4 10: e50b0010 str r0, [fp, #-16] 14: e51b0010 ldr r0, [fp, #-16] 18: ebfffffe bl 0 <thumb_callee> 1c: e1a02000 mov r2, r0 20: e51b3010 ldr r3, [fp, #-16] 24: e0823003 add r3, r2, r3 28: e1a00003 mov r0, r3 2c: e24bd00c sub sp, fp, #12 ; 0xc 30: e89d6800 ldmia sp, {fp, sp, lr} 34: e12fff1e bx lr … 関数からの戻りは bx 命令でモード切り替えをするようになってますが,thumb_callee の呼び出しは bl 命令を使っており,このままでは Thumb モードにはスイッチできませんね. もっとも,単体でコンパイルしているのでこの段階ではコンパイラは「thumb_callee が Thumb 命令の関数である」ということを知らないわけで,無理もありません. thumb.o も同様にディスアセンブルしてみます. $ objdump -d thumb.o thumb.o: ファイル形式 elf32-littlearm セクション .text の逆アセンブル: 00000000 <thumb_caller>: 0: b580 push {r7, lr} 2: b081 sub sp, #4 4: af00 add r7, sp, #0 6: 1c3b adds r3, r7, #0 8: 6018 str r0, [r3, #0] a: 1c3b adds r3, r7, #0 c: 681b ldr r3, [r3, #0] e: 1c18 adds r0, r3, #0 10: f7ff fffe bl 0 <arm_callee> 14: 1c02 adds r2, r0, #0 16: 1c3b adds r3, r7, #0 18: 681b ldr r3, [r3, #0] 1a: 18d3 adds r3, r2, r3 1c: 1c18 adds r0, r3, #0 1e: 46bd mov sp, r7 20: b001 add sp, #4 22: bc80 pop {r7} 24: bc02 pop {r1} 26: 4708 bx r1 … 1命令が 16bit になっており,ちゃんと Thumb 命令が使われてますね. 関数呼び出しと関数からのリターンは arm.o と同様のことになってます. リンク †というわけで,おもむろにリンク. $ gcc -o main main.o arm.o thumb.o /usr/bin/ld: Warning: /usr/lib/gcc/arm-linux-gnu/4.1.2/libgcc_s.so does not support interworking, whereas main does /usr/bin/ld: Warning: /lib/libc.so.6 does not support interworking, whereas main does /usr/bin/ld: Warning: /usr/lib/libc_nonshared.a(elf-init.oS) does not support interworking, whereas main does /usr/bin/ld: Warning: /lib/ld-linux.so.2 does not support interworking, whereas main does /usr/bin/ld: Warning: /usr/lib/gcc/arm-linux-gnu/4.1.2/libgcc_s.so does not support interworking, whereas main does /usr/bin/ld: Warning: /usr/lib/gcc/arm-linux-gnu/4.1.2/crtend.o does not support interworking, whereas main does /usr/bin/ld: Warning: /usr/lib/gcc/arm-linux-gnu/4.1.2/../../../crtn.o does not support interworking, whereas main does ELF ヘッダの interworking フラグを見ているようで,warning の嵐. main.o arm.o thumb.o は Thumb Interwork の ELF ファイルですが,libc や crt.o (スタートアップルーチン.main の前に実行される部分)などは 非 Thumb Interwork ファイルなので「形式が違うぞ」と警告しているわけです. とりあえずこのままでも実行ファイルはできているのですが,warning が嫌いという場合には $ gcc -Wl,--no-warn-mismatch -o main main.o arm.o thumb.o これで warning は表示されなくなります. ただし,「表示されない」というだけで問題が解決したわけではないので注意しましょう. 実行ファイルのディスアセンブル †生成された実行ファイルもディスアセンブルしてみましょう. $ objdump -d main …(略)… 0000840c <arm_caller>: 840c: e1a0c00d mov ip, sp 8410: e92dd800 stmdb sp!, {fp, ip, lr, pc} 8414: e24cb004 sub fp, ip, #4 ; 0x4 8418: e24dd004 sub sp, sp, #4 ; 0x4 841c: e50b0010 str r0, [fp, #-16] 8420: e51b0010 ldr r0, [fp, #-16] 8424: eb000060 bl 85ac <__thumb_callee_from_arm> 8428: e1a02000 mov r2, r0 842c: e51b3010 ldr r3, [fp, #-16] 8430: e0823003 add r3, r2, r3 8434: e1a00003 mov r0, r3 8438: e24bd00c sub sp, fp, #12 ; 0xc 843c: e89d6800 ldmia sp, {fp, sp, lr} 8440: e12fff1e bx lr …(略)… 00008498 <thumb_callee>: 8498: b580 push {r7, lr} 849a: b081 sub sp, #4 849c: af00 add r7, sp, #0 849e: 1c3b adds r3, r7, #0 84a0: 6018 str r0, [r3, #0] 84a2: 1c3b adds r3, r7, #0 84a4: 681b ldr r3, [r3, #0] …(略)… 000085ac <__thumb_callee_from_arm>: 85ac: e59fc000 ldr ip, [pc, #0] ; 85b4 <__thumb_callee_from_arm+0x8> 85b0: e12fff1c bx ip 85b4: 00008499 muleq r0, r9, r4 …(略)… 関数呼び出しの間に __thumb_callee_from_arm というルーチンが挟まっていて,arm_caller → __thum_callee_from_arm → arm_callee という経路で呼び出されています. で,__thumb_callee_from_arm の中で ARM → Thumb のモード切り替えが行われています. ARM 社のツールキットの説明では,このように間に挟まってモード切り替えを行うルーチンを「ベニア (veneer)」と呼んでいます. 余談ですが,アドレス 0x85b4 に入っているのはホントは命令ではなくて「thumb_callee の先頭アドレス + thumb フラグ」のデータなのですが,ディスアセンブラは命令として解釈してしまってるようですね. 実行パスを解釈してディスアセンブルしているわけじゃないので,こんなこともよく起こります. 実行 †プログラムを実行してみましょう. $ ./main 3 3 $ echo $? 0 ちゃんと動いてますね. まとめ †Thumb Interwork は ARM Linux 上でもかろうじて使える,のかなぁ? ARM Thumb 命令セット Super Interworking †gcc での Thumb 命令サポートの Super Interworking について. オプションの説明 †gcc の info より抜粋. `-mcallee-super-interworking' Gives all externally visible functions in the file being compiled an ARM instruction set header which switches to Thumb mode before executing the rest of the function. This allows these functions to be called from non-interworking code. `-mcaller-super-interworking' Allows calls via function pointers (including virtual functions) to execute correctly regardless of whether the target code has been compiled for interworking or not. There is a small overhead in the cost of executing a function pointer if this option is enabled. オプションの名前は対称的なんだけど,やってることは非対称な雰囲気ですね. -mcallee-super-interworking のほうは関数先頭で Thumb モードへの切り替えを行い,外面的には ARM 命令セットの関数として振る舞うようです. 一方,-mcaller-super-interworking のほうは関数ポインタでの呼び出しをうまく処理してくれるようです. どう処理してくれるかはこの説明だけではよくわかりませんね. 環境 †今回試した環境は
です. Callee Super Interworking †まずはこっちのほうを試してみましょう. ソース †main.c #include <stdio.h> extern int thumb_callee ( int ); int main ( void ) { printf ( "%d\n", thumb_callee ( 1 ) ); return 0; } thumb.c int thumb_callee ( int a ) { return a + 1; } コンパイル †コンパイルします. main.c は ARM 命令セットで,thumb.c は Thumb 命令セットでコンパイルします. $ gcc -mthumb -mcallee-super-interworking thumb.c -c $ gcc main.c -c ディスアセンブル †$ objdump -d thumb.o thumb.o: ファイル形式 elf32-littlearm セクション .text の逆アセンブル: 00000000 <thumb_callee>: 0: e38fc001 orr ip, pc, #1 ; 0x1 4: e12fff1c bx ip 00000008 <.real_start_ofthumb_callee>: 8: b580 push {r7, lr} a: b081 sub sp, #4 c: af00 add r7, sp, #0 e: 1c3b adds r3, r7, #0 10: 6018 str r0, [r3, #0] 12: 1c3b adds r3, r7, #0 14: 681b ldr r3, [r3, #0] 16: 3301 adds r3, #1 18: 1c18 adds r0, r3, #0 1a: 46bd mov sp, r7 1c: b001 add sp, #4 1e: bc80 pop {r7} 20: bc02 pop {r1} 22: 4708 bx r1 関数先頭で ARM → Thumb のモード切り替えが入っています. 戻り側も bx 命令で戻っているので ARM モードに復帰するようになっています. ELF ヘッダ †ELF ヘッダを見てみます. 両者に違いはないようです. $ readelf -h main.o ELF ヘッダ: マジック: 7f 45 4c 46 01 01 01 61 00 00 00 00 00 00 00 00 クラス: ELF32 データ: 2 の補数、リトルエンディアン バージョン: 1 (current) OS/ABI: ARM ABI バージョン: 0 タイプ: REL (再配置可能ファイル) マシン: ARM バージョン: 0x1 エントリポイントアドレス: 0x0 プログラムの開始ヘッダ: 0 (バイト) セクションヘッダ始点: 232 (バイト) フラグ: 0x0 このヘッダのサイズ: 52 (バイト) プログラムヘッダサイズ: 0 (バイト) プログラムヘッダ数: 0 セクションヘッダ: 40 (バイト) Number of section headers: 10 Section header string table index: 7 $ readelf -h thumb.o ELF ヘッダ: マジック: 7f 45 4c 46 01 01 01 61 00 00 00 00 00 00 00 00 クラス: ELF32 データ: 2 の補数、リトルエンディアン バージョン: 1 (current) OS/ABI: ARM ABI バージョン: 0 タイプ: REL (再配置可能ファイル) マシン: ARM バージョン: 0x1 エントリポイントアドレス: 0x0 プログラムの開始ヘッダ: 0 (バイト) セクションヘッダ始点: 200 (バイト) フラグ: 0x0 このヘッダのサイズ: 52 (バイト) プログラムヘッダサイズ: 0 (バイト) プログラムヘッダ数: 0 セクションヘッダ: 40 (バイト) Number of section headers: 8 Section header string table index: 5 リンク †というわけで,リンクは何も小細工無しに通ってしまいます. $ gcc -o main main.o thumb.o 実行 †もちろん,問題なく実行できます. $ ./main 2 Caller Super Interworking †というわけで,謎の Caller Super Interworking について. ソース †main.c #include <stdio.h> extern int thumb_callee ( int ); int main ( void ) { printf ( "%d\n", thumb_callee ( 1 ) ); return 0; } int arm_callee ( int a ) { return a + 1; } thumb.c extern int arm_callee ( int ); static int (*acallee)( int ) = arm_callee; int thumb_callee ( int a ) { return 1 + acallee ( a ); } main → thumb_callee → arm_callee という経路で関数が呼び出されます. コンパイル †$ gcc -mthumb -mcallee-super-interworking -mcaller-super-interworking thumb.c -c $ gcc main.c -c ディスアセンブル †$ objdump -d thumb.o thumb.o: ファイル形式 elf32-littlearm セクション .text の逆アセンブル: 00000000 <thumb_callee>: 0: e38fc001 orr ip, pc, #1 ; 0x1 4: e12fff1c bx ip 00000008 <.real_start_ofthumb_callee>: 8: b580 push {r7, lr} a: b081 sub sp, #4 c: af00 add r7, sp, #0 e: 1c3b adds r3, r7, #0 10: 6018 str r0, [r3, #0] 12: 4b07 ldr r3, [pc, #28] (30 <.text+0x30>) 14: 681a ldr r2, [r3, #0] 16: 1c3b adds r3, r7, #0 18: 681b ldr r3, [r3, #0] 1a: 1c18 adds r0, r3, #0 1c: f7ff fffe bl 0 <_interwork_call_via_r2> 20: 1c03 adds r3, r0, #0 22: 3301 adds r3, #1 24: 1c18 adds r0, r3, #0 26: 46bd mov sp, r7 28: b001 add sp, #4 2a: bc80 pop {r7} 2c: bc02 pop {r1} 2e: 4708 bx r1 30: 0000 lsls r0, r0, #0 ... この段階では _interwork_call_via_r2 という関数経由で arm_callee を呼び出そうとしているようです. ELF ヘッダ †$ readelf -h thumb.o ELF ヘッダ: マジック: 7f 45 4c 46 01 01 01 61 00 00 00 00 00 00 00 00 クラス: ELF32 データ: 2 の補数、リトルエンディアン バージョン: 1 (current) OS/ABI: ARM ABI バージョン: 0 タイプ: REL (再配置可能ファイル) マシン: ARM バージョン: 0x1 エントリポイントアドレス: 0x0 プログラムの開始ヘッダ: 0 (バイト) セクションヘッダ始点: 228 (バイト) フラグ: 0x0 このヘッダのサイズ: 52 (バイト) プログラムヘッダサイズ: 0 (バイト) プログラムヘッダ数: 0 セクションヘッダ: 40 (バイト) Number of section headers: 10 Section header string table index: 7 ARM 命令セットの obj ファイルと一緒ですね. リンク †というわけでリンクしてみます. $ gcc -o main main.o thumb.o thumb.o: In function `.real_start_ofthumb_callee': thumb.c:(.text+0x1c): undefined reference to `_interwork_call_via_r2' collect2: ld returned 1 exit status 先ほどの _interwork_call_via_r2 が見つからず,リンクが失敗しています. 今回の環境ではこの関数は用意されてないようです. 残念. ARM Thumb 命令セット gcc サポート ひとまずまとめ †いろいろつついてみて,わかったことをひとまずまとめてみる. Thumb Interwork と Super Interworking †これら 2 つの枠組みの性格としては,おそらく
というようなところなのだろう. 根本的な問題 †Thumb Interwork, Super Interworking に共通する問題として C プログラムの場合,ARM モードと Thumb モードの指定はファイル単位でしか行えない,という制限がある. 例えばデータ処理で
コンパイルしたい,なんてことは十分有り得るだろう. で,このようなプログラムの場合,(規模にもよるが)前処理と本処理は1つのソースファイルにまとめるのが普通である. が,gcc の縛りにより,1 つのソースファイル中で任意に ARM モード・Thumb モードの切り替えはできない. フルスクラッチでプログラムを起こす場合は「しょうがないから ARM モードの部分と Thumb モードの部分でソースを分けよう」なんてこともできるが,既存のプログラムを移植するような場合は…ちょっと考えたくない. ソースファイル内で例えば #pragma ARM とかのコンパイラ指令でモードを行き来できたりすると,移植はしやすいんだけどなぁ. |