2017年02月01日
GCC(ARM)のunaligned data accessコード生成
手元にあった弊社製の古い評価ボード KZM-ARM11-01(※1) で、musl libc のテストスイートを、開発中のバージョンの exeGCC(※2) でコンパイルして動かしてみた所、最適化オプション Os だと一部テストが FAIL するが、O2 だと PASS するという現象が発生しました。それだけだとよく聞くような話ですが、分割コンパイルしていて、別ファイルのコードを変更すると、アセンブラソースもオブジェクトファイルも全く同じなのにリンクすると期待通り動かなくなるなど、奇妙な挙動にだいぶ悩まされました。
※1 既にディスコンなので、この URL はそのうち消えるかもしれません。
※2 サイトの exeGCC に関する記述がだいぶ古くてすみません。昨年全面リニューアルの予定だったのですが、私が難病(SLE)を発症してしまい半年ほど入院&自宅療養してましたので…。
問題が発生した fdopen.c からコードを切り出して作った再現コードが以下です。(OS 無しのベアメタル環境ですが、VLINK という、デバッガのコンソールに printf() ができたり、ホスト OS のファイルにアクセスできる機能を使っています。)
しばらく悩んでいるうちに、デバッガで d コマンド(バイトアクセス)なら正しく文字列定数があるアドレスのメモリが見えますが、dd コマンド(4 バイトアクセス)だとおかしく見える(tmp[] の値と同じ)ことに気付き、定数が 4 バイト境界に揃っていないことに気付きました。(遅い)
実際に確認してみると、Os 時に生成されたアセンブラコードには、以下のように .align 指定がありませんでした。
GCC のマニュアルには、ARMv6 以前と、ARMv6-M(まだリリースされていませんが、GCC 7 以降は ARMv8-M も)以外は、デフォルトで -munaligned-access が有効と書いてありました。
試しに -mno-unaligned-access オプションを付けてみた所、期待通りに動作しました。この場合は unaligned data access を前提としたコードがインライン展開されるのではなく、memcpy() を呼ぶようになりました。(デバッガの表示が __aeabi_memcpy になっているのは、現在開発中のバージョンの exeGCC では memcpy の alias として __aeabi_memcpy が定義されているので、アドレスが同じで区別が付かないためでしょう。)
※2 サイトの exeGCC に関する記述がだいぶ古くてすみません。昨年全面リニューアルの予定だったのですが、私が難病(SLE)を発症してしまい半年ほど入院&自宅療養してましたので…。
問題が発生した fdopen.c からコードを切り出して作った再現コードが以下です。(OS 無しのベアメタル環境ですが、VLINK という、デバッガのコンソールに printf() ができたり、ホスト OS のファイルにアクセスできる機能を使っています。)
#include <stdlib.h> #include <stdio.h> #include <unistd.h> int main() { char tmp[] = "/tmp/testsuite-XXXXXX"; /* here */ int fd; // fprintf(stderr, "filename: %s\n", tmp); if ((fd = mkstemp(tmp)) > 2) { if (write(fd, "hello", 6) == 6) { close(fd); if (unlink(tmp) != -1) return 0; } } return -1; }問題は、Os 最適化コンパイル時に、この tmp[] が、デバッガで見ると (char [22])tmp=@82002FB0 "/to\0/tmptsesteuiXX-X\0\0" のように、何やらメチャクチャな値になっているのです。
>gcc -Os -g -Wall Os_test.c当初はデバッガのバグも疑い、printf してみようと思ったのですが、コード中のコメントを外すと再現しなくなってしまいました。そして奇妙なことに、tmp[] を初期化しているアセンブラコードは diff を取っても全く同じにしか見えない(ラベル等は異なりますが)のです。オブジェクトファイルを
>objdump -s -j ".rodata.str1.1" Os_test.o Os_test.o: file format elf32-littlearm Contents of section .rodata.str1.1: 0000 68656c6c 6f002f74 6d702f74 65737473 hello./tmp/tests 0010 75697465 2d585858 58585800 uite-XXXXXX.などしてみても、文字列は正しく入ってる感じですし、コンパイラやアセンブラは無罪そう。(複数バージョンのGCC/binutils を試しましたが、結果は同じでした。)
しばらく悩んでいるうちに、デバッガで d コマンド(バイトアクセス)なら正しく文字列定数があるアドレスのメモリが見えますが、dd コマンド(4 バイトアクセス)だとおかしく見える(tmp[] の値と同じ)ことに気付き、定数が 4 バイト境界に揃っていないことに気付きました。(遅い)
* main() 先頭からステップ実行し、r3 レジスタに定数のアドレスがセットされた直後にデバッガのコマンドを実行 >d _r3 8001C4FE 2F 74 6D 70 2F 74 65 73 74 73 75 69 74 65 2D 58 /tmp/testsuite-X >dd _r3 8001C4FE 006F742F 706D742F 73657374 69756574 /to./tmptsesteuiわかってしまえば簡単なことで、どうも Os だとサイズ最適化なので、定数はアライメントを無視して敷き詰められるようです。
実際に確認してみると、Os 時に生成されたアセンブラコードには、以下のように .align 指定がありませんでした。
.section .rodata.str1.1,"aMS",%progbits,1 .LC1: .ascii "hello\000" .LC0: .ascii "/tmp/testsuite-XXXXXX\000"一方 O2 時には、.rodata セクションの align が 2(GNU as の ARM 版の仕様で 2^2=4 バイトの意味)になっていて、.space 指令で 4 バイト境界になるように調整されています。(.ascii 指令のヌル文字の次の "00" の意味がマニュアルを読んでもよくわからないのですが、objdump の出力を見る限り何も出力されないので、一見ダブルクォーテーションの中は 8 バイトに見えますが、"00" のぶんの .space 2 が必要なようです。)
.section .rodata.str1.4,"aMS",%progbits,1 .align 2 .LC1: .ascii "hello\000" .space 2 .LC0: .ascii "/tmp/testsuite-XXXXXX\000"そして、GCC は、-march=armv6 の場合、unaligned data access を前提としたコードを生成していました。(実行例では march オプション等が指定されていませんが、汎用版の exeGCC にはセットアップツールによって設定された環境変数から適切なオプションを自動的に読み込む機能があります。)
>gcc -dM -E Os_test.c | grep UNALIGN #define __ARM_FEATURE_UNALIGNED 1具体的には、E 番地からループで回しつつ文字列定数を LDR(4 バイトアクセス)でロードして、スタック上の配列にストアするようなコードが生成されていました。CP15 レジスタの A-bit がリセット時には立って無いので例外は出ませんでしたが、正しくロードできていません。これがローカル変数配列の初期化がおかしくなる原因でした。
800082D4 E59F308C LDR r3,80008368 ;=8001C4FE _etext+6 ... Os_test.c:7: char tmp[] = "/tmp/testsuite-XXXXXX"; ... 800082EC E5930000 LDR r0,[r3]分割コンパイルで完全に独立した他のソースコードを弄っただけで挙動が変わったのも、定数の数や内容が変わるたびにアドレスが変わったのが原因でした。たまたま 4 バイト境界に目的の文字列定数が来た時には期待通りに動いたというわけです。
GCC のマニュアルには、ARMv6 以前と、ARMv6-M(まだリリースされていませんが、GCC 7 以降は ARMv8-M も)以外は、デフォルトで -munaligned-access が有効と書いてありました。
試しに -mno-unaligned-access オプションを付けてみた所、期待通りに動作しました。この場合は unaligned data access を前提としたコードがインライン展開されるのではなく、memcpy() を呼ぶようになりました。(デバッガの表示が __aeabi_memcpy になっているのは、現在開発中のバージョンの exeGCC では memcpy の alias として __aeabi_memcpy が定義されているので、アドレスが同じで区別が付かないためでしょう。)
800082D4 E92D407F STMDB sp!,{r0-r6,lr} Os_test.c:7: char tmp[] = "/tmp/testsuite-XXXXXX"; 800082D8 E3A02016 MOV r2,#0x16 800082DC E59F1050 LDR r1,80008334 ;=8001C4CE _etext+6 800082E0 E1A0000D MOV r0,sp 800082E4 EB0001F6 BL 80008AC4 __aeabi_memcpyまた、ちゃんと以下のような unaligned data access を有効にする初期化を追加しても、期待通り動作しました。
void _kmc_c_init(void) { asm(" mrc p15, 0, r0, c1, c0, 0"); asm(" bic r0, r0, #0x00000002"); /* clear A bit (disable alignment fault) */ asm(" orr r0, r0, #0x00400000"); /* set U bit (v6 unaligned data access) */ asm(" mcr p15, 0, r0, c1, c0, 0"); }